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
orgidand API key from Sign up. - At least one contact in your org.
- 1
Create the app
flutter create hello_appmint cd hello_appmintAdd
httpandflutter_dotenvtopubspec.yaml:dependencies: flutter: sdk: flutter http: ^1.2.0 flutter_dotenv: ^5.1.0flutter pub get - 2
Configure env
Create
.envat the project root:APPENGINE_ENDPOINT=https://appengine.appmint.io ORG_ID=your-org-id APPMINT_API_KEY=amk_your_long_lived_keyAdd it to
pubspec.yamlassets so it's bundled:flutter: assets: - .envAdd
.envto.gitignoreimmediately. 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
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,findDataagainst/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
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
Run it
flutter runPick a connected device or simulator. You should see a list of contacts.
A 400 means the
orgidheader is missing — double-check.envloaded. 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.
/repository/find/{datatype}JWT+APIKEYThe 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. SendAuthorization: Bearer <jwt>instead ofx-api-key. - Refresh the JWT via
POST /customer/refreshwhen it expires. The referencehttp_client.dartinappmint_mobileshows the auto-refresh pattern. - Keep
orgidin app config — it's not secret, but it identifies your tenant.
Next
- Glossary
- Concepts — User vs Customer, BaseModel<T>
- Multi-tenancy —
orgidenforcement and principals