Multi-environment Flutter Projects with Flavors

In a real-world Flutter project, you usually develop against separate environments, such as development and production. Each environment can have its own back end, API, configuration, and even separate visual identities if you want to clearly differentiate them.

Flutter Flavors make it possible to run different versions of your app on the same device. At the same time, they keep those environments isolated from one another.

In this tutorial, you’ll transform a single-environment Flutter project called BuzzwordBingo into a multi-flavor app. In particular, you’ll learn how to:

  • Isolate your configuration for each environment.
  • Flavorize your Android and iOS apps.
  • Flavorize the web part of your project.
  • Integrate Firebase and work with separate Firebase projects.

So get ready to track all the buzzwords in the next marketing presentation you watch!

NB: In this tutorial, the Firebase configuration focuses on a typical use case with Firebase and Cloud Firestore. If your project requires native plugins like Firebase Analytics, Crashlytics or Performance Monitoring, you will need to configure Firebase integration differently. We might cover this more advanced use case in a future tutorial.

Getting Started

Download the starter project by clicking here. You can either clone this starter branch with git or download a ZIP file by clicking the green Code button on Github. Open the project in the latest version of Android Studio with the latest version of the Flutter and Dart plugins installed.

Also, make sure that you use Flutter version 2.10 or above. Open pubspec.yaml and click Pub get in the upper right corner of the editor to download all dependencies.

Next, select a mobile simulator and run the app. You’ll see a home screen with three pre-filled buzzwords and a text field to add new ones.

BuzzwordBingo running on a mobile simulator
Buzzword Bingo running on a mobile simulator

BuzzwordBingo helps you keep track of all the buzzwords used in a marketing presentation. If you type a new buzzword in the top text field, and then the enter or the done button, you add a new buzzword to the list. If you type any of the existing buzzwords, you increase its count by one.

In the end, you’ll store buzzwords in a Firebase Firestore collection. But the starter project stores buzzwords in memory.

So if you restart the app, the state is reset. That’s not what you want, so you’ll need to add some persistence to this app. But first, you have to prepare your project to handle several environments. So, let’s start with configuration.

Isolating Your App’s Configuration

When your app is available in several flavors, you need to customize some settings for each of those flavors. For example, those settings can include the app’s name, colors and some of its keys for third-party APIs.

A naive approach would be to detect the app variant currently running everywhere you need to customize something. But that would quickly become unmanageable.

Instead, you’ll group all those environment-specific settings into the following AppConfig class. In lib, create a new file named app_config.dart and add:

import 'package:flutter/material.dart';

// 1
enum Environment { dev, prod } 

// 2
class AppConfig extends InheritedWidget {
  // 3
  final Environment environment;
  final String appTitle;

  // 4
  const AppConfig({ 
    Key? key, 
    required Widget child, 
    required this.environment, 
    required this.appTitle,
  }) : super(
          key: key,
          child: child,
        );

  // 5
  static AppConfig of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppConfig>()!;
  }

  // 6
  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; 
}
Code language: Dart (dart)

Here’s a code breakdown:

  1. First, you declare an Environment enumerated type to define your environments.
  2. Next, you declare the AppConfig class, which is an InheritedWidget. You can attach an InheritedWidget to a BuildContext. This way, you can access this InheritedWidget from any of its descendants in the widget tree.
  3. Your AppConfig class has two properties: one for environment and one for the environment-specific appTitle.
  4. The constructor takes required values for your environment and environment-specific properties. It also takes a child widget. AppConfig will sit at the root of your widget tree.
  5. The static of function makes it easy to access the closest instance of AppConfig by searching the BuildContext parameter and all its parents. That’s what BuildContext‘s dependOnInheritedWidgetOfExactType does.
  6. Finally, since AppConfig extends InheritedWidget, it has to override updateShouldNotify. The framework calls this function to decide whether it should notify widgets that use AppConfig when values inside AppConfig change. Since all AppConfig‘s properties are final and AppConfig is immutable, you can return false.

By default, when you build and run a Flutter project, it runs the main function inside main.dart. To have several flavors of your app, you need one entry point for each flavor.

