Implementing Consent Management by Data Categories in Mobile Apps
Consent Management is not just one "I agree" button. It's a system tracking exactly what user consented to, when, under what document version, and what to do if revoked.
Without this system impossible to comply with GDPR/CCPA or prove compliance at audit.
Why Single "Accept" Insufficient
Regulators require granular consent: user must be able to consent to analytics but refuse advertising profiling. Forcing accept-all or nothing — violates free choice principle per GDPR. ICO (UK) and CNIL (France) already fined for this.
Technically means: separate boolean per category, not single consent_accepted field.
Consent Manager Architecture
enum class ConsentPurpose(val id: String) {
NECESSARY("necessary"),
ANALYTICS("analytics"),
MARKETING("marketing"),
PERSONALIZATION("personalization"),
THIRD_PARTY_SHARING("third_party_sharing"),
LOCATION_TRACKING("location_tracking"),
PUSH_MARKETING("push_marketing")
}
data class ConsentRecord(
val purpose: ConsentPurpose,
val granted: Boolean,
val grantedAt: Long?, // null if not granted
val revokedAt: Long?, // null if not revoked
val policyVersion: String, // policy version consent given under
val collectionMethod: String // "explicit_ui", "imported", "default_opt_in"
)
class ConsentManager(
private val store: ConsentStore,
private val server: ConsentSyncService
) {
fun grant(purpose: ConsentPurpose) {
val record = ConsentRecord(
purpose = purpose,
granted = true,
grantedAt = System.currentTimeMillis(),
revokedAt = null,
policyVersion = PolicyVersionProvider.current(),
collectionMethod = "explicit_ui"
)
store.save(record)
server.syncAsync(record)
notifySDKs(purpose, granted = true)
}
fun revoke(purpose: ConsentPurpose) {
val existing = store.get(purpose)?.copy(
granted = false,
revokedAt = System.currentTimeMillis()
) ?: return
store.save(existing)
server.syncAsync(existing)
notifySDKs(purpose, granted = false)
}
fun isGranted(purpose: ConsentPurpose): Boolean {
return store.get(purpose)?.granted == true
}
}
SDK Synchronization
On consent change — immediately update affected SDKs:
private fun notifySDKs(purpose: ConsentPurpose, granted: Boolean) {
when (purpose) {
ANALYTICS -> {
FirebaseAnalytics.getInstance(context)
.setAnalyticsCollectionEnabled(granted)
amplitude.setOptOut(!granted)
}
MARKETING -> {
MobileAds.setRequestConfiguration(
RequestConfiguration.Builder()
.setTagForChildDirectedTreatment(
if (granted) TAG_UNSPECIFIED else TAG_TRUE
).build()
)
}
PUSH_MARKETING -> {
if (!granted) {
// Unsubscribe from marketing FCM topics
FirebaseMessaging.getInstance()
.unsubscribeFromTopic("marketing_campaigns")
}
}
else -> {}
}
}
Consent Management Screen
Available from profile settings anytime — not just on first launch. Structure:
Data Management
├── Necessary (non-togglable)
│ └── Authentication and Security
├── Usage Analytics [ON] ←→
│ └── Helps us improve app
├── Personalization [OFF] ←→
│ └── Recommendations based on your actions
├── Marketing Communications [OFF] ←→
│ └── Email and push about news
└── Partner Sharing [OFF] ←→
└── Ad networks and analytics
Consent revocation works immediately. Can't add friction ("Sure?" → "Really sure?") — this is dark pattern.
Re-obtaining Consent
When new Privacy Policy version releases — assess whether changes affect prior consent. If new processing category added — need fresh consent for it. If just wording changes without substance change — notification sufficient.
Mechanism: on app startup check policyVersion in stored consent records. If outdated — show updated screen only for changed categories.
Storage and Audit
Consent records can't be deleted on account deletion — needed to prove compliance at check. Stored separately from user data, retention per requirements (usually 5–7 years for legally significant documents).
Timeline: full Consent Management system with UI, SDK orchestration and server sync: 2–3 days. Cost calculated individually.







