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, every platform Flutter supports.

Supported platforms: iOS 13+, Android API 24+, macOS 12+, Windows 10+, Linux (x86_64).

1. Add the dependency

flutter pub add amba

Or in pubspec.yaml:

dependencies:
  amba: ^1.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 rejects server keys at configure time with AmbaException("server key not allowed in client SDK").

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 allAmba.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 on first 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 session token persists across app restarts (amba writes to platform-appropriate storage), so you only mint a new anonymous session on first launch.

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
iOSBundled with the package. Sign in / push via the platform packages above.
AndroidBundled for all four standard ABIs (arm64-v8a, armeabi-v7a, x86_64, x86). Minimum API 24.
macOSBundled with the package. Standard flutter build macos works out of the box.
WindowsBundled with the package. Standard flutter build windows works out of the box.
LinuxBundled with the package. Standard flutter build linux works out of the box.
WebFlutter for Web is not supported by this package. Use @layers/amba-web from JS interop if you target the browser.

Common pitfalls

  • Events silently vanishAmba.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;
    }
  • AmbaException: Failed to load amba at runtime — the bundled artifact didn't ship with your build. Run flutter clean then rebuild; if it persists, verify your pubspec.yaml references amba: ^1.0.0 (not a path: override pointing at an empty checkout) and that flutter pub get ran without errors.
  • 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