Inside lib, create a file named main_dev.dart and add the following code to it:

import 'package:flutter/material.dart';

import 'app_config.dart';
import 'buzzword_bingo_app.dart';

void main() {
  // 1
  const configuredApp = AppConfig(
    child: BuzzwordBingoApp(),
    // 2
    environment: Environment.dev,
    // 3
    appTitle: '[DEV] BuzzwordBingo',
  );
  // 4
  runApp(configuredApp);
}
Code language: Dart (dart)

The main function:

  1. Wraps AppConfig around calling BuzzwordBingoApp.
  2. Sets the environment property to the dev environment.
  3. Adds [DEV] to the appTitle.
  4. Then it runs the app.

Next, in the lib folder, create a file called main_prod.dart.

import 'package:flutter/material.dart';

import 'app_config.dart';
import 'buzzword_bingo_app.dart';

void main() {
  const configuredApp = AppConfig(
    child: BuzzwordBingoApp(),
    // 1
    environment: Environment.prod,
    // 2
    appTitle: 'BuzzwordBingo',
  );
  runApp(configuredApp);
}
Code language: Dart (dart)

The code is almost the same as in main_dev.dart, except:

  1. The environment property is set for prod.
  2. appTitle is the production title.

Now that you’ve set up the dev and prod environments, delete lib/main.dart.

Open lib/buzzword_bingo_screen.dart which declares the app’s main screen. After // TODO: replace with AppConfig-extracted app title, replace the title property with:

title: Text(AppConfig.of(context).appTitle),Code language: Dart (dart)

Instead of using a generic constant, you inject the environment-specific value of appTitle. There’s a compile error so make sure to import app_config.dart to resolve it.

import 'app_config.dart';Code language: Dart (dart)

Next, open lib/buzzword_bingo_app.dart and look for the comment that says // TODO: replace with AppConfig-extracted app title. Replace the title of your MaterialApp, like you did before:

title: AppConfig.of(context).appTitle,Code language: Dart (dart)

Like before, import app_config.dart as well.

import 'app_config.dart';Code language: Dart (dart)

Last but not least, in Android Studio’s toolbar, click the build configuration dropdown. Then click Edit configurations….

Edit Configurations...

Rename the Run/Debug configuration from main.dart to dev. In Dart entrypoint, replace main.dart at the end of the path with main_dev.dart.

Rename the Run/Debug configuration

Duplicate this configuration by selecting dev under Flutter in the left menu. Then click Copy Configuration.

Copy the dev build configuration

Rename the new configuration prod.

In Dart entrypoint change main_dev.dart to main_prod.dart and click OK.

Configure the prod Run/Debug configuration

Back in the editor, select dev as your Run/Debug configuration and click Debug to run the app again.

Select the dev configuration

Note that the title in the app bar contains [DEV].

Dev configuration running in a mobile simulator

Select prod as your build configuration. This time, the title is Buzzword Bingo:

Prod configuration running in a mobile simulator

If you run the app in a browser by selecting Chrome (web) as a Flutter Device, this is what you get for the dev flavor:

Dev configuration running in a web browser

As you can see, both the title in the app bar and the page’s title in the browser tab are environment-specific.

Now you can install either the dev or the prod version of your app, but not both on the same mobile device yet. That’s because, on Android as in iOS, both versions of your app have the same identifier. You need to configure each native project to use a different identifier for each flavor.

You’ll start with the Android app.

Preparing Your Android App

In Android Studio, open android/app/build.gradle. In the defaultConfig section, notice that applicationId is com.epseelon.buzzwordbingo.

That’s perfect for the production version of your app, but you want to override it for its development variant. This is where you’re going to use Android flavors.

Under // TODO: insert flavor configuration here, add:

// 1
flavorDimensions "env"

// 2
productFlavors {
    // 3
    dev {
        dimension "env"
        applicationIdSuffix ".dev"
        resValue "string", "app_name", "[DEV] BuzzwordBingo"
    }
    // 4
    prod {
        dimension "env"
        resValue "string", "app_name", "BuzzwordBingo"
    }
}Code language: Gradle (gradle)

