Documentation

Hello World (Mobile)

Stand up a Flutter app, talk to AppEngine, render a list of contacts.

The mobile version of the same one-call-and-done quickstart. By the end you'll have a Flutter screen pulling contacts from your org. We'll use a thin SDK-style wrapper modeled after appmint_mobile/lib/services/ — clean enough to grow into.

Prerequisites

  • Flutter 3.16+ (flutter --version).
  • Android Studio or Xcode set up for whichever platform you'll run on.
  • An orgid and API key from Sign up.
  • At least one contact in your org.
  1. 1

    Create the app

    flutter create hello_appmint
    cd hello_appmint
    

    Add http and flutter_dotenv to pubspec.yaml:

    dependencies:
      flutter:
        sdk: flutter
      http: ^1.2.0
      flutter_dotenv: ^5.1.0
    
    flutter pub get
    
  2. 2

    Configure env

    Create .env at the project root:

    APPENGINE_ENDPOINT=https://appengine.appmint.io
    ORG_ID=your-org-id
    APPMINT_API_KEY=amk_your_long_lived_key
    

    Add it to pubspec.yaml assets so it's bundled:

    flutter:
      assets:
        - .env
    

    Add .env to .gitignore immediately. The API key is sensitive — once shipped in an app binary it can be extracted. For production, the right pattern is a short-lived JWT signed for the user, not a long-lived API key in the bundle. The key is fine for development.

  3. 3

    Write the SDK wrapper

    Create lib/appmint/appmint_client.dart:

    import 'dart:convert';
    import 'package:http/http.dart' as http;
    import 'package:flutter_dotenv/flutter_dotenv.dart';
    
    class AppmintClient {
      AppmintClient._();
      static final AppmintClient instance = AppmintClient._();
    
      String get _host => dotenv.env['APPENGINE_ENDPOINT']!;
      String get _orgId => dotenv.env['ORG_ID']!;
      String get _apiKey => dotenv.env['APPMINT_API_KEY']!;
    
      Map<String, String> get _headers => {
            'Content-Type': 'application/json',
            'orgid': _orgId,
            'x-api-key': _apiKey,
          };
    
      Future<Map<String, dynamic>> findData(
        String datatype, {
        Map<String, dynamic> query = const {},
        Map<String, dynamic> options = const {'pageSize': 50},
      }) async {
        final uri = Uri.parse('$_host/repository/find/$datatype');
        final res = await http.post(
          uri,
          headers: _headers,
          body: jsonEncode({'query': query, 'options': options}),
        );
        if (res.statusCode < 200 || res.statusCode >= 300) {
          throw Exception('AppEngine ${res.statusCode}: ${res.body}');
        }
        return jsonDecode(res.body) as Map<String, dynamic>;
      }
    }
    

    This mirrors the shape used in appmint_mobile/lib/services/repository_service.dart — singleton, findData against /repository/find/:datatype, query + options in the body.

    The real mobile SDK adds token refresh, structured errors, and a session-aware HTTP client. For a hello-world we keep it deliberately small. Extend toward the full pattern as you need it.

  4. 4

    Render the list

    Replace lib/main.dart:

    import 'package:flutter/material.dart';
    import 'package:flutter_dotenv/flutter_dotenv.dart';
    import 'appmint/appmint_client.dart';
    
    Future<void> main() async {
      await dotenv.load(fileName: '.env');
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(home: ContactsScreen());
      }
    }
    
    class ContactsScreen extends StatefulWidget {
      const ContactsScreen({super.key});
      @override
      State<ContactsScreen> createState() => _ContactsScreenState();
    }
    
    class _ContactsScreenState extends State<ContactsScreen> {
      late Future<List<dynamic>> _future;
    
      @override
      void initState() {
        super.initState();
        _future = _load();
      }
    
      Future<List<dynamic>> _load() async {
        final res = await AppmintClient.instance
            .findData('contact', options: {'pageSize': 20});
        return (res['data'] ?? []) as List<dynamic>;
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Contacts')),
          body: FutureBuilder<List<dynamic>>(
            future: _future,
            builder: (context, snap) {
              if (snap.connectionState != ConnectionState.done) {
                return const Center(child: CircularProgressIndicator());
              }
              if (snap.hasError) {
                return Center(child: Text('Error: ${snap.error}'));
              }
              final rows = snap.data ?? [];
              return ListView.separated(
                itemCount: rows.length,
                separatorBuilder: (_, __) => const Divider(height: 1),
                itemBuilder: (_, i) {
                  final data = (rows[i]['data'] ?? {}) as Map<String, dynamic>;
                  final name = '${data['firstName'] ?? ''} ${data['lastName'] ?? ''}'.trim();
                  return ListTile(
                    title: Text(name.isEmpty ? '(no name)' : name),
                    subtitle: Text(data['email'] ?? ''),
                  );
                },
              );
            },
          ),
        );
      }
    }
    
  5. 5

    Run it

    flutter run
    

    Pick a connected device or simulator. You should see a list of contacts.

    A 400 means the orgid header is missing — double-check .env loaded. A 401 means the API key is rejected. An empty list means the call worked but you have no contacts.

What just happened

Your app sent POST /repository/find/contact with the orgid and x-api-key headers. AppEngine returned a BaseModel<Contact>[] payload; you read row['data'] to get the contact fields.

POST/repository/find/{datatype}JWT+APIKEY

The same call shape works for every collection — product, order, lead, custom collections. Swap the datatype and the typed payload changes; the call doesn't.

Production notes

For a real app, replace the API-key-in-bundle pattern with one of these:

  • Have the user sign in via POST /profile/customer/signin (or magic link) and store the returned JWT in secure storage. Send Authorization: Bearer <jwt> instead of x-api-key.
  • Refresh the JWT via POST /customer/refresh when it expires. The reference http_client.dart in appmint_mobile shows the auto-refresh pattern.
  • Keep orgid in app config — it's not secret, but it identifies your tenant.

Next