GDPR Compliance in Flutter Mobile Applications

Everything you need to know about GDPR compliance in Flutter mobile applications.

GDPR Compliance in Flutter Mobile Applications

GDPR Scope

If an app serves even a single user in the EU, it falls under the scope of GDPR. This has been explicitly stated by the European Court of Justice and within the GDPR text itself.

So, if you publish your Flutter app on the App Store / Google Play and it is downloaded from the EU, GDPR obligations automatically apply.

The Cost of Non-Compliance

When it comes to GDPR violations, penalties are truly deterrent:

  • Fines up to 20 million Euros
  • 4% of the company’s annual global revenue (whichever is higher will be applied)

These aren’t just theoretical numbers. In recent years, many companies worldwide have paid millions of euros in fines. Even for a small startup, these amounts can mean the end of the company.

Purpose of This Guide

This document aims to provide a step-by-step practical guide for Flutter developers to ensure GDPR compliance. Instead of theoretical information, you’ll find concrete solutions and best practices that you can integrate into your application today.

GDPR is built upon 7 core principles of data protection. Understanding these principles will help you make the right decisions when designing your application.

1. Lawfulness, Fairness, and Transparency

Personal data must be processed lawfully, fairly, and in a transparent manner.

🎯 Tell users CLEARLY what data you collect and why. Hidden data collection is a GDPR violation

// ✅ Good Example: Transparent information
void showPrivacyNotice() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('Privacy and Your Data'),
      content: Text(
        'Our app collects the following data to provide you with better service:\n\n'
        '• Usage analytics (which features you use)\n'
        '• Location information (for nearby stores - with your permission)\n'
        '• Device information (for app compatibility)\n\n'
        'You can read in detail how we use this data in our Privacy Policy.'
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Read Privacy Policy'),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('I Understand'),
        ),
      ],
    ),
  );
}

// ❌ Bad Example: Vague information
void wrongApproach() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      content: Text('By using this app, you agree to our data collection'),
      // What data? For what purpose? VAGUE - GDPR VIOLATION!
    ),
  );
}

2. Purpose Limitation

Data must be collected for specified, explicit, and legitimate purposes and not processed in a manner incompatible with those purposes.

🎯 Data collected for analytics CANNOT be used for marketing (without new consent)!

// ❌ WRONG: Using data for different purposes
class WrongPurposeUsage {
  Future<void> collectAnalytics() async {
    // User gave permission for "analytics"
    final userBehavior = await getUserBehaviorData();
    
    // But it's being used for marketing - GDPR VIOLATION!
    await sendToMarketingTeam(userBehavior); // ❌
    await targetAdsBasedOnBehavior(userBehavior); // ❌
  }
}

// ✅ CORRECT: Purpose-based permission
class CorrectPurposeUsage {
  Future<void> collectData() async {
    // SEPARATE consent for each purpose
    final analyticsConsent = await getAnalyticsConsent();
    final marketingConsent = await getMarketingConsent();
    
    if (analyticsConsent) {
      // Only for analytics purposes
      await sendToAnalytics(userData);
    }
    
    if (marketingConsent) {
      // Marketing only with separate consent
      await sendToMarketing(userData);
    }
  }
}

3. Data Minimisation

Collect only the data you need — nothing more.

🎯 “Will the app not work without this data?” If the answer is NO, DON’T COLLECT IT!

// ❌ Bad Example: Excessive data collection
class UserProfile {
  String name;
  String email;
  String phone;                // Really necessary?
  String address;              // Really necessary?
  String birthDate;            // Really necessary?
  String gender;               // Really necessary?
  String socialSecurityNumber; // NEVER COLLECT!
  String motherMaidenName;     // NEVER COLLECT!
  double latitude;             // Always necessary?
  double longitude;            // Always necessary?
}

// ✅ Good Example - Only REQUIRED data
class UserProfile {
  String userId;               // REQUIRED for account management
  String email;                // REQUIRED for communication
  // Nothing else!
}

// Extra data only if user wants
class OptionalUserData {
  String? displayName;         // User adds themselves (optional)
  String? avatarUrl;           // User adds themselves (optional)
}

4. Accuracy

Personal data must be correct and kept up to date.

🎯 Users must always have the ability to review and correct their personal data without delay!

// Allow users to update their data
class ProfileUpdatePage extends StatelessWidget {
  void updateUserData(String newEmail) async {
    await userRepository.updateEmail(newEmail);
    // Update in database
    await database.update('users', {'email': newEmail});
  }
}

5. Storage Limitation

Keep data only for as long as necessary.

🎯 If the data is no longer needed for its original purpose, it must be deleted or anonymized immediately!