In this code:

  1. You declare a new flavor dimension called env.
  2. Then you specify all the product flavors and override certain properties for each value of the env dimension.
  3. For the dev environment, you add a suffix to the applicationId. This way, the identifier for the dev version of your app is com.epseelon.buzzwordbingo.dev. You also define a new string resource called app_name which you’ll use in a minute.
  4. For the prod flavor, notice that applicationSuffix isn’t listed. This is because you’re keeping the default applicationId without any suffix, but you set the app_name string resource to a different value.

Now, open android/app/src/main/AndroidManifest.xml. Find this comment: <!-- TODO change the android:label attribute here -->.

Replace the content of the android:label attribute with:

  android:label="@string/app_name"Code language: XL (xl)

This ensures that each flavor of your app shows a different name on the Android launcher.

Notice that android:icon uses a mipmap resource called ic_launcher. Thanks to the flavor configuration you added earlier, Gradle looks for this resource under android/app/src/dev and android/app/src/prod respectively for each flavor. You’ll find environment-specific icons in those folders.

Flavor-specific icons

Now that you customized the app’s identifier, name and icon, you need to update the dev and prod configurations. Click Edit Configurations again.

Edit Configurations...

For the dev configuration, type dev in the Build flavor field to point Flutter’s build to the dev flavor of the Gradle project as you configured it.

Add dev build flavor

Do the same for the prod configuration, setting Build flavor to prod.

Add prod build flavor

Click OK.

Make sure you have an Android emulator running. First, run your app on the Android emulator with the dev configuration.

Dev app running in an Android simulator

Now run the prod configuration.

Prod app running in an Android simulator

Leave both apps to come back to the launcher. You’ll see that both flavors of the app are there, with different names and icons:

Both apps installed in parallel on the same simulator

You can even run both of them at the same time, with different buzzwords.

Android app: check! Now on to your iOS app.

Preparing Your iOS App

Creating Build Configurations

In Android Studio, right-click the root of the project in the Project view, navigate to Flutter and choose Open iOS module in Xcode.

Menu to open iOS module in XCode

In Xcode’s Project navigator, select the root Runner, then Runner project and finally Info tab. In the outlined Configurations section, Xcode created three build configurations for you: Debug, Release and Profile.

Access the Info tab for the Runner project

First, rename each of these three configurations respectively to Debug-prod, Release-prod and Profile-prod.

Rename all configurations by adding a "-prod" suffix

Next, click the + button under the list of configurations. Then click Duplicate “Debug-prod” Configuration.

Duplicate Debug-prod configuration

Rename the new configuration Debug-dev.

Rename it into Debug-dev

Do the same for Release-dev and Profile-dev until you get this list of configurations:

Do the same for all the configurations

Next, you need to create separate schemes for dev and prod.

Creating Schemes

In Xcode’s top bar, click the Runner scheme, which has a green icon, and then Manage schemes….

Manage schemes...

In the dialog that pops up, rename the Runner scheme to prod. With this scheme still selected, click the ellipsis icon under the list of schemes and then Duplicate.

Rename the Runner scheme to "prod"

This creates a new scheme and opens a new dialog to configure it. Rename it to dev. Next, press Enter.

You’ll land back in the list of schemes:

Rename the new scheme into "dev"

Time to configure the build configurations each scheme uses. Double-click the dev scheme and the list of configurations is displayed.

Select the dev build configuration for each stage of the dev scheme

Select Run and set the Build Configuration to Debug-dev. Repeat these steps for the Test and Analyze build configurations.

Select Debug-dev build configuration for the Run stage

Now, select Profile and set its Build Configuration to Profile-dev.

Select Profile-dev build configuration for the Profile stage

Finally for Archive, set the Build Configuration to Release-dev.

Select Release-dev build configuration for the Archive stage

Click Manage Schemes… to return to the list of schemes.

Manage Schemes...

Double-click the prod scheme.

Check the prod scheme

Configure all the build phases to use the -prod build configurations.

