Flutter
1.0.0-beta.2Add DPDPA consent management to your Flutter app. The plugin wraps the native Kotlin Android SDK through MethodChannel and EventChannel, so you get the same offline queue, app-identity verification, IAB TCF writing, and fail-closed gating from Dart without re-implementing any of it.
π€ Native Android
Writing Kotlin or Java? Use the Android SDK page.
π¦ Flutter
This page.
π± React Native / Expo
TypeScript, one file to copy.
Requirements
- Flutter 3.10 or later, Dart 3.0 or later
- Android
minSdk 24(Android 7.0+) - iOS is not supported yet (on the roadmap)
- A DPDPA Shield account with a published consent notice
Installation
The plugin is on pub.dev. Add it to your pubspec.yaml:
dependencies:
shield_consent: ^1.0.0-beta.2Or install from GitHub directly (latest unreleased code):
dependencies:
shield_consent:
git:
url: https://github.com/DPDPA-Shield/flutter-plugin
ref: mainThen add JitPack to your app's android/settings.gradle repositories so the underlying Kotlin SDK resolves:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}Quick start
import 'package:shield_consent/shield_consent.dart';
// 1. Initialise once, before runApp() or in main()
await ShieldConsent.init(apiKey: 'dpdpa_live_YOUR_KEY', cmpId: 1);
// 2. Load the published notice
final notice = await ShieldConsent.loadNotice();
// 3. Localise for the device language
final localized = ShieldConsent.localize(notice, languageCode: 'HI');
// 4. Show your consent UI using localized.title, localized.purposes, etc.
// ...
// 5. Record the user's decision
await ShieldConsent.recordDecision(
ConsentDecision(
noticeId: notice.id,
given: {'analytics-purpose-id': true, 'marketing-purpose-id': false},
languageShown: 'HI',
decidedAtEpochMs: DateTime.now().millisecondsSinceEpoch,
),
identifier: 'user@example.com', // hashed on-device before leaving the app
languageShown: 'HI',
externalId: 'user.id', // optional - YOUR OWN user ID, sent as-is (never hashed)
);Full API
ShieldConsentinit({required String apiKey, int cmpId = 1})Future<void>Initialise the SDK. Call once before any other method.loadNotice({String? noticeId})Future<ConsentNotice>Fetch the published notice. Cached after the first call.localize(ConsentNotice, {String languageCode})LocalizedNotice?Per-field fallback to English. Pure Dart, no platform round-trip.recordDecision(ConsentDecision, {required String identifier, String languageShown, String? externalId})Future<void>Save and queue a consent decision. identifier is hashed on-device. externalId (optional) is your own internal user ID, sent as-is - never hashed - to correlate this record back to your own database.restoredDecision()Future<ConsentDecision?>Last decision from encrypted storage. Null if the user has not decided yet.flushQueue()Future<void>Retry any queued writes. Call on app foreground or connectivity changes.ShieldGateisConsented(String purposeId)Future<bool>One-shot check. Required purposes always return true.runIfConsented(String purposeId, Future<void> Function() block)Future<bool>Runs block only if consented. Returns true if the block ran.observeConsented(String purposeId)Stream<bool>Emits the current state immediately, then again on every change.Localisation (22 languages)
ShieldConsent.localize() is pure Dart. No platform call. Falls back per field, not per language. If a Hindi translation exists for the notice title but not for a purpose description, the title shows in Hindi and the description falls back to English. No blank fields, ever.
final notice = await ShieldConsent.loadNotice();
// Use the device locale
final langCode = Platform.localeName.split('_').first.toUpperCase(); // e.g. "HI"
final localized = ShieldConsent.localize(notice, languageCode: langCode);
// localized.title - translated, or English fallback
// localized.purposes - each purpose with translated name/description
// localized.languageCode - normalised uppercase code usedSupported: English, Hindi, Tamil, Telugu, Kannada, Malayalam, Marathi, Gujarati, Punjabi, Odia, Bengali, Assamese, Urdu, and more (all 22 scheduled Indian languages).
ShieldGate
ShieldGate is fail-closed: if the user has not consented to a purpose, it reads as not consented. A tracker gated on a purpose the user was never asked about does not run. This is the opposite of ad SDKs that assume consent until told otherwise.
// One-shot check
if (await ShieldGate.isConsented('analytics')) {
initAnalytics();
}
// Run a block only if consented (returns true if the block ran)
final ran = await ShieldGate.runIfConsented('marketing', () async {
await FacebookAds.initialize();
});
// Reactive stream: fires immediately with the current state, then on every change
ShieldGate.observeConsented('analytics').listen((given) {
FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(given);
});Offline queue
ShieldGate updates synchronously when a decision is recorded. Trackers stop or start immediately, regardless of connectivity. The backend write is queued and retried with exponential backoff when the app comes back online.
// Call flushQueue() when connectivity returns or the app resumes
import 'package:connectivity_plus/connectivity_plus.dart';
Connectivity().onConnectivityChanged.listen((result) {
if (result != ConnectivityResult.none) {
ShieldConsent.flushQueue();
}
});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ShieldConsent.flushQueue();
}
}IAB TCF v2 (optional)
After every recordDecision() call, the SDK writes IABTCF_TCString and IABTCF_gdprApplies=0 to SharedPreferences. Any third-party SDK that reads IAB TCF in-app storage (Firebase, AppLovin, etc.) picks up consent state with no extra configuration from you.
IABTCF_gdprApplies is always 0. DPDPA 2023 is not GDPR. Setting it to 1 would trigger EU-specific flows that Indian apps do not need. The purpose-consent bitfield is still written accurately, so SDKs using it as a general opt-out signal work correctly.
Testing
Run the Dart tests
cd mobile/flutter-plugin
flutter test12 tests covering models, the localiser, and fromMap round-trips. No emulator needed.
Point at production
The production API at api.dpdpashield.in supports MOBILE_APP consents. Use a dpdpa_live_ API key with a published notice. An API key with no registered app identities lets any app through, so you can test freely before locking it down.
Troubleshooting
MissingPluginException: No implementation found for method initβΌ
ShieldConsentPlugin is not registered. Standard Flutter auto-linking handles this. If it does not, add ShieldConsentPlugin() manually to your MainActivity.
loadNotice throws or returns an errorβΌ
Check that your API key is valid and the notice is published in the dashboard. Verify with: curl "https://api.dpdpashield.in/api/v1/consent/public-notice?apiKey=YOUR_KEY"
403 APP_IDENTITY_NOT_ALLOWEDβΌ
Your package name + signing certificate fingerprint is registered but does not match. Check the fingerprint in Settings > API Keys > App Identities against what your debug or release keystore produces.
ShieldGate.observeConsented emits nothingβΌ
The EventChannel only emits after the first state change. Call recordDecision() to produce one, or check that ShieldConsent.init() ran first.
On this page