// ❌ Bad Example: Data kept forever
class DataRetentionService {
  Future<void> doNothing() async {
    // User data and analytics never deleted
    // ❌ Endless storage = GDPR violation!
  }
}

// ✅ Good Example: Data automatically removed
class DataRetentionService {
  // Auto-delete analytics older than 30 days
  Future<void> cleanupOldData() async {
    final thirtyDaysAgo = DateTime.now().subtract(Duration(days: 30));
    await database.delete(
      'analytics',
      where: 'created_at < ?',
      whereArgs: [thirtyDaysAgo.toIso8601String()],
    );
  }

  // Delete all user data if account removed
  Future<void> deleteAllUserData(String userId) async {
    await database.delete('users', where: 'id = ?', whereArgs: [userId]);
    await database.delete('user_sessions', where: 'user_id = ?', whereArgs: [userId]);
    await database.delete('user_preferences', where: 'user_id = ?', whereArgs: [userId]);
  }
}

6. Integrity and Confidentiality

Personal data must be processed securely and protected from unauthorized access, loss, or damage.

🎯 Always encrypt and protect personal data. IP address, location (lat/long), PII must NOT be sent to third parties

// ✅ Encrypt sensitive data
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureDataService {
  final storage = FlutterSecureStorage();
  
  Future<void> saveSecureData(String key, String value) async {
    await storage.write(
      key: key,
      value: value,
      aOptions: AndroidOptions(encryptedSharedPreferences: true),
    );
  }
  
  Future<String?> getSecureData(String key) async {
    return await storage.read(key: key);
  }
}


// ❌ BAD: Sending PII to third parties
class WrongApproach {
  void trackUser() async {
    await analytics.logEvent('user_data', {
      'ip_address': ip,        // 🔴 GDPR VIOLATION!
      'latitude': lat,         // 🔴 GDPR VIOLATION!
      'email': user.email,     // 🔴 GDPR VIOLATION!
    });
  }
}

// ✅ GOOD: Use anonymous IDs, no PII to third parties
class CorrectApproach {
  void trackUser() async {
    final anonymousId = Uuid().v4();
    await analytics.setUserId(anonymousId); // ✅ Anonymous
    
    // City/country instead of exact location
    final city = await getCityFromCoordinates(lat, lon);
    await analytics.logEvent('user_region', {
      'city': city, // ✅ General info, no lat/long
    });
  }
}

7. Accountability

You must be able to demonstrate GDPR compliance at any time. Documentation, logs, and audit trails are essential.

🎯 Get OPT-IN consent BEFORE entering the app! Keep audit logs — if you cannot prove compliance, regulators assume non-compliance!

// ✅ Opt-in approach
class CorrectConsentApproach {
  void initApp() async {
    final hasConsent = await checkGDPRConsent();
    
    if (!hasConsent) {
      await showMandatoryConsentScreen();
      if (!await checkGDPRConsent()) {
        initBasicFeaturesOnly();
        return;
      }
    }
    
    await FirebaseAnalytics.instance.logAppOpen(); // ✅ After consent
  }
}

// ❌ Bad Example: No logging or audit trail
class UserService {
  Future<void> updateEmail(String newEmail) async {
    await database.update('users', {'email': newEmail});
    // ❌ No record of who changed it, when, or why
  }
}

// ✅ Good Example: Keep audit logs for accountability
class UserService {
  Future<void> updateEmail(String userId, String newEmail) async {
    await database.update('users', {'email': newEmail});
    await database.insert('audit_logs', {
      'user_id': userId,
      'action': 'update_email',
      'new_value': newEmail,
      'timestamp': DateTime.now().toIso8601String(),
    });
    // ✅ Now you can prove changes if regulators ask
  }
}

GDPR Critical Requirements

The following are the most common GDPR violations in Flutter apps. Each one can result in significant fines. Make sure your app complies with ALL of these before launching.

1. Third-Party Package Management

Every package you add is a potential GDPR violation risk. Each package may collect data without your knowledge!

🎯 Minimize third-party packages as much as possible!

// ❌ BAD: Too many packages
dependencies:
  awesome_analytics: ^1.0.0      # What does it collect? Unknown!
  tracking_package: ^2.0.0       # Another tracking SDK
  data_collector: ^1.5.0         # Unclear privacy policy
  cool_feature: ^3.0.0           # Entire package for one feature

// ✅ GOOD: Minimal dependencies
dependencies:
  firebase_analytics: ^10.0.0    # Single analytics solution
  shared_preferences: ^2.0.0     # Only local storage
  # Before adding ANY package, ask:
  # 1. Is it REALLY necessary?
  # 2. What data does it collect?
  # 3. Is it GDPR compliant?
  # 4. Can I do it with native code instead?

