Amba

Flutter SDK

Flutter quickstart for the amba Dart package — pubspec dep, Amba.configure(), first track, first collection insert, platform-channel notes for sign-in and push.

Amba for Flutter is published to pub.dev as amba. One package for Flutter apps on iOS and Android.

Supported platforms: iOS 13+ and Android API 24+. The package bundles native code for these two platforms only.

1. Add the dependency

flutter pub add amba

Or in pubspec.yaml:

dependencies:
  amba: ^4.0.0
  flutter:
    sdk: flutter

Run flutter pub get. The package bundles prebuilt native binaries for iOS and Android — no separate build step is needed for those targets.

2. Configure at app start

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:amba/amba.dart';
 
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Amba.configure(
    apiKey: const String.fromEnvironment('AMBA_API_KEY'),
  );
  runApp(const MyApp());
}

Pass the key via --dart-define:

flutter run --dart-define=AMBA_API_KEY=<your client key from amba init>
flutter build apk --dart-define=AMBA_API_KEY=<your client key from amba init>

Amba.configure() is idempotent only for the first call — re-initializing in the same process throws AmbaException("amba already initialized"). To swap tenants, use the SDK's reset flow before re-calling configure. For typical apps this never matters: configure once at boot.

Which key goes here

For a Flutter app, always use the client key that npx @layers/amba init issued for you (printed as clientKey in the CLI output; visible in the amba console under your project's API keys tab). The client key is designed to ship to end-user devices.

Never put a server key in a Flutter build. Server keys are for trusted server contexts only — a server-side cron job, a backend endpoint, an edge function — and grant elevated privileges that would let any reverse-engineered APK/IPA impersonate your tenant. The SDK checks only that the key is non-empty and accepts a server key without an error, so keeping server keys out of the app is up to your build configuration. Server keys start with amb_dev_sk_ or amb_live_sk_; a build check that fails on either prefix catches a misplaced key.

3. Verify the SDK reached amba

After flutter run, check your terminal (or Flutter DevTools → Logging) for the SDK's startup line via dart:developer:

import 'dart:developer' as developer;
import 'package:amba/amba.dart';
 
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  try {
    await Amba.configure(
      apiKey: const String.fromEnvironment('AMBA_API_KEY'),
    );
    final ping = await Amba.diagnostics.ping();
    assert(ping.ok, 'amba: ping returned ok=false — ${ping.error}');
    developer.log(
      'amba: connected, project=${ping.serverProjectId} env=${ping.environment} key=${ping.keyFingerprint} latency=${ping.latencyMs}ms',
      name: 'amba',
    );
  } catch (e, stack) {
    developer.log('amba: verify failed', name: 'amba', error: e, stackTrace: stack);
  }
  runApp(const MyApp());
}

If you see amba: connected, project=prj_... in your terminal, your client key resolved to a real project on the server and the wire round-tripped end-to-end. Compare serverProjectId against the project id you think you configured — a mismatch means a wrong --dart-define is loading. If you don't see the line:

  • No amba: log at all — Amba.configure(...) threw and was swallowed. Wrap with a try/catch (as above) so the actual error surfaces.
  • AmbaException: amba already initialized after hot-reload — expected; hot-reload re-runs main(). See the Common pitfalls section for the standard try/on AmbaException guard.
  • AmbaApiError: unauthorized — the --dart-define=AMBA_API_KEY=... value is empty, malformed, or revoked. Re-run npx @layers/amba init to mint a fresh one.
  • Hangs for >5s with no output — the device can't reach api.amba.dev. On Android emulator, check that you didn't restrict network with a custom DNS proxy; on iOS simulator, check the Mac's connection.

For machine-checkable verify in tests or CI, assert on ok plus the resolved project id:

final ping = await Amba.diagnostics.ping();
assert(ping.ok, 'amba round-trip failed — ${ping.error}');
assert(
  ping.serverProjectId != null,
  'amba round-trip failed — no project id from server',
);

A successful ping with the expected serverProjectId means the key resolved to a real project, the network round-trip completed, and your environment is the one you expected.

4. First sign-in (anonymous)

events.track is authenticated server-side — the request needs a session token. Mint one anonymously at launch:

import 'package:amba/amba.dart';
 
final session = await Amba.auth.signInAnonymously();
debugPrint('signed in as ${session['user']['id']}');

signInAnonymously() returns a Map<String, dynamic>; cast through your own model classes for type safety. The SDK keeps the session token, refresh token, and anonymous id in memory for the life of the process and writes none of them to disk. Each launch begins signed out with a new anonymous id, so sign in on every launch. An anonymous sign-in after a restart creates a new anonymous user; to keep one user across launches, sign them in with a durable method such as Sign in with Apple, Google sign-in, or email.