Next, you’ll override settings for each scheme and build configuration.

Customizing Settings for Each Scheme

In the Project navigator, select Info or Info.plist in the Runner group. Look for the Bundle display name entry and change BuzzwordBingo to $(APP_NAME).

Set Bundle display name to $(APP_NAME)

Now in the Project navigator, select Runner at the root again and then the Runner TARGET. Go to the Build Settings tab.

Access Build Settings for the Runner target

In the tab’s toolbar, click the + button and Add User-Defined Setting. Rename the new setting APP_NAME to match the variable name you used earlier in Info.plist. Next, type [DEV] BuzzwordBingo as a value in dev-related build configurations and BuzzwordBingo for prod-related ones.

Customize APP_NAME for each build configuration

Next, in the search bar of the Build Settings tab, type AppIcon.

Search for AppIcon in Build Settings

Expand Primary App Icon Set Name. Then add the appropriate suffix corresponding to each build configuration:

Set build configuration-specific app icons

These names match the assets included in the starter project.

Still in the Build Settings tab in the Runner target settings. This time, search for bundle identifier. Expand the Product Bundle Identifier setting and add .dev as a suffix for all three dev-related build configurations, like so:

Configure bundle identifier for each build configuration

Now, you can run both flavors of your app on the same iOS device.

Go back to Android Studio. Select an iOS simulator and the dev build configuration that points to the dev flavor.

Build and run. You’ll see the development app.

Dev app running on an iOS simulator

Run the prod build configuration and notice the title in the app bar is the prod version.

Prod app running on an iOS simulator

After running the prod version, look at the home screen and you’ll see both apps installed side by side on your iOS device, with different names and different icons.

Both iOS apps installed in parallel on the same simulator

Preparing Your Web App

In the starter project, you’ll find a webenv folder. This folder contains a subfolder for each environment. Inside each environment-specific folder, there’s a favicon.png and a couple of other icon images.

Webenv folder with environment-specific files

You’ll need to copy the content of the correct flavor-specific webenv subfolder to the web folder before you run your app. The starter project contains scripts to automate this process for both Windows and Unix/macOS.

OS-specific scripts to copy the content of the proper webenv subdirectory to web

The copy_webenv.* scripts are pretty straightforward.

