Clean Architecture is a design principle that promotes separation of concerns. Aims to create a modular, scalable and testable code base. Clean Architecture provides instructions on how to structure the code base. It also includes guidelines on how to define dependencies between different layers of an application. It is not specific to Flutter.
The clean architecture consists of three layers. These are the Presentation Layer, Domain Layer and Data Layer.Let’s examine these now.
Presentation Layer: The presentation layer is the layer where the user interacts with the application. The presentation layer consists of two main parts. These are Widgets and Presentation Logic Holders. The Widgets section contains widgets, screens and views. In the Presentation Logic Holders section, there are classes such as bloc and getx, where we combine the interface and data.
Domain Layer: The Domain Layer represents the business logic of the feature. Domain Layer includes Use Cases, Entities and Repositories. The domain layer should be agnostic of any specific framework or technology.
Data Layer: Data layer is the data retrieval and data storage part. The data layer consists of three parts. These are Models, Repositories and Data Sources. The data sources part can be APIs, local databases, or other external data providers.
Why Clean architecture ?
Clean architecture benefits us in many areas. Firstly it facilitates testability. Because the layers can be tested independently. The code becomes easier to make changes and additions as the application grows. The codebase becomes more modular as each layer has different responsibilities.
Filling Example
Let’s imagine an application where users come from API and are listed on the screen. First, let’s create the “features” folder for the features of the application. Then create a “users” folder on the “features” folder and add the folder required for our layers.
I created two folders, bloc and screens, inside the Presentation Layer. Then I created the entities, repository and usecases folder for the Domain Layer. Finally I created data_sources, models and repository folder for the Data Layer.
Code Example
I will explain the Entities, Use Case and Bloc parts with more detail in my other posts. For now, let’s go through the example.
To analyze the state of the data, I created the data_state.dart file in the core/resources/ location.
abstractclassDataState<T> { final T? data; final FlutterError? error;
Now we can start coding the layers. Since I have a block and listing screen in my presentation layer, I will not explain it in detail. We can start with the domain layer.
I created the User Entity class in the domain/entity/ for users’ information as follows.
We have completed the domain layer. First, I create the UserModel for the data layer. UserModel inherits from the UserEntitiy class. Here I added the functions of converting the data coming from the API into a model and converting the model into an entity.
UsersRemoteDataSource class is the class from which we retrieve data from the Api. Here we convert the data from the API into a list of models. It would be better to customize the data conversion and error throwing parts according to the API from which you pull the data.
How to Setup Flutter & Firebase with Multiple Flavors using the FlutterFire CLI
If your Flutter app supports multiple flavors and connects to Firebase, you need some extra setup to ensureeach flavor corresponds to a different Firebase environment.
The best approach is to create a separate Firebase project for each flavor. This keeps your development, staging, and production environments separate.
When using custom backends or Dart SDKs like Supabase, you can connect to the correct environment by switching the URL and API key based on the flavor. But Firebase does not offer a Dart SDK and requires some platform-specific setup, making the flavoring process more complex.
Thankfully, the FlutterFire CLI comes to the rescue. I'll walk you through using it to flavor your Flutter & Firebase apps without losing your mind. 😅
Here's what we will cover:
Why do we need FlutterFire?
Installing the Firebase and FlutterFire CLI
FlutterFire Config Syntax for Multiple Flavors
Easier Setup with a Shell Script
Initializing Firebase during App Startup (iOS, Android, and web)
By the end, you'll be able to confidently integrate Firebase into your multi-flavor Flutter app, saving time and avoiding common setup headaches.
Adding Firebase to a Flutter app used to be a tedious process. You’d have to manually download configuration files for each platform (like GoogleService-Info.plist for iOS and google-services.json for Android).
Now, the process is much simpler. You just run flutterfire configure and follow some interactive prompts. Once finished, these files are automatically added to your project:
lib/firebase_options.dart
ios/Runner/GoogleService-Info.plist
android/app/google-services.json
However, when working with multiple flavors, it gets trickier. You’ll need separate versions of these files for each flavor, stored in different locations to avoid overwriting them during the configuration process.
Luckily, FlutterFire 1.0.0 added support for multiple flavors. Let's explore how to use it.
This opens a browser window where you can sign in with Google. After selecting your account, you’ll see this:
Click "Allow" and close the window. Now, the Firebase CLI is logged in.
Note: Make sure you use the Google account linked to the Firebase projects you want to work with. If you’re logged in with the wrong account, run firebase logout and firebase login again.
While flutterfire config handles most of the work, you still need to run it for each flavor with different arguments.
To streamline this, create a flutterfire-config.sh script and save it at the root of your project:
#!/bin/bash# Script to generate Firebase configuration files for different environments/flavors# Feel free to reuse and adapt this script for your own projectsif[[$#-eq0]];thenecho"Error: No environment specified. Use 'dev', 'stg', or 'prod'."exit1ficase$1indev)flutterfireconfig\--project=flutter-ship-dev\--out=lib/firebase_options_dev.dart\--ios-bundle-id=com.codewithandrea.flutterShipApp.dev\--ios-out=ios/flavors/dev/GoogleService-Info.plist\--android-package-name=com.codewithandrea.flutter_ship_app.dev\--android-out=android/app/src/dev/google-services.json
;;stg)flutterfireconfig\--project=flutter-ship-stg\--out=lib/firebase_options_stg.dart\--ios-bundle-id=com.codewithandrea.flutterShipApp.stg\--ios-out=ios/flavors/stg/GoogleService-Info.plist\--android-package-name=com.codewithandrea.flutter_ship_app.stg\--android-out=android/app/src/stg/google-services.json
;;prod)flutterfireconfig\--project=flutter-ship-prod\--out=lib/firebase_options_prod.dart\--ios-bundle-id=com.codewithandrea.flutterShipApp\--ios-out=ios/flavors/prod/GoogleService-Info.plist\--android-package-name=com.codewithandrea.flutter_ship_app\--android-out=android/app/src/prod/google-services.json
;;*)echo"Error: Invalid environment specified. Use 'dev', 'stg', or 'prod'."exit1;;esac
With this script, you still need to set the correct arguments for your project, but you only need to do this once.
Then, generating all the Firebase config files becomes a breeze—no need to remember each argument.
? You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. ›
❯ Build configuration
Target
Then, choose the Debug-dev build configuration:
? Please choose one of the following build configurations ›
Debug
Release
Profile
❯ Debug-dev
Profile-dev
Release-dev
Debug-stg
Profile-stg
Release-stg
Debug-prod
Profile-prod
Release-prod
Note: If you encounter a "Failed to list Firebase projects" error, run firebase logout, then firebase login, and try again.
Next, choose the platforms you want to configure:
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
✔ web
windows
This step may take some time as the CLI registers the necessary apps with Firebase. If successful, you’ll see a confirmation similar to this:
✔ You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. · Build configuration
✔ Please choose one of the following build configurations · Debug-dev
i Found 40 Firebase projects. Selecting project flutter-ship-dev.
✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios, web
i Firebase android app com.codewithandrea.flutter_ship_app.dev is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase android app on Firebase project flutter-ship-dev.
i Firebase ios app com.codewithandrea.flutterShipApp.dev is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase ios app on Firebase project flutter-ship-dev.
i Firebase web app flutter_ship_app (web) is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase web app on Firebase project flutter-ship-dev.
Firebase configuration file lib/firebase_options_dev.dart generated successfully with the following Firebase apps:
Platform Firebase App Id
web 1:424176442589:web:c86e231d1eeaba0e90cf34
android 1:424176442589:android:c5841ba53606b4c490cf34
ios 1:424176442589:ios:592b56a800affa4e90cf34
Learn more about using this file and next steps from the documentation:
> https://firebase.google.com/docs/flutter/setup
Next, repeat the same steps for the stg flavor by running:
./flutterfire-config.shstg
And again for the prod flavor:
./flutterfire-config.shprod
Once complete, your project will have these new files:
One option is to create a firebase.dart file with the following code:
// firebase.dartimport'package:firebase_core/firebase_core.dart';import'package:flutter/foundation.dart';import'package:flutter/services.dart';import'package:flutter_ship_app/firebase_options_prod.dart'asprod;import'package:flutter_ship_app/firebase_options_stg.dart'asstg;import'package:flutter_ship_app/firebase_options_dev.dart'asdev;Future<void>initializeFirebaseApp()async{// Determine which Firebase options to use based on the flavorfinalfirebaseOptions=switch(appFlavor){'prod'=>prod.DefaultFirebaseOptions.currentPlatform,'stg'=>stg.DefaultFirebaseOptions.currentPlatform,'dev'=>dev.DefaultFirebaseOptions.currentPlatform,_=>throwUnsupportedError('Invalid flavor: $flavor'),};awaitFirebase.initializeApp(options:firebaseOptions);}
This works by switching on the appFlavor constant to return the correct FirebaseOptions object based on the flavor.
Note: When running on Flutter web with the --flavor option, you'll get a warning you that flavors are not fully supported. But the appFlavor constant will still return the correct value.
Now, you can simply call await initializeFirebaseApp() in lib/main.dart, which remains the single entry point for the app:
By default, Flutter Flavorizr generates separate entry points like main_dev.dart, main_stg.dart, and main_prod.dart. This can lead to unwanted code duplication, so I prefer using a single main.dart, as shown above, with switch expressions to handle flavor-specific initialization (there's a caveat though—more on this below).
With this setup, the Flutter app will initialize and connect to the correct Firebase project, depending on the flavor.
If you look closely at firebase.dart, you’ll notice that although the correct Firebase config is selected based on the flavor, all threefirebase_options_*.dart files are still imported:
// firebase.dartimport'package:firebase_core/firebase_core.dart';import'package:flutter/foundation.dart';import'package:flutter/services.dart';// Note: all three files are importedimport'package:flutter_ship_app/firebase_options_prod.dart'asprod;import'package:flutter_ship_app/firebase_options_stg.dart'asstg;import'package:flutter_ship_app/firebase_options_dev.dart'asdev;Future<void>initializeFirebaseApp()async{// Determine which Firebase options to use based on the flavorfinalfirebaseOptions=switch(appFlavor){'prod'=>prod.DefaultFirebaseOptions.currentPlatform,'stg'=>stg.DefaultFirebaseOptions.currentPlatform,'dev'=>dev.DefaultFirebaseOptions.currentPlatform,_=>throwUnsupportedError('Invalid flavor: $flavor'),};awaitFirebase.initializeApp(options:firebaseOptions);}
This means that all three files are compiled and bundled during the build process because tree-shaking doesn't work here (the switch happens at runtime).
In theory, this could expose your development or staging environment details (which may be less secure than production) if someone reverse engineers your app.
While this might not be a big issue for apps that don’t handle sensitive data, it’s still a potential risk. If you want to mitigate this entirely, consider a more secure approach. 👇
These files should do one thing only: import the correct firebase_options_*.dart file and pass the config as an argument to a function inside main.dart that performs the actual initialization. Here’s an example:
This approach ensures that only the required Firebase configuration file is bundled, making it a secure and efficient solution for managing multiple flavors.
But how do you run the app with the right flavor? 👇
Both options have their pros and cons, and the right choice depends on your project’s needs:
Option 1: Centralized Firebase Initialization. This approach is easier and quicker to implement. It allows you to use a single main.dart file and handle flavor-specific Firebase options dynamically at runtime. However, because all Firebase configuration files are bundled in the final app (even if they’re not used), it's not the best choice for security reasons.
Option 2: Multiple Entry Points for Each Flavor. This option requires a bit more setup because you’ll need to create separate entry points for each flavor (main_dev.dart, main_stg.dart, main_prod.dart). However, it only bundles the necessary Firebase configuration file for each build, making it a more secure solution, since an attacker won’t have access to the environment details of other flavors.
While option 2 takes a bit more work, I recommend it for multi-flavor Flutter apps that use Firebase. 👍
Option 1 works just fine for non-Firebase apps, since you can use --dart-define-from-file and define environment variables inside separate files (e.g. .env.dev, .env.stg, .env.prod) for each flavor. To learn more, read: How to Store API Keys in Flutter: --dart-define vs .env files.
By leveraging FlutterFire alongside a simple shell script, we’ve streamlined what used to be a complex and error-prone flavoring process. Instead of manually configuring Firebase for each flavor, you can now generate the necessary files for all environments with a single script, saving time and reducing the chance of mistakes.
The centralized Firebase initialization option offers a quick and simple way to connect your app to the correct Firebase project at startup, using a single main.dart file and flavor-specific logic. However, this approach bundles all Firebase configurations, which may not be ideal for security-sensitive apps.
For more secure setups, the multiple entry points strategy ensures that only the necessary Firebase configuration is included in each build, making it a better choice when handling sensitive data or production-grade apps.
With either approach, your Flutter app will automatically connect to the appropriate Firebase environment—whether that’s dev, stg, or prod—ensuring your app behaves as expected in every stage of development and production.
This setup makes it easier to manage multiple flavors in your Flutter & Firebase apps, and I've been happily using it in production for my own apps. ✅
When it comes to shipping and maintaining apps in production, there are many important aspects to consider:
Preparing for release: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&Cs
App Submissions: app store metadata & screenshots, compliance, testing vs distribution tracks, dealing with rejections
Release automation: CI workflows, environment variables, custom build steps, code signing, uploading to the stores
Post-release: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates
My latest course will help you get your app to the stores faster and with fewer headaches.
If you’re interested, you can learn more and enroll here (currently 40% off!).