5. First event

Once a session exists, track engagement events:

await Amba.events.track('app_opened', {'source': 'icon'});

6. Sign in with Apple

Use sign_in_with_apple on iOS, then forward the identity token:

import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:amba/amba.dart';
 
Future<void> signInWithApple() async {
  final credential = await SignInWithApple.getAppleIDCredential(
    scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName],
  );
  if (credential.identityToken != null) {
    await Amba.auth.signInWithApple(credential.identityToken!);
  }
}

Enable the Sign in with Apple capability in Xcode (ios/Runner/Runner.entitlements) before building.

7. Sign in with Google

Use google_sign_in for the OAuth dance, then forward the idToken:

import 'package:google_sign_in/google_sign_in.dart';
import 'package:amba/amba.dart';
 
final _googleSignIn = GoogleSignIn(
  serverClientId: 'YOUR_GOOGLE_OAUTH_WEB_CLIENT_ID',
);
 
Future<void> signInWithGoogle() async {
  final account = await _googleSignIn.signIn();
  if (account == null) return;
  final auth = await account.authentication;
  if (auth.idToken != null) {
    await Amba.auth.signInWithGoogle(auth.idToken!);
  }
}

The serverClientId is the Web OAuth client ID from Google's API credentials console — not iOS or Android. Set up iOS in ios/Runner/Info.plist (CFBundleURLTypes) and Android in android/app/build.gradle per the google_sign_in setup guide.

8. Register for push

Use firebase_messaging on both platforms — it handles APNs registration on iOS and FCM on Android, returning a single token type per platform that you forward to amba:

import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:amba/amba.dart';
 
Future<void> registerForPush() async {
  final messaging = FirebaseMessaging.instance;
  final settings = await messaging.requestPermission();
  if (settings.authorizationStatus != AuthorizationStatus.authorized) return;
 
  final token = Platform.isIOS
      ? await messaging.getAPNSToken()
      : await messaging.getToken();
  if (token == null) return;
 
  await Amba.push.register(
    token,
    Platform.isIOS ? 'apns' : 'fcm',
    bundleId: 'com.example.myapp',
  );
}

Add the Push Notifications capability + Background Modes → Remote notifications in Xcode for iOS. For Android, add Firebase the standard way (google-services.json in android/app/).

9. First collection insert

import 'package:amba/amba.dart';
 
final inserted = await Amba.collections.insert(
  'posts',
  {'title': 'Hello amba', 'body': 'first post from Flutter'},
);
 
final response = await Amba.collections.find('posts', {
  'order': [{'column': 'created_at', 'direction': 'desc'}],
  'limit': 20,
});
debugPrint('got ${(response['data'] as List).length} posts');

For typed reads, cast through your own model classes or generate them with json_serializable.

10. AI proxy

final response = await Amba.ai.anthropic.messages.create({
  'prompt_slug': 'support_assistant',
  'variables': {'user_query': 'How do I cancel?'},
  'max_tokens': 1024,
});
debugPrint(response.toString());

Calling SDK methods from build()

Every Amba.* method returns a Future — don't await them from build() (it's synchronous). Use FutureBuilder for one-shot reads, or kick the call from initState and setState when it completes:

class PostsScreen extends StatefulWidget {
  const PostsScreen({super.key});
  @override
  State<PostsScreen> createState() => _PostsScreenState();
}
 
class _PostsScreenState extends State<PostsScreen> {
  late final Future<dynamic> _posts;
 
  @override
  void initState() {
    super.initState();
    _posts = Amba.collections.find('posts', {'limit': 20});
  }
 
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _posts,
      builder: (context, snap) {
        if (!snap.hasData) return const CircularProgressIndicator();
        final data = (snap.data as Map)['data'] as List;
        return ListView.builder(
          itemCount: data.length,
          itemBuilder: (_, i) => ListTile(title: Text(data[i]['title'])),
        );
      },
    );
  }
}

For mutations triggered by user interaction (button taps, form submits), wrap the call in async directly on the handler — that's the only context where await is allowed inside a widget tree:

ElevatedButton(
  onPressed: () async {
    try {
      await Amba.events.track('button_tapped');
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('failed: $e')),
      );
    }
  },
  child: const Text('Track'),
)

Always check mounted before touching context after an await — the widget may have been disposed mid-call.

Hot reload + hot restart

  • Hot reload (r) preserves widget state, so Amba.configure(...) doesn't re-run. Safe.
  • Hot restart (R) re-runs main() from the top. Amba.configure(...) will throw AmbaException('amba already initialized') on the second invocation because the SDK's process-global state survives. Guard your main() with the try/on AmbaException pattern in Common pitfalls so a stale state doesn't cascade into a red screen.