Windows: xcopy webenv\%1 web\ /E/Y
Unix/macOS: cp -rf ./webenv/$1/* web/

These scripts take the environment name as a parameter; %1 and $1 respectively. So all you need to do is call that script with the right parameter right before running the app.

In Android Studio, click the build configuration drop-down menu and then Edit Configurations….

Edit Configurations...

In the toolbar of the left menu, click the + icon to add a new configuration and scroll down and select Shell script.

Add a new "Shell Script" Run/Debug configuration

Rename the shell script configuration to Copy webenv dev.

In the Script path field, select either copy_webenv.sh if you’re on macOS or Linux, or copy_webenv.cmd in you’re on Windows. As shown earlier those scrips are at the project root level.

Then set Script options to dev and leave Working directory to the root of your project.

Here is what you must end up with:

Configure the "Copy webenv dev" Run/Debug Configuration

Using the Copy Configuration button in the toolbar of the left panel, duplicate the Copy webenv dev and rename the copy to Copy webenv prod. Type prod in the Script options field.

Configure the "Copy webenv prod" Run/Debug Configuration

Click Apply to save the new configurations.

Then, in the left panel, expand the Flutter section and select the dev configuration. In the Before launch section, click the + button, then Run Another Configuration:

Add a "Run Another Configuration" step before launching the dev Flutter Run/Debug Configuration

Select Copy webenv dev in the list that pops up. Here is what you must end up with:

Double check your "Before launch" step

Repeat the process for the prod Flutter configuration. Add a new configuration before launch and select Copy webenv prod this time. Click OK.

Now select Chrome (web) as a target, the variant of your app you want to run and click Run or Debug.

Select "Chrome (web)" as a target and debug

Keep an eye on the favicon in the browser tab for each flavor:

Dev app running in a web browser
Prod app running in a web browser

Integrating Firebase

If you look at lib/buzzword_bingo_screen.dart, you’ll see that for now, the app reads buzzwords from a StreamBuilder. It initializes this StreamBuilder with a List of Buzzwords. It manipulates the Stream using a StreamController. So every time you rerun the app, it starts with the same initial list and forgets about the buzzwords you might have added or changed. It’s time to make this more persistent, but in a way that lets you manipulate separate databases for each flavor. You’ll do that using Firebase’s Cloud Firestore.

Note: Firebase integration into a Flutter project used to be more painful than it is now. You had to integrate it at a native level, download separate configurations for Android, iOS and the web and it was quite error-prone. But recently, the FlutterFire team changed that for the better. So if you already experienced the legacy Firebase/Flutter integration process, this tutorial will show you the new way. But again, keep in mind that certain Firebase features like Analytics, Crashlytics or Performance Monitoring still required legacy native configuration. We might cover this in a future tutorial.

Integrating Firebase Core

flutter pub add firebase_coreCode language: Shell Session (shell)

In Android Studio, open the Terminal panel and type the following command:

This will add the following line to pubspec.yaml:

firebase_core: ^1.15.0Code language: YAML (yaml)

Note that the exact version may vary if a more recent version is available.

Next, since the new integration process uses FlutterFire CLI, which depends on Firebase CLI, you need to install Firebase CLI on a global level, and log in if you have not already done so. If you have NodeJS 10+ installed on your system, the easiest way to do it is to run the following command, whether you’re on Windows, Linux or macOS:

npm install -g firebase-toolsCode language: Shell Session (shell)

If you don’t have NodeJS, there are other ways described in Firebase CLI’s documentation.

Then, log in to your Firebase account with the following command:

firebase loginCode language: Shell Session (shell)

If you’re not logged in yet, this command will open a browser to let you log in to your account. Then, run the following command in the same terminal to install FlutterFire CLI:

dart pub global activate flutterfire_cliCode language: Shell Session (shell)

Next, you need to prepare directories for your multi-flavor setup. In your project, create the following directories:

  • lib/firebase/dev
  • lib/firebase/prod

Then, back in the Terminal, run the following command to create a Firebase project for your dev app:

flutterfire configure -i com.epseelon.buzzwordbingo.dev \
-a com.epseelon.buzzwordbingo.dev \
-o lib/firebase/dev/firebase_options.dart \
--no-apply-gradle-plugins \
--no-app-id-jsonCode language: Shell Session (shell)

This command will create a Firebase project and register an iOS app with com.epseelon.buzzwordbingo.dev as a bundle identifier. It will also register an Android app with com.epseelon.buzzwordbingo.dev as its package name. It will generate a configuration class in lib/firebase/dev/firebase_options.dart.

Finally, it will not apply gradle plugins in the Android project and it will not generate an application id file for iOS, which are only useful if you use Firebase services like Analytics, Performance Monitoring or Crashlytics

But the command is interactive. First, it retrieves a list of existing projects associated with the Firebase account you are logged in to. Using the arrow keys on your keyboard, place the cursor in front of <create a new project> and type Enter.

Then it asks for a project id for your new Firebase project, which needs to be unique across all of Firebase. So feel free to come up with a unique identifier of your own, something like buzzwordbingo-dev-<a number of your choice>.

The next question is about which platforms you want your configuration to support. Leave the default selection of android, ios and web as it is.

Once the command is complete, it generates lib/firebase/dev/firebase_options.dart with the DefaultFirebaseOptions class in it.

Next, repeat the process for the prod flavor of your app, using the following command:

flutterfire configure -i com.epseelon.buzzwordbingo \
-a com.epseelon.buzzwordbingo \
-o lib/firebase/prod/firebase_options.dart \
--no-apply-gradle-plugins \
--no-app-id-jsonCode language: Shell Session (shell)

Notice that the iOS bundle identifier, the Android package name and the output are different.

Answer the questions like before and in the end, you’ll have a corresponding lib/firebase/prod/firebase_options.dart file.

NB: You should keep these firebase_options.dart files out of any public version control as they contain Google API keys you might want to keep private.

You can open the Firebase Console to see the two projects you have just created.

Back in Android Studio, open lib/main_dev.dart. Make the main() function async so that you can use await inside it.

void main() async {
  ...
}
Code language: Dart (dart)

Then insert the following code at the beginning of the main() function, right before the AppConfig initialization:

WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
  options: DefaultFirebaseOptions.currentPlatform,
);Code language: Dart (dart)

The first line makes sure all the Flutter environment initialization is complete. Then you initialize Firebase itself using the DefaultFirebaseOptions class that FlutterFire CLI generated earlier.

Add the following imports:

import 'package:firebase_core/firebase_core.dart';
import 'firebase/dev/firebase_options.dart';Code language: Dart (dart)

Notice that since you’re in the dev entry point, you import the dev Firebase project configuration.

Repeat the same process for lib/main_prod.dart until you end up with:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'firebase/prod/firebase_options.dart';
import 'app_config.dart';
import 'buzzword_bingo_app.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  const configuredApp = AppConfig(
    child: BuzzwordBingoApp(),
    environment: Environment.prod,
    appTitle: 'BuzzwordBingo',
  );
  runApp(configuredApp);
}
Code language: Dart (dart)

Enabling Firestore

Open Firebase Console and select the dev project you created earlier with FlutterFire CLI.

In the left menu, under the Build section, select Firestore Database.

Next, click the Create database button to enable Firestore. In the dialog that pops up, select Start in test mode.

Then, select the Cloud Firestore location closest to you and click Enable.

Repeat the same process in your prod Firebase project.

Then, back in Android Studio, in the Terminal, run the following command to add the latest version of Firestore as a dependency:

flutter pub add cloud_firestoreCode language: Shell Session (shell)

This will add the following line to your pubspec.yaml. The version might be different but that’s OK.

cloud_firestore: ^3.1.13Code language: YAML (yaml)

Integrating Firestore in iOS

Then, open ios/Podfile, uncomment the line after the comment that says # TODO: Uncomment this line to define a global platform for your project. Set the minimum iOS version supported by your app to 10.0, which Cloud Firestore now requires:

platform :ios, '10.0'Code language: plaintext (plaintext)

Still in ios/Podfile, replace # TODO: Add precompiled Firestore dependency here with:

pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '8.15.0'Code language: JavaScript (javascript)

This adds a precompiled version of some Firestore dependencies to greatly speed up your iOS build.

Then run the app on an iPhone simulator if you can. If your build fails with an error message that starts with Error running pod install, look for another part of the error log that says something like this:

[!] CocoaPods could not find compatible versions for pod "FirebaseFirestore":
  In Podfile:
    FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `8.14.0`)

    cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) was resolved to 3.1.13, which depends on
       Firebase/Firestore (= 8.15.0) was resolved to 8.15.0, which depends on
         FirebaseFirestore (~> 8.15.0)Code language: Shell Session (shell)

It means that your version of cloud_firestore depends on a more recent version of the precompiled firestore-ios-sdk-frameworks dependency. Normally, the error message should also mention which version it expects. In that case, upgrade the dependency to that version in ios/Podfile, delete ios/Podfile.lock (if it already exists) and the entire ios/Pods directory, and run your app again.

Now, you need to configure Android for Firestore.

Integrating Firestore in Android

Open android/app/build.gradle and replace this line:

minSdkVersion flutter.minSdkVersionCode language: Gradle (gradle)

With this one:

minSdkVersion 19Code language: Gradle (gradle)

The Firestore SDK for Android also comes with a lot of functions. By default, Android limits the number of functions you can compile into an app. You need to enable multidex support to work around that. Flutter recently added a --multidex command-line argument that should take care of that. But in my experience, it doesn’t work very well, so it’s best to do it manually. Still in android/app/build.gradle, replace the comment that says // TODO: enable multidex here with:

multiDexEnabled trueCode language: Gradle (gradle)

Finally, at the end of the file, in the dependencies section, replace the comment that says // TODO: multidex dependency here with:

implementation 'com.android.support:multidex:1.0.3'Code language: Gradle (gradle)

With that, you can run the app on an Android simulator.

Updating BuzzwordBingo to Use Firestore

Open lib/buzzword_bingo_screen.dart. Replace the comment that says // TODO: Add Firestore properties here with:

final _firestore = FirebaseFirestore.instance;
late CollectionReference<Map<String, dynamic>> _buzzwordsCollection;Code language: Dart (dart)

Then, add the missing import:

import 'package:cloud_firestore/cloud_firestore.dart';Code language: Dart (dart)

Next, replace the entire implementation of _watchBuzzwords() with:

Stream<List<Buzzword>> _watchBuzzwords() {
  // 1
  _buzzwordsCollection = _firestore.collection('buzzwords');
  // 2
  return _buzzwordsCollection.orderBy('word').snapshots().map((snapshot) {
    // 3
    return snapshot.docs.map((document) {
      // 4
      final documentData = document.data();
      return Buzzword(
        word: documentData['word'] as String,
        count: documentData['count'] as int,
      );
    }).toList();
  });
}Code language: Dart (dart)
  1. Create a reference to a buzzwords collection in your Firestore database.
  2. Return a stream that receives an event each time a change occurs on your collection of buzzwords sorted by word.
  3. Each stream event is a Firestore QuerySnapshot that you need to map to a list of Buzzwords.
  4. In turn, you can map each QuerySnapshot document onto a new instance of Buzzword.

With that, your grid of buzzwords will synchronize with your Firestore database.

Then, replace the implementation of _addWord(String word) {} with:

void _addWord(String word) async {
  // 1
  _wordController.clear();
  
  // 2
  final buzzwords =
      await _buzzwordsCollection.where('word', isEqualTo: word,).get();
  if (buzzwords.size == 0) {
    // 3
    await _buzzwordsCollection.add(<String, dynamic>{
      'word': word,
      'count': 1,
    });
  } else {
    // 4
    final buzzwordDocument = buzzwords.docs.first;
    final oldCount = buzzwordDocument.data()['count'] as int;
    await buzzwordDocument.reference.update({'count': oldCount + 1});
  }
}Code language: Dart (dart)
  1. First, you clear the text field.
  2. You query the buzzwords collection to look for any existing buzzword with the same word.
  3. If there are none, you create a new one with a count of 1.
  4. If there is one already, you update the existing one.

Finally, you can remove the declarations of the _buzzwords and _buzzwordsStreamController properties.

Running the App

Now you can run the dev app on a simulator:

Dev app plugged into the dev Firebase project running in a mobile simulator

It starts with an empty list of buzzwords since your database is still empty. Try adding a couple of buzzwords and incrementing their count:

Adding a few buzzwords in the dev app

Check your Firebase console and you’ll see the new buzzwords in the database:

New buzzwords appear in your Firestore console

Now run the prod version of your app. It starts with an empty screen, since it connects to an separate database. Try adding different buzzwords:

Adding buzzwords in the prod app running in a mobile simulator

You can find the buzzwords you add in the Firebase console of your prod project too.

Last but not least, run the dev version of your app in a browser:

Dev app running in a web browser

It starts with the buzzwords you created earlier, which demonstrates you have persistence now.

Where to go from here?

In this tutorial, I showed you how to make your Flutter project multi-flavored, how to isolate your configuration, run several flavors of the same app on the same device, and how to integrate this setup with Firebase as a back end.

You can download the final version of this project by cloning this final branch from Github, or downloading the ZIP file for it.

I have been wanting to create this tutorial for a long time, as a lot of those I found out there do not cover web configuration and/or the latest and greatest of FlutterFire.

In the future, I might follow up with more details about how to configure your multi-flavor project when you need Firebase libraries like Analytics, Crashlytics or Performance monitoring.

But for now, this should be enough to get you started with your next project. Of course, if you don’t use Firebase as your backend, it’s easy to add other environment-specific backend URLs to AppConfig.

Hopefully, you will find this tutorial useful. If you have any question, feel free to leave them in the comments.