This post is not legal advice. Let’s just focus on the technical aspects of creating a cookie banner similar to what you find on almost any website these days.

You can build websites with Flutter. And cookie regulations might not apply to just websites, nor to just the cookie: header.

“No cookies here!”

If you’re not using cookies or anything similar to cookies - and you don’t use 3:rd party libraries that does - then you might be in the clear.

“I don’t use cookies, I store my sessions in localstorage”

If your persistence technique approximate what cookies do, then you might have to put up a cookie banner despite your server never emitting a cookie: header.

“I ship native apps, cookies only exist in browsers?”

It might even be the case that a native app might be subject to these laws, not just code running in a web browser.

Websites built with Flutter exist too.

But regardless, if you’re using Flutter Web to produce a website, then you’re clearly producing a website.

So you probably need a cookie banner in some Flutter apps.

For simplicity, let’s assume you’re building a website with Flutter Web, and you’re saving the user’s preferences such as dark mode vs light mode using shared_preferences, (which uses localstorage under the hood), then it seems pretty clear that status quo is to expect you to have a cookie banner. Same if they log in to your backend and you store a session secret with shared_preferences.

Let’s do it!

So how do you build a consent banner in Flutter? Here’s a straight-forward way

Step 1 - visuals

Display the banner visually using Flutter widgets. To get away with less work, perhaps pick some existing modal tool you like such as SnackBar or modal_bottom_sheet. You are often expected to link out to your privacy policy - url_launcher is useful for that. Here’s an example of a simple snack bar:

What should a cookie banner look like? This is what Stack Overflow shows us when visiting for the first time or in incognito mode:

Let’s try to quickly wrap up something similar with Flutter. SnackBar is not visually identical, but feels somewhat out of the way and cookie-banner-esk, let’s try it!:

consent_snackbar.dart

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';

showConsentSnackbar(BuildContext context,
  {bool onlyShowIfNotSet = true}) async {
final prefs = await SharedPreferences.getInstance();
// await prefs.remove('user_consent'); // uncomment to reset for debug purposes
final existingConsentValue = prefs.getString('user_consent');
if (onlyShowIfNotSet && existingConsentValue != null) {
  debugPrint(
      'consent is already set to $existingConsentValue: not showing dialog');
  return;
}
final snackBar = SnackBar(
  content: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        const Padding(
          padding: EdgeInsets.symmetric(vertical: 8.0),
          child: Text(
            'Your privacy',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        ),
        RichText(
            text: TextSpan(
                style: const TextStyle(
                    color: Colors.white, fontStyle: FontStyle.italic),
                children: [
              const TextSpan(text: 'By clicking '),
              const TextSpan(
                  text: 'Accept all Cookies',
                  style: TextStyle(fontWeight: FontWeight.bold)),
              const TextSpan(
                  text: ', you agree that we can store cookies '
                      'on your device and disclose information in accordance with our '),
              TextSpan(
                  text: 'Cookie Policy.',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                  recognizer: TapGestureRecognizer()
                    ..onTap = () => launchUrl(Uri.parse(
                        'https://stackoverflow.com/legal/cookie-policy'))), // use your own instead
            ])),
        const SizedBox(height: 10),
        Row(
          children: [
            ElevatedButton(
              onPressed: () async {
                await prefs.setString('user_consent', 'all');
                debugPrint(
                    'user_consent is now set to ${prefs.getString('user_consent')}');
              },
              child: const Text('Accept all cookies'),
            ),
            const SizedBox(width: 10),
            ElevatedButton(
              onPressed: () async {
                await prefs.setString('user_consent', 'only-essential');
                debugPrint(
                    'user_consent is now set to ${prefs.getString('user_consent')}');
              },
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.grey)),
              child: const Text('Only essential cookies',
                  style: TextStyle(color: Colors.black)),
            )
          ],
        ),
      ]),
  duration: const Duration(seconds: 9999999), // do not auto-close
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

If we try it in a simple app, calling showSnackbar(context) gives us the following result:

Step 2 - store user input for later

If the user clicks one of the buttons, then save their intent - as a drumroll* cookie - or similar. So that they don’t have it interact with the banner every time they open your site or app. In Flutter, including in Flutter Web, shared_preferences is a decent tool for this.

We actually already included that step above, this section:

 onPressed: () async {
                await prefs.setString('user_consent', 'only-essential');
                debugPrint(
                    'user_consent is now set to ${prefs.getString('user_consent')}');
              },

Step 3 - respect the user’s intent, or lack thereof

A correctly implemented user consent dialog should block all cookie-setting activities until the user has indicated their intent in step 2, so you ideally need some sort of callback. Three ideas:

  • A onUserChoice() callback on your consent widget
  • A userConsentProvided Completer that provides a future you can await
  • A simple boolean state provider final consentProvider = StateProvider<bool>(_) => false implemented with Riverpod, that you can listen to in your widgets and services.

Which one is best for you depends on what workflow you’re used to and what you use cookies or cookie-like mechanisms for.

Waiting for consent primarily affects writing of state. Wheras reading cookie values potentially set previously should be fine to do at any point in time. Just make sure to hold off with those writes!

One example in this direction is included above: if consent is set, don’t show the banner, is implemented like so:

final prefs = await SharedPreferences.getInstance();
// await prefs.remove('user_consent'); // uncomment to reset for debug purposes
final existingConsentValue = prefs.getString('user_consent');
if (onlyShowIfNotSet && existingConsentValue != null) {
  debugPrint(
      'consent is already set to $existingConsentValue: not showing dialog');
  return;
}

If you like this article, you might also like the cookie_consent Flutter package we made, that goes a bit deeper by playing with different visuals and adding a settings screen similar to Stack Overflow’s.

Try it out on pub.dev!




sponsor

Need private photo sharing that's not tied up in the social media circus? Photoroll is a good way to share photos with your clients.
Or privately gather media from participants of weddings and other small to medium-sized events.