Users must see a clear consent choice when the app first opens, and tracking must remain disabled until consent is given.

🎯 Ask for consent before any tracking begins*. The app must not send analytics, ads, or location data* until the user explicitly accepts*.

// ❌ BAD: Tracking starts immediately without consent
class AppStartup {
  void initApp() {
    AnalyticsService().enable(); // ❌ Tracking before user consents
  }
}

// ✅ GOOD: Show consent banner before enabling tracking
class AppStartup {
  Future<void> initApp(BuildContext context) async {
    final consent = await ConsentManager().getAnalyticsConsent();
    if (!consent) {
      showConsentDialog(context); // ask user first
    } else {
      AnalyticsService().enable();
    }
  }
}

// ✅ GOOD: Allow users to change consent later in Settings
class SettingsPage extends StatelessWidget {
  final ConsentManager consentManager;

  SettingsPage(this.consentManager);

  @override
  Widget build(BuildContext context) {
    return SwitchListTile(
      title: Text('Allow Analytics'),
      value: consentManager.analyticsConsent,
      onChanged: (value) {
        consentManager.setAnalyticsConsent(value);
      },
    );
  }
}

3. IP Address & Location Data

IP address and precise location (latitude/longitude) are considered personal data under GDPR.

🎯 Only collect or send them if strictly necessary, and always anonymize or ask for explicit consent!

// ❌ BAD: Always sending full IP and exact GPS location
class TrackingService {
  void sendData(String ip, double lat, double lon) {
    api.post('/track', {
      'ip': ip,            // ❌ full IP
      'latitude': lat,     // ❌ exact location
      'longitude': lon,
    });
  }
}

// ✅ GOOD: Anonymize IP and use coarse location
class TrackingService {
  void sendData(String ip, double lat, double lon) {
    final anonymizedIp = ip.replaceAll(RegExp(r'\.\d+$'), '.0'); // mask last octet
    final roundedLat = double.parse(lat.toStringAsFixed(2));     // ~1km accuracy
    final roundedLon = double.parse(lon.toStringAsFixed(2));
    
    api.post('/track', {
      'ip': anonymizedIp,  // ✅ anonymized
      'latitude': roundedLat,
      'longitude': roundedLon,
    });
  }
}

// ✅ BEST: Ask for explicit consent before collecting
if (userConsentManager.locationConsent) {
  TrackingService().sendData(ip, lat, lon);
}

4. Data Retention for Analytics & Logs

Keep raw, user-level data for the shortest possible time; keep only aggregates longer.

🎯 Data Retention Rules:

- Keep raw identifiable data for no longer than 14–30 days*.

- Keep aggregated or anonymized data for 12–24 months*.

- Keep consent records and audit logs for longer periods, as evidence of compliance.

Examples (Sentry + Amplitude)

  • Sentry = Crash reporting (functional, needed for app quality)
  • Amplitude = Analytics (optional, needs consent)
Future<void> initApp(BuildContext context) async {
  // 1. Ask for consent FIRST
  final consent = await askConsentDialog(context);

  // 2. Sentry: Enable but scrub ALL PII
  await SentryFlutter.init((o) {
    o.dsn = 'YOUR_SENTRY_DSN';
    o.sendDefaultPii = false;  // ✅ No PII
    o.beforeSend = (e, {hint}) => e.copyWith(
      user: null,      // ✅ Remove user info
      request: null,   // ✅ Remove request data
    );
  });

  // 3. Amplitude: ONLY if user consented
  if (consent) {
    Amplitude(Configuration(
      apiKey: 'YOUR_AMPLITUDE_KEY',
      trackingOptions: TrackingOptions(
        ipAddress: false,  // ✅ No IP
        adid: false,       // ✅ No ad ID
        idfv: false,       // ✅ No device ID
        latLng: false,     // ✅ No GPS coordinates
        city: false,       // ✅ No city
        region: false,     // ✅ No region
        country: false,    // ✅ No country
        carrier: false,    // ✅ No carrier
        dma: false,        // ✅ No DMA
      ),
    ));
  } else {
    // ✅ No Amplitude = No tracking
    print('User rejected analytics consent');
  }
}

Key Points:

  • Sentry enabled for crash reports (legitimate interest)
  • BUT all PII removed from Sentry
  • Amplitude ONLY enabled if user consents
  • Even with consent, ALL tracking options disabled
  • No IP, no location, no device IDs sent anywhere

References:


Thank you for reading, I hope it is helpful. If you have any questions, feel free to ask on LinkedIn.