Developer SDKs

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

The package names, APIs, and endpoints in this section are placeholders that show how the integration is intended to work. They are not published, so the code won't resolve until the real SDK is released — the actual coordinates and endpoints will come from Digital Anumati.

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

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.

ChannelNameUsed for
MethodChannelcom.digitalanumati/consentinitialize, showConsent, getConsentStatus, getConsentPreferences, updateConsent, revokeConsent, isInitialized
EventChannelcom.digitalanumati/consent/eventsSDK_INITIALIZED, CONSENT_SHOWN, CONSENT_ACCEPTED, CONSENT_REJECTED, CONSENT_UPDATED, CONSENT_REVOKED, CONSENT_ERROR

Installation

Add the package to your app:

pubspec.yaml
# pubspec.yaml of your app
dependencies:
flutter:
sdk: flutter
# DEMO / FICTIONAL package
digital_anumati_consent: ^1.0.0

Then 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)
// android/build.gradle (plugin) — pulls in the native SDK
dependencies {
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
# ios/digital_anumati_consent.podspec — pulls in the native SDK
Pod::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 / FICTIONAL
end

Usage

Dart
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.

MethodArgumentsReturn valueAndroid implementationiOS implementationDart API
initialize{ "siteKey": String, "applicationId": String, "environment": "uat" | "production" }nullDigitalAnumati.initialize(context, siteKey, applicationId, env)try DigitalAnumati.initialize(siteKey:applicationId:environment:)Future<void> initialize({siteKey, applicationId, environment})
showConsentnone{ "status": String, "preferences": Map, "timestamp": int }DigitalAnumati.showConsent(activity, listener)try await DigitalAnumati.showConsent(from:)Future<ConsentResult> showConsent()
getConsentStatusnone"NOT_DETERMINED" | "GRANTED" | "DENIED" | "PARTIAL"DigitalAnumati.getConsentStatus().nameDigitalAnumati.getConsentStatus().rawValueFuture<ConsentStatus> getConsentStatus()
getConsentPreferencesnone{ "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)
revokeConsentnonenullDigitalAnumati.revokeConsent() (coroutine)try await DigitalAnumati.revokeConsent()Future<void> revokeConsent()
isInitializednoneboolDigitalAnumati.isInitialized()DigitalAnumati.isInitializedFuture<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" } }.

EventWhenPayload
SDK_INITIALIZEDinitialize() succeeded—
CONSENT_SHOWNThe native consent screen opened—
CONSENT_ACCEPTEDThe user saved and every optional category is on (GRANTED)result
CONSENT_REJECTEDThe user saved and every optional category is off (DENIED)result
CONSENT_UPDATEDA PARTIAL choice was saved, or updateConsent() changed preferencesresult
CONSENT_REVOKEDrevokeConsent() completed; status is NOT_DETERMINED—
CONSENT_ERRORA background operation failed (for example a sync)error
Dart
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.

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
ConsistencyIdentical on native, Flutter and React Native appsEach framework re-implements the screen
Consent rulesStay in one place (native SDK)Risk of diverging rules in Dart
UpdatesScreen changes ship with the native SDKEvery framework package must be updated
Look and feelThemed through SDK configuration, not Flutter widgetsFull control with Flutter widgets
Compliance reviewOne screen to reviewOne screen per framework

Plugin code

Package layout:

digital_anumati_consent/
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.md
pubspec.yaml (plugin)
name: digital_anumati_consent
description: DEMO / FICTIONAL Flutter plugin that wraps the Digital Anumati native consent SDKs.
version: 1.0.0
publish_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: DigitalAnumatiConsentPlugin

Dart API & models

lib/digital_anumati_consent.dart
// lib/digital_anumati_consent.dart
export 'src/digital_anumati_consent.dart';
export 'src/models.dart';
lib/src/channels.dart
// lib/src/channels.dart
import '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
// 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.dart
// lib/src/digital_anumati_consent.dart
import '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).

DigitalAnumatiConsentPlugin.kt
// android/src/main/kotlin/com/digitalanumati/consent/flutter/DigitalAnumatiConsentPlugin.kt
package com.digitalanumati.consent.flutter
import android.app.Activity
import android.content.Context
import com.digitalanumati.consent.ConsentError
import com.digitalanumati.consent.ConsentErrorCode
import com.digitalanumati.consent.ConsentEvent
import com.digitalanumati.consent.ConsentListener
import com.digitalanumati.consent.ConsentPreferences
import com.digitalanumati.consent.ConsentResult
import com.digitalanumati.consent.DigitalAnumati
import com.digitalanumati.consent.Environment
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import 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:).

DigitalAnumatiConsentPlugin.swift
// ios/Classes/DigitalAnumatiConsentPlugin.swift
import Flutter
import UIKit
import 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
)
}
@MainActor
private 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
// example/lib/main.dart — anumati_flutter_demo
import '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

Error flow
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)
LayerAndroidiOSDart
Native SDKConsentError(code: ConsentErrorCode, message)ConsentError(code: ConsentError.Code, message)—
Bridgeresult.error(code.name, message, null)FlutterError(code: code.rawValue, message:)—
ChannelPlatformException(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:

Initialization flow
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 Dart

Testing

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
// 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')),
);
});
}
ScenarioTypeHow to verify
InitializationIntegrationinitialize() completes; isInitialized() is true; SDK_INITIALIZED arrives
Invalid configurationUnit + integrationEmpty site key rejects with INVALID_SITE_KEY / INVALID_CONFIGURATION
Accept / reject / partialIntegrationAccept All → GRANTED, Reject All → DENIED, some toggles → PARTIAL, with matching events
Preference updatesUnit + integrationupdateConsent() sends the contract map and returns the new result; CONSENT_UPDATED arrives
RevokeIntegrationStatus becomes NOT_DETERMINED; CONSENT_REVOKED arrives
ErrorsUnitMocked PlatformException codes surface as ConsentError with the same code
App restartIntegrationKill and relaunch: status is restored without showing the screen
BridgesNative unitKotlin/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

SymptomLikely causeFix
MissingPluginExceptionThe app wasn't rebuilt after adding the pluginStop the app and run a full rebuild (not hot reload); run pod install on iOS.
UNKNOWN_ERROR: No foreground activity / view controllershowConsent() was called before the first frameCall it after the first frame, e.g. from initState via a post-frame callback or a button.
INITIALIZATION_FAILEDA call ran before initialize() completedAwait initialize() before other calls; check isInitialized().
No events receivedNobody listened to the stream yetSubscribe to DigitalAnumatiConsent.events before triggering actions.
Package resolution failsThe demo package isn't publishedExpected for this demo.

Security guidance (keys, credentials, logging, storage) is shared across platforms — see Security.