Flutter SDK (demo)
Use Digital Anumati consent in Flutter apps on Android and iOS through digital_anumati_consent, a thin plugin over the native SDKs.
DEMO / FICTIONAL SDK — NOT A REAL DIGITAL ANUMATI SDK
Overview
The plugin exposes the native Android and iOS SDKs to Dart. It only serializes arguments, calls the native SDK, and converts results, events, and errors. Consent storage, consent rules, validation, backend communication, and the consent screen all stay in native code, so a Flutter app behaves exactly like a native one.
Architecture
Consent Backend | +--------------+--------------+ | | Android Native SDK iOS Native SDK (consent logic, storage, (consent logic, storage, consent UI, networking) consent UI, networking) | | +-------+-------+ +-------+-------+ | | | | Flutter React Native Flutter React Native plugin native module plugin native module (Kotlin) (Kotlin) (Swift) (Swift) | | | | +-------+-------+-------------+-------+-------+ | | Dart API (Flutter) TypeScript API (React Native) | | +--------- Host application --+Two channels connect Dart to the native plugin code. A MethodChannel fits commands — every call such as getConsentStatus() is one request with one response or error. An EventChannel fits SDK events — the native SDK pushes them at any time (for example when the user saves preferences in the native screen), and Dart receives them as a Stream.
| Channel | Name | Used for |
|---|---|---|
| MethodChannel | com.digitalanumati/consent | initialize, showConsent, getConsentStatus, getConsentPreferences, updateConsent, revokeConsent, isInitialized |
| EventChannel | com.digitalanumati/consent/events | SDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED, CONSENT_UPDATED, CONSENT_REVOKED, CONSENT_ERROR |
Installation
Add the package to your app:
# pubspec.yaml of your appdependencies: flutter: sdk: flutter # DEMO / FICTIONAL package digital_anumati_consent: ^1.0.0Then run flutter pub get, and cd ios && pod install for iOS. The plugin brings in the native SDK on each platform — no extra native setup is needed beyond minSdk 24 on Android and iOS 15 on iOS.
// android/build.gradle (plugin) — pulls in the native SDKdependencies { implementation "com.digitalanumati:consent-sdk:1.0.0" // DEMO / FICTIONAL implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"}# ios/digital_anumati_consent.podspec — pulls in the native SDKPod::Spec.new do |s| s.name = 'digital_anumati_consent' s.version = '1.0.0' s.summary = 'DEMO / FICTIONAL Flutter bridge for the Digital Anumati consent SDK.' s.homepage = 'https://github.com/example/digital_anumati_consent' s.license = { :type => 'MIT' } s.author = 'Demo' s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.platform = :ios, '15.0' s.swift_version = '5.9' s.dependency 'Flutter' s.dependency 'DigitalAnumatiConsent', '~> 1.0' # DEMO / FICTIONALendUsage
import 'package:digital_anumati_consent/digital_anumati_consent.dart'; // 1. Initialize once, at app start.await DigitalAnumatiConsent.initialize( siteKey: 'demo-site-key-123', applicationId: 'demo-mobile-app-123', environment: Environment.uat,); // 2. Listen to SDK events.DigitalAnumatiConsent.events.listen((event) { if (event.type == 'CONSENT_ERROR') { debugPrint('Consent error: ' + event.error!.code); }}); // 3. Ask for consent if the user hasn't decided yet.if (await DigitalAnumatiConsent.getConsentStatus() == ConsentStatus.notDetermined) { final result = await DigitalAnumatiConsent.showConsent(); debugPrint('Consent status: ' + result.status.wire);} // 4. Read or change preferences, or revoke.final preferences = await DigitalAnumatiConsent.getConsentPreferences();await DigitalAnumatiConsent.updateConsent(const ConsentPreferences(analytics: true));await DigitalAnumatiConsent.revokeConsent();MethodChannel contract
Channel com.digitalanumati/consent. Arguments and return values use Flutter's standard codec (maps, strings, booleans, ints). Errors are returned as PlatformException(code, message) with a code from the error contract.
| Method | Arguments | Return value | Android implementation | iOS implementation | Dart API |
|---|---|---|---|---|---|
| initialize | { "siteKey": String, "applicationId": String, "environment": "uat" | "production" } | null | DigitalAnumati.initialize(context, siteKey, applicationId, env) | try DigitalAnumati.initialize(siteKey:applicationId:environment:) | Future<void> initialize({siteKey, applicationId, environment}) |
| showConsent | none | { "status": String, "preferences": Map, "timestamp": int } | DigitalAnumati.showConsent(activity, listener) | try await DigitalAnumati.showConsent(from:) | Future<ConsentResult> showConsent() |
| getConsentStatus | none | "NOT_DETERMINED" | "GRANTED" | "DENIED" | "PARTIAL" | DigitalAnumati.getConsentStatus().name | DigitalAnumati.getConsentStatus().rawValue | Future<ConsentStatus> getConsentStatus() |
| getConsentPreferences | none | { "necessary": true, "analytics": bool, "personalization": bool, "advertising": bool } | DigitalAnumati.getConsentPreferences() | DigitalAnumati.getConsentPreferences() | Future<ConsentPreferences> getConsentPreferences() |
| updateConsent | { "analytics": bool, "personalization": bool, "advertising": bool } | ConsentResult map (as showConsent) | DigitalAnumati.updateConsent(preferences) (coroutine) | try await DigitalAnumati.updateConsent(_:) | Future<ConsentResult> updateConsent(ConsentPreferences) |
| revokeConsent | none | null | DigitalAnumati.revokeConsent() (coroutine) | try await DigitalAnumati.revokeConsent() | Future<void> revokeConsent() |
| isInitialized | none | bool | DigitalAnumati.isInitialized() | DigitalAnumati.isInitialized | Future<bool> isInitialized() |
timestamp is epoch milliseconds; Dart converts it to a DateTime. necessary is always true and is never sent to updateConsent.
EventChannel events
Channel com.digitalanumati/consent/events. Each event is a map: { "type": String, "result"?: ConsentResult map, "error"?: { "code", "message" } }.
| Event | When | Payload |
|---|---|---|
| SDK_INITIALIZED | initialize() succeeded | — |
| CONSENT_SHOWN | The native consent screen opened | — |
| CONSENT_ACCEPTED | The user saved and every optional category is on (GRANTED) | result |
| CONSENT_REJECTED | The user saved and every optional category is off (DENIED) | result |
| CONSENT_UPDATED | A PARTIAL choice was saved, or updateConsent() changed preferences | result |
| CONSENT_REVOKED | revokeConsent() completed; status is NOT_DETERMINED | — |
| CONSENT_ERROR | A background operation failed (for example a sync) | error |
final subscription = DigitalAnumatiConsent.events.listen((event) { switch (event.type) { case 'CONSENT_ACCEPTED': case 'CONSENT_REJECTED': case 'CONSENT_UPDATED': debugPrint('New status: ' + event.result!.status.wire); case 'CONSENT_REVOKED': debugPrint('Consent revoked'); case 'CONSENT_ERROR': debugPrint('Consent error: ' + event.error!.message); }}); // Later, e.g. in dispose():await subscription.cancel();The native side of the stream is in the bridge code below: onListen registers a listener with the native SDK and forwards each event; onCancel removes it.
Consent UI
The consent screen — “Privacy & Consent” with Necessary, Analytics, Personalization, and Advertising, plus Accept All, Reject All, and Save Preferences — is owned by the native SDK. showConsent() asks the native SDK to present it and returns the result. Try the screen.
| Native SDK-owned UI (chosen) | Flutter-owned UI | |
|---|---|---|
| Consistency | Identical on native, Flutter and React Native apps | Each framework re-implements the screen |
| Consent rules | Stay in one place (native SDK) | Risk of diverging rules in Dart |
| Updates | Screen changes ship with the native SDK | Every framework package must be updated |
| Look and feel | Themed through SDK configuration, not Flutter widgets | Full control with Flutter widgets |
| Compliance review | One screen to review | One screen per framework |
Plugin code
Package layout:
digital_anumati_consent/ # DEMO / FICTIONAL Flutter plugin├── lib/│ ├── digital_anumati_consent.dart # public exports│ └── src/│ ├── channels.dart # channel names│ ├── models.dart # ConsentStatus, ConsentPreferences, ConsentResult, ConsentError, ConsentEvent, Environment│ └── digital_anumati_consent.dart # DigitalAnumatiConsent API (MethodChannel + EventChannel)├── android/│ ├── build.gradle # depends on com.digitalanumati:consent-sdk│ └── src/main/kotlin/com/digitalanumati/consent/flutter/│ └── DigitalAnumatiConsentPlugin.kt # Kotlin bridge├── ios/│ ├── digital_anumati_consent.podspec # depends on DigitalAnumatiConsent│ └── Classes/│ └── DigitalAnumatiConsentPlugin.swift # Swift bridge├── example/ # anumati_flutter_demo│ ├── lib/main.dart│ ├── android/│ └── ios/├── test/│ └── digital_anumati_consent_test.dart├── pubspec.yaml└── README.mdname: digital_anumati_consentdescription: DEMO / FICTIONAL Flutter plugin that wraps the Digital Anumati native consent SDKs.version: 1.0.0publish_to: none # demo only — not published on pub.dev environment: sdk: ">=3.3.0 <4.0.0" flutter: ">=3.19.0" dependencies: flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter: plugin: platforms: android: package: com.digitalanumati.consent.flutter pluginClass: DigitalAnumatiConsentPlugin ios: pluginClass: DigitalAnumatiConsentPluginDart API & models
// lib/digital_anumati_consent.dartexport 'src/digital_anumati_consent.dart';export 'src/models.dart';// lib/src/channels.dartimport 'package:flutter/services.dart'; /// Commands: request/response calls into the native SDK.const methodChannel = MethodChannel('com.digitalanumati/consent'); /// Events: a continuous stream pushed from the native SDK.const eventChannel = EventChannel('com.digitalanumati/consent/events');// lib/src/models.dart — plain data types. No consent rules live here.import 'package:flutter/services.dart'; enum Environment { uat('uat'), production('production'); const Environment(this.wire); final String wire;} enum ConsentStatus { notDetermined('NOT_DETERMINED'), granted('GRANTED'), denied('DENIED'), partial('PARTIAL'); const ConsentStatus(this.wire); final String wire; static ConsentStatus fromWire(String value) => values.firstWhere( (status) => status.wire == value, orElse: () => ConsentStatus.notDetermined, );} enum ConsentCategory { necessary, analytics, personalization, advertising } class ConsentPreferences { const ConsentPreferences({ this.analytics = false, this.personalization = false, this.advertising = false, }); /// Always on; the native SDK enforces this. bool get necessary => true; final bool analytics; final bool personalization; final bool advertising; factory ConsentPreferences.fromMap(Map<Object?, Object?> map) => ConsentPreferences( analytics: map['analytics'] as bool? ?? false, personalization: map['personalization'] as bool? ?? false, advertising: map['advertising'] as bool? ?? false, ); Map<String, Object> toMap() => { 'analytics': analytics, 'personalization': personalization, 'advertising': advertising, };} class ConsentResult { const ConsentResult({ required this.status, required this.preferences, required this.timestamp, }); final ConsentStatus status; final ConsentPreferences preferences; final DateTime timestamp; factory ConsentResult.fromMap(Map<Object?, Object?> map) => ConsentResult( status: ConsentStatus.fromWire(map['status'] as String), preferences: ConsentPreferences.fromMap(map['preferences'] as Map<Object?, Object?>), timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp'] as int), );} class ConsentError implements Exception { const ConsentError(this.code, this.message); /// One of the error-contract codes, e.g. NETWORK_ERROR or USER_CANCELLED. final String code; final String message; factory ConsentError.fromPlatformException(PlatformException e) => ConsentError(e.code, e.message ?? 'Unknown error'); @override String toString() => 'ConsentError($code): $message';} class ConsentEvent { const ConsentEvent({required this.type, this.result, this.error}); /// SDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED, /// CONSENT_UPDATED, CONSENT_REVOKED or CONSENT_ERROR. final String type; final ConsentResult? result; final ConsentError? error; factory ConsentEvent.fromMap(Map<Object?, Object?> map) { final result = map['result'] as Map<Object?, Object?>?; final error = map['error'] as Map<Object?, Object?>?; return ConsentEvent( type: map['type'] as String, result: result == null ? null : ConsentResult.fromMap(result), error: error == null ? null : ConsentError(error['code'] as String, error['message'] as String), ); }}// lib/src/digital_anumati_consent.dartimport 'package:flutter/services.dart'; import 'channels.dart';import 'models.dart'; /// DEMO / FICTIONAL — a thin wrapper over the native SDKs. It only converts types and errors;/// consent storage, rules, validation and networking all stay in the native SDK.class DigitalAnumatiConsent { DigitalAnumatiConsent._(); /// SDK events. Broadcast stream: listen from as many places as you need. static final Stream<ConsentEvent> events = eventChannel .receiveBroadcastStream() .map((event) => ConsentEvent.fromMap(event as Map<Object?, Object?>)); static Future<void> initialize({ required String siteKey, required String applicationId, Environment environment = Environment.uat, }) => _invoke<void>('initialize', { 'siteKey': siteKey, 'applicationId': applicationId, 'environment': environment.wire, }); /// Shows the native, SDK-owned consent screen and returns the user's choice. static Future<ConsentResult> showConsent() async => ConsentResult.fromMap((await _invoke<Map<Object?, Object?>>('showConsent'))!); static Future<ConsentStatus> getConsentStatus() async => ConsentStatus.fromWire((await _invoke<String>('getConsentStatus'))!); static Future<ConsentPreferences> getConsentPreferences() async => ConsentPreferences.fromMap((await _invoke<Map<Object?, Object?>>('getConsentPreferences'))!); static Future<ConsentResult> updateConsent(ConsentPreferences preferences) async => ConsentResult.fromMap( (await _invoke<Map<Object?, Object?>>('updateConsent', preferences.toMap()))!, ); static Future<void> revokeConsent() => _invoke<void>('revokeConsent'); static Future<bool> isInitialized() async => (await _invoke<bool>('isInitialized')) ?? false; static Future<T?> _invoke<T>(String method, [Map<String, Object>? arguments]) async { try { return await methodChannel.invokeMethod<T>(method, arguments); } on PlatformException catch (e) { throw ConsentError.fromPlatformException(e); } }}Android bridge
Kotlin plugin: maps channel arguments to native SDK calls, runs suspend calls in a coroutine, and turns ConsentError into result.error(code, message).
// android/src/main/kotlin/com/digitalanumati/consent/flutter/DigitalAnumatiConsentPlugin.ktpackage com.digitalanumati.consent.flutter import android.app.Activityimport android.content.Contextimport com.digitalanumati.consent.ConsentErrorimport com.digitalanumati.consent.ConsentErrorCodeimport com.digitalanumati.consent.ConsentEventimport com.digitalanumati.consent.ConsentListenerimport com.digitalanumati.consent.ConsentPreferencesimport com.digitalanumati.consent.ConsentResultimport com.digitalanumati.consent.DigitalAnumatiimport com.digitalanumati.consent.Environmentimport io.flutter.embedding.engine.plugins.FlutterPluginimport io.flutter.embedding.engine.plugins.activity.ActivityAwareimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBindingimport io.flutter.plugin.common.EventChannelimport io.flutter.plugin.common.MethodCallimport io.flutter.plugin.common.MethodChannelimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.cancelimport kotlinx.coroutines.launch /** DEMO / FICTIONAL — translates channel calls to the native SDK. No consent logic here. */class DigitalAnumatiConsentPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, EventChannel.StreamHandler, ActivityAware { private lateinit var methodChannel: MethodChannel private lateinit var eventChannel: EventChannel private lateinit var context: Context private var activity: Activity? = null private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { context = binding.applicationContext methodChannel = MethodChannel(binding.binaryMessenger, "com.digitalanumati/consent") methodChannel.setMethodCallHandler(this) eventChannel = EventChannel(binding.binaryMessenger, "com.digitalanumati/consent/events") eventChannel.setStreamHandler(this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodChannel.setMethodCallHandler(null) eventChannel.setStreamHandler(null) DigitalAnumati.setEventListener(null) scope.cancel() } // Flutter calls this on the platform (main) thread. override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { reportErrors(result) { when (call.method) { "initialize" -> { DigitalAnumati.initialize( context = context, siteKey = call.argument<String>("siteKey").orEmpty(), applicationId = call.argument<String>("applicationId").orEmpty(), environment = if (call.argument<String>("environment") == "production") { Environment.PRODUCTION } else { Environment.UAT }, ) result.success(null) } "isInitialized" -> result.success(DigitalAnumati.isInitialized()) "getConsentStatus" -> result.success(DigitalAnumati.getConsentStatus().name) "getConsentPreferences" -> result.success(DigitalAnumati.getConsentPreferences().toMap()) "showConsent" -> showConsent(result) "updateConsent" -> scope.launch { reportErrors(result) { result.success(DigitalAnumati.updateConsent(call.toPreferences()).toMap()) } } "revokeConsent" -> scope.launch { reportErrors(result) { DigitalAnumati.revokeConsent() result.success(null) } } else -> result.notImplemented() } } } private fun showConsent(result: MethodChannel.Result) { val current = activity ?: return result.error( ConsentErrorCode.UNKNOWN_ERROR.name, "No foreground activity to present the consent screen.", null, ) DigitalAnumati.showConsent(current, object : ConsentListener { override fun onConsentCompleted(consent: ConsentResult) = result.success(consent.toMap()) override fun onError(error: ConsentError) = result.error(error.code.name, error.message, null) }) } /** Native ConsentError -> PlatformException(code, message) on the Dart side. */ private inline fun reportErrors(result: MethodChannel.Result, block: () -> Unit) { try { block() } catch (e: ConsentError) { result.error(e.code.name, e.message, null) } catch (e: Exception) { result.error(ConsentErrorCode.UNKNOWN_ERROR.name, e.message, null) } } // EventChannel: forward every native SDK event to Dart (the SDK emits on the main thread). override fun onListen(arguments: Any?, events: EventChannel.EventSink) { DigitalAnumati.setEventListener { event -> events.success(event.toMap()) } } override fun onCancel(arguments: Any?) { DigitalAnumati.setEventListener(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity } override fun onDetachedFromActivityForConfigChanges() { activity = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity } override fun onDetachedFromActivity() { activity = null }} // ---- Type conversions (native -> channel) ------------------------------------------------- private fun ConsentPreferences.toMap(): Map<String, Boolean> = mapOf( "necessary" to necessary, "analytics" to analytics, "personalization" to personalization, "advertising" to advertising,) private fun ConsentResult.toMap(): Map<String, Any> = mapOf( "status" to status.name, // enum -> "GRANTED" "preferences" to preferences.toMap(), "timestamp" to timestamp, // Long epoch millis -> Dart int) private fun ConsentEvent.toMap(): Map<String, Any?> = mapOf( "type" to type.name, "result" to result?.toMap(), "error" to error?.let { mapOf("code" to it.code.name, "message" to it.message) },) private fun MethodCall.toPreferences() = ConsentPreferences( analytics = argument<Boolean>("analytics") ?: false, personalization = argument<Boolean>("personalization") ?: false, advertising = argument<Boolean>("advertising") ?: false,)iOS bridge
Swift plugin: the same mapping, using async/await and FlutterError(code:message:details:).
// ios/Classes/DigitalAnumatiConsentPlugin.swiftimport Flutterimport UIKitimport DigitalAnumatiConsent /// DEMO / FICTIONAL — translates channel calls to the native SDK. No consent logic here.public final class DigitalAnumatiConsentPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { public static func register(with registrar: FlutterPluginRegistrar) { let instance = DigitalAnumatiConsentPlugin() let methods = FlutterMethodChannel( name: "com.digitalanumati/consent", binaryMessenger: registrar.messenger() ) registrar.addMethodCallDelegate(instance, channel: methods) let events = FlutterEventChannel( name: "com.digitalanumati/consent/events", binaryMessenger: registrar.messenger() ) events.setStreamHandler(instance) } // Flutter calls this on the main thread. public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let args = call.arguments as? [String: Any] ?? [:] switch call.method { case "initialize": do { try DigitalAnumati.initialize( siteKey: args["siteKey"] as? String ?? "", applicationId: args["applicationId"] as? String ?? "", environment: args["environment"] as? String == "production" ? .production : .uat ) result(nil) } catch { result(flutterError(from: error)) } case "isInitialized": result(DigitalAnumati.isInitialized) case "getConsentStatus": result(DigitalAnumati.getConsentStatus().rawValue) case "getConsentPreferences": result(DigitalAnumati.getConsentPreferences().asDictionary) case "showConsent": Task { @MainActor in guard let presenter = topViewController() else { result(FlutterError(code: "UNKNOWN_ERROR", message: "No view controller to present from.", details: nil)) return } do { result(try await DigitalAnumati.showConsent(from: presenter).asDictionary) } catch { result(flutterError(from: error)) } } case "updateConsent": let preferences = ConsentPreferences( analytics: args["analytics"] as? Bool ?? false, personalization: args["personalization"] as? Bool ?? false, advertising: args["advertising"] as? Bool ?? false ) Task { @MainActor in do { result(try await DigitalAnumati.updateConsent(preferences).asDictionary) } catch { result(flutterError(from: error)) } } case "revokeConsent": Task { @MainActor in do { try await DigitalAnumati.revokeConsent() result(nil) } catch { result(flutterError(from: error)) } } default: result(FlutterMethodNotImplemented) } } // EventChannel: forward every native SDK event to Dart (the SDK emits on the main thread). public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { DigitalAnumati.setEventHandler { event in events(event.asDictionary) } return nil } public func onCancel(withArguments arguments: Any?) -> FlutterError? { DigitalAnumati.setEventHandler(nil) return nil }} // ---- Helpers and type conversions (native -> channel) -------------------------------------- /// Native ConsentError -> FlutterError(code, message) -> PlatformException in Dart.private func flutterError(from error: Error) -> FlutterError { let consentError = error as? ConsentError return FlutterError( code: consentError?.code.rawValue ?? "UNKNOWN_ERROR", message: consentError?.message ?? error.localizedDescription, details: nil )} @MainActorprivate func topViewController() -> UIViewController? { let scene = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive } var top = scene?.windows.first { $0.isKeyWindow }?.rootViewController while let presented = top?.presentedViewController { top = presented } return top} private extension ConsentPreferences { var asDictionary: [String: Bool] { [ "necessary": necessary, "analytics": analytics, "personalization": personalization, "advertising": advertising, ] }} private extension ConsentResult { var asDictionary: [String: Any] { [ "status": status.rawValue, // "GRANTED" "preferences": preferences.asDictionary, "timestamp": Int(timestamp.timeIntervalSince1970 * 1000), // Date -> epoch millis ] }} private extension ConsentEvent { var asDictionary: [String: Any] { var map: [String: Any] = ["type": type.rawValue] if let result { map["result"] = result.asDictionary } if let error { map["error"] = ["code": error.code.rawValue, "message": error.message] } return map }}Example app
anumati_flutter_demo: the home screen shows the SDK status, consent status, and preferences, with Initialize SDK, Manage Consent (opens the native consent screen), Refresh Status, and Revoke Consent. It selects UAT in debug builds and production in release builds.
// example/lib/main.dart — anumati_flutter_demoimport 'dart:async'; import 'package:digital_anumati_consent/digital_anumati_consent.dart';import 'package:flutter/foundation.dart';import 'package:flutter/material.dart'; void main() => runApp(const AnumatiFlutterDemo()); class AnumatiFlutterDemo extends StatelessWidget { const AnumatiFlutterDemo({super.key}); @override Widget build(BuildContext context) => MaterialApp( title: 'Anumati Flutter Demo', theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true), home: const HomeScreen(), );} class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState();} class _HomeScreenState extends State<HomeScreen> { bool _initialized = false; ConsentStatus _status = ConsentStatus.notDetermined; ConsentPreferences? _preferences; String? _message; StreamSubscription<ConsentEvent>? _events; @override void initState() { super.initState(); _events = DigitalAnumatiConsent.events.listen((event) { final result = event.result; if (result != null && mounted) { setState(() { _status = result.status; _preferences = result.preferences; }); } if (event.type == 'CONSENT_REVOKED') _refresh(); }); _initialize(); } @override void dispose() { _events?.cancel(); super.dispose(); } Future<void> _initialize() => _run(() async { await DigitalAnumatiConsent.initialize( siteKey: 'demo-site-key-123', applicationId: 'demo-mobile-app-123', environment: kReleaseMode ? Environment.production : Environment.uat, ); await _refresh(); // First launch (or after a revoke): ask before continuing. if (_status == ConsentStatus.notDetermined) { await DigitalAnumatiConsent.showConsent(); } }); Future<void> _refresh() async { final initialized = await DigitalAnumatiConsent.isInitialized(); if (!initialized) { if (mounted) setState(() => _initialized = false); return; } final status = await DigitalAnumatiConsent.getConsentStatus(); final preferences = await DigitalAnumatiConsent.getConsentPreferences(); if (!mounted) return; setState(() { _initialized = true; _status = status; _preferences = preferences; }); } Future<void> _run(Future<void> Function() action) async { try { await action(); if (mounted) setState(() => _message = null); } on ConsentError catch (error) { // USER_CANCELLED only means the screen was closed; keep the previous state. if (error.code != 'USER_CANCELLED' && mounted) { setState(() => _message = error.message); } } } @override Widget build(BuildContext context) { final sdkLabel = _initialized ? 'Initialized' : 'Not initialized'; final consentLabel = _status.wire; final preferences = _preferences; return Scaffold( appBar: AppBar(title: const Text('Anumati Flutter Demo')), body: ListView( padding: const EdgeInsets.all(24), children: [ Text('SDK status: $sdkLabel', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Text('Consent: $consentLabel', style: Theme.of(context).textTheme.titleMedium), if (preferences != null) ...[ const SizedBox(height: 16), Text('Preferences', style: Theme.of(context).textTheme.titleSmall), _preferenceRow('Necessary', preferences.necessary), _preferenceRow('Analytics', preferences.analytics), _preferenceRow('Personalization', preferences.personalization), _preferenceRow('Advertising', preferences.advertising), ], if (_message != null) ...[ const SizedBox(height: 16), Text(_message!, style: TextStyle(color: Theme.of(context).colorScheme.error)), ], const SizedBox(height: 24), FilledButton(onPressed: _initialize, child: const Text('Initialize SDK')), const SizedBox(height: 8), FilledButton.tonal( onPressed: _initialized ? () => _run(DigitalAnumatiConsent.showConsent) : null, child: const Text('Manage Consent'), ), const SizedBox(height: 8), OutlinedButton( onPressed: _initialized ? () => _run(_refresh) : null, child: const Text('Refresh Status'), ), const SizedBox(height: 8), OutlinedButton( onPressed: _initialized && _status != ConsentStatus.notDetermined ? () => _run(DigitalAnumatiConsent.revokeConsent) : null, child: const Text('Revoke Consent'), ), ], ), ); } Widget _preferenceRow(String label, bool allowed) => ListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text(label), trailing: Icon( allowed ? Icons.check_circle : Icons.cancel_outlined, color: allowed ? Colors.green : Colors.grey, ), );}Error propagation
Android: native SDK throws ConsentError(code = NETWORK_ERROR) → Kotlin plugin: result.error("NETWORK_ERROR", message, null) → MethodChannel → Dart: PlatformException(code: "NETWORK_ERROR") → DigitalAnumatiConsent throws ConsentError("NETWORK_ERROR", message) iOS: native SDK throws ConsentError(code: .networkError) → Swift plugin: FlutterError(code: "NETWORK_ERROR", message: ...) → MethodChannel → Dart: PlatformException → ConsentError("NETWORK_ERROR", message)| Layer | Android | iOS | Dart |
|---|---|---|---|
| Native SDK | ConsentError(code: ConsentErrorCode, message) | ConsentError(code: ConsentError.Code, message) | — |
| Bridge | result.error(code.name, message, null) | FlutterError(code: code.rawValue, message:) | — |
| Channel | PlatformException(code, message) | PlatformException(code, message) | caught in _invoke() |
| App code | — | — | ConsentError(code, message) — same code string on both platforms |
Every code, and what it means, is listed in the shared error contract. Unexpected native exceptions become UNKNOWN_ERROR. The initialization flow works the same way:
Flutter app starts ↓Dart: DigitalAnumatiConsent.initialize(siteKey, applicationId, environment) ↓MethodChannel "com.digitalanumati/consent" → method "initialize" + arguments map ↓Android: DigitalAnumatiConsentPlugin.onMethodCall | iOS: DigitalAnumatiConsentPlugin.handle ↓Native SDK: DigitalAnumati.initialize(...) → emits SDK_INITIALIZED on the EventChannel ↓result.success(null) → Future completes in Dart or result.error(code, message) → PlatformException → ConsentError thrown in DartTesting
Unit-test the Dart wrapper by mocking the MethodChannel — this checks the channel contract and error conversion without a device:
// test/digital_anumati_consent_test.dart — unit tests for the Dart wrapper (no device needed)import 'package:digital_anumati_consent/digital_anumati_consent.dart';import 'package:flutter/services.dart';import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); const channel = MethodChannel('com.digitalanumati/consent'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; tearDown(() => messenger.setMockMethodCallHandler(channel, null)); test('getConsentStatus maps the wire value to the enum', () async { messenger.setMockMethodCallHandler(channel, (call) async => 'PARTIAL'); expect(await DigitalAnumatiConsent.getConsentStatus(), ConsentStatus.partial); }); test('initialize sends the channel-contract arguments', () async { MethodCall? received; messenger.setMockMethodCallHandler(channel, (call) async { received = call; return null; }); await DigitalAnumatiConsent.initialize( siteKey: 'demo-site-key-123', applicationId: 'demo-mobile-app-123', ); expect(received?.method, 'initialize'); expect(received?.arguments, { 'siteKey': 'demo-site-key-123', 'applicationId': 'demo-mobile-app-123', 'environment': 'uat', }); }); test('updateConsent sends preferences and parses the result', () async { MethodCall? received; messenger.setMockMethodCallHandler(channel, (call) async { received = call; return { 'status': 'PARTIAL', 'preferences': { 'necessary': true, 'analytics': true, 'personalization': false, 'advertising': false, }, 'timestamp': 1759054500000, }; }); final result = await DigitalAnumatiConsent.updateConsent( const ConsentPreferences(analytics: true), ); expect(received?.arguments, {'analytics': true, 'personalization': false, 'advertising': false}); expect(result.status, ConsentStatus.partial); expect(result.preferences.analytics, isTrue); }); test('native errors surface as ConsentError with the same code', () async { messenger.setMockMethodCallHandler(channel, (call) async { throw PlatformException(code: 'NETWORK_ERROR', message: 'offline'); }); expect( DigitalAnumatiConsent.revokeConsent(), throwsA(isA<ConsentError>().having((e) => e.code, 'code', 'NETWORK_ERROR')), ); });}| Scenario | Type | How to verify |
|---|---|---|
| Initialization | Integration | initialize() completes; isInitialized() is true; SDK_INITIALIZED arrives |
| Invalid configuration | Unit + integration | Empty site key rejects with INVALID_SITE_KEY / INVALID_CONFIGURATION |
| Accept / reject / partial | Integration | Accept All → GRANTED, Reject All → DENIED, some toggles → PARTIAL, with matching events |
| Preference updates | Unit + integration | updateConsent() sends the contract map and returns the new result; CONSENT_UPDATED arrives |
| Revoke | Integration | Status becomes NOT_DETERMINED; CONSENT_REVOKED arrives |
| Errors | Unit | Mocked PlatformException codes surface as ConsentError with the same code |
| App restart | Integration | Kill and relaunch: status is restored without showing the screen |
| Bridges | Native unit | Kotlin/Swift: conversion helpers produce the documented maps and error codes |
Run integration tests with the integration_test package on an Android emulator and an iOS simulator, against the UAT environment.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| MissingPluginException | The app wasn't rebuilt after adding the plugin | Stop the app and run a full rebuild (not hot reload); run pod install on iOS. |
| UNKNOWN_ERROR: No foreground activity / view controller | showConsent() was called before the first frame | Call it after the first frame, e.g. from initState via a post-frame callback or a button. |
| INITIALIZATION_FAILED | A call ran before initialize() completed | Await initialize() before other calls; check isInitialized(). |
| No events received | Nobody listened to the stream yet | Subscribe to DigitalAnumatiConsent.events before triggering actions. |
| Package resolution fails | The demo package isn't published | Expected for this demo. |
Security guidance (keys, credentials, logging, storage) is shared across platforms — see Security.