Constructor DI for tests

// test/my_test.dart
import 'package:amba/amba.dart';
import 'package:flutter_test/flutter_test.dart';
 
class FakeBindings implements AmbaBindings {
  // implement the abstract methods with in-memory fakes
}
 
void main() {
  test('client sends correct payload', () async {
    final client = AmbaClient(bindings: FakeBindings());
    await client.initialize(apiKey: 'test');
    await client.events.track('test_event');
    // assert on the fake's recorded calls
  });
}

AmbaClient accepts an AmbaBindings via the constructor — tests inject a fake, production code goes through the singleton Amba.configure(...) which wires real native bindings.

Platform notes

PlatformPrebuiltNotes
iOS✓Bundled for devices (arm64) and simulators (arm64, x86_64). Minimum iOS 13. Sign in / push via the platform packages above.
Android✓Bundled for all four standard ABIs (arm64-v8a, armeabi-v7a, x86_64, x86). Minimum API 24.
macOS—No native code ships for macOS. The SDK can't load on a macOS build.
Windows—No native code ships for Windows. The SDK can't load on a Windows build.
Linux—No native code ships for Linux. The SDK can't load on a Linux build.
Web—Flutter for Web is not supported by this package. Use @layers/amba-web from JS interop if you target the browser.

4.0 surface highlights

// users — fetch + update profiles by id.
final me = await Amba.users.get();
final other = await Amba.users.get(userId: 'usr_abc');                  // public-safe other user
await Amba.users.update({'display_name': 'Ada'});                        // self
await Amba.users.update({'display_name': 'Ada'}, userId: 'usr_abc');    // requires users:write
 
// friends — unfriend by user id.
await Amba.friends.removeFriend('usr_abc');                             // NEW in 4.0
 
// flags — single-key lookup; null on unknown/disabled.
final flag = await Amba.flags.get('new_onboarding');
 
// sessions / sync / leagues — new namespaces.
final sessions = await Amba.sessions.list();
await Amba.sessions.revoke(sessions[0]['id'] as String);
final meLeague = await Amba.leagues.me();
final cohort = await Amba.leagues.cohort();
 
// content — today() returns the named schedule's item for today.
final today = await Amba.content.today();
final lib = await Amba.content.library('Daily Tips', 20);
final item = await Amba.content.item(lib.first.id);
 
// messaging — `conversations` is canonical.
final convos = await Amba.messaging.conversations();
final message = await Amba.messaging.getMessage(conversationId, messageId);
 
// storage — list returns the cursor envelope.
final list = await Amba.storage.list(prefix: 'avatars/');
// list['data'], list['next_cursor']

Common pitfalls

  • Events silently vanish — Amba.events.track queues, batches, and retries by design; a wrong key, wrong env, or revoked credential does not throw at the call site. Always run the verify step in section 3 on every fresh build so a misconfiguration shows up as a loud failure, not silence.
  • Wrong env in a build flavour — a clientKey minted for staging will happily configure against the production stack and write to the wrong project. The verify snippet in section 3 prints the server-resolved serverProjectId + environment on every cold start; compare against the values your build expects on first launch instead of finding the drift in retention dashboards a week later.
  • Amba.configure not called before any other Amba.* access throws StateError("Amba.configure must be called before accessing Amba.*"). Always configure in main() after WidgetsFlutterBinding.ensureInitialized().
  • Hot-restart re-running main() — calling Amba.configure() again throws AmbaException("amba already initialized") on the second run. Wrap with a try/catch for development:
    try {
      await Amba.configure(apiKey: ...);
    } on AmbaException catch (e) {
      if (!e.message.contains('already initialized')) rethrow;
    }
  • Failed to load dynamic library at the first Amba.configure (on iOS, Failed to load dynamic library 'amba_core.framework/amba_core') — the bundled native library didn't ship with your build. Run flutter clean then rebuild; if it persists, verify your pubspec.yaml references amba: ^4.0.7 (not a path: override pointing at an empty checkout) and that flutter pub get ran without errors.
  • Failed to lookup symbol 'amba_init' on iOS, or Library 'amba_core' not found in a simulator build — the app uses a version before 4.0.7, whose iOS library the app's build dropped. Upgrade to amba: ^4.0.7.
  • Apple sign-in works in simulator, fails on device — the entitlement requires the app to be signed with a provisioning profile that includes the Sign in with Apple capability. Re-fetch the profile in Xcode after enabling.

See also