In an era of so much rapid development in this field of technology, efficiency and speed in the development of a mobile application are very important. In this article, I will discuss the flavor of Flutter that makes mobile application development more simple and seamless, as well as the solution to the problem of efficiency and speed in the development of mobile applications that we are facing today.
🔍 Navigating the Challenges
Imagine you, as a mobile developer, are asked to create a complex application. You are asked for your application to have several versions for several target users. For example, in an application that already exists in the Play Store and App Store, you are required to create three applications: applications that are used for global users, applications used for testers, and the last application used for developers.
If you create the three applications manually, imagine how tricky and time-consuming the development process is. You need to customize the UI, change the source of your data, and build it one by one for every required application. In some cases, you may even need more than three of the same applications in different environments.
Here's the flavor role that can help simplify and shorten the problem that was discussed earlier: With Flutter Flavor, you just need to create one source code; all you need to do is just configure whatever environment you're going to make and whatever content or assets that you'll make dynamic on each application in a different environment. So implementing Flutter Flavor will not only speed up and simplify the development process, but it will also reduce the risk of errors that will occur if you separate ingredients manually.
🔧 How it works?
In this current case, we will try to differentiate the application name, application logo, and base URL API for every application variant. This configuration will later be encapsulated in every flavor file you create, which will then be dynamically accessible to your source code in real time.
To illustrate how Flutter Flavor works, first we need to name our three applications as more clear and understandable environments by engineers; usually, these three environments will be separated into "production, staging, and development.”
Production
This environment will be used for the entire final user that will be using your application, which will be on the Play Store and App Store.Staging
This environment will be used for application testing that also reflects production data without interfering with original production data.Development
This environment will be used for developers, where developers develop applications and also experiment.
The way it works is that you need to define the three environments described; each environment needs a configuration file that contains contents that you’re going to create dynamically (in this case, the name of the application, the application's logo, and the API base URL). Then you have to do the configuration on each native platform (Android and iOS), so that the native platform that you build will later know the three platforms that you have created.
After making the configurations on the native platform, you also need to configure the IDE that you use (Android Studio or Visual Studio Code) so the IDE knows every environment you’ve created, which later will make it easier for you when running or debugging your applications (you just need to choose the environment that you’re going to run).
💻 Implementation
Let's demonstrate the implementation of flavors in a Flutter project using the example outlined above:
Step 1: Create Flavor Configuration Files
flavor_config.dart
import 'package:flutter/material.dart';
// Define an enum for every environment you want to create.
enum Flavor { prod, staging, dev }
// Add more values as needed.
class FlavorValues {
final String apiBaseUrl;
final String appIcon;
final String appName;
FlavorValues({
required this.apiBaseUrl,
required this.appIcon,
required this.appName,
});
}
class FlavorConfig {
final Flavor flavor;
final String name;
final Color color;
final FlavorValues values;
static late FlavorConfig _instance;
factory FlavorConfig({
required Flavor flavor,
// Color is an optional value that will be used for the flavor banner.
// Flavor banner is one way to identify the environment you used.
// You can remove this line of code if you don't need it.
Color color = Colors.blue,
required FlavorValues values,
}) {
_instance = FlavorConfig._internal(flavor, flavor.name, color, values);
return _instance;
}
FlavorConfig._internal(this.flavor, this.name, this.color, this.values);
static FlavorConfig get instance {
return _instance;
}
// Method to check your current environment in your realtime code
static bool isDevelopment() => _instance.flavor == Flavor.dev;
static bool isStaging() => _instance.flavor == Flavor.staging;
static bool isProduction() => _instance.flavor == Flavor.prod;
Step 1.5 (Optional): Create Flavor Banner Configuration Files
flavor_banner.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'flavor_config.dart';
const double _kHeightBanner = 12.0;
const double _kOffsetBanner = 40.0;
final double _kSizeTriangle =
_kOffsetBanner + 0.707 * _kHeightBanner * (Platform.isIOS ? 2 : 3);
class FlavorBanner extends StatelessWidget {
final Widget child;
final BannerConfig? bannerConfig;
const FlavorBanner({
Key? key,
required this.child,
this.bannerConfig,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// You need to make sure your flavor banner doesn't show up in production.
// If you want to show the banner in production, delete this line below.
if (FlavorConfig.isProduction()) return child;
BannerConfig config = bannerConfig ?? _getDefaultBanner();
return Stack(
children: <Widget>[
child,
_buildBanner(
context: context,
bannerConfig: config,
),
],
);
}
BannerConfig _getDefaultBanner() {
return BannerConfig(
bannerName: FlavorConfig.instance.name,
bannerColor: FlavorConfig.instance.color,
);
}
// Creating banner allies so the banner will not be too long and overlap the UI.
String getBannerAlias(String bannerName) {
switch (bannerName) {
case "production":
return "PROD";
case "stage":
return "STAGE";
case "dev":
return "DEV";
default:
return "";
}
}
Widget _buildBanner({
required BuildContext context,
required BannerConfig bannerConfig,
BannerLocation location = BannerLocation.topStart,
}) {
return CustomPaint(
painter: BannerPainter(
message: getBannerAlias(bannerConfig.bannerName),
textDirection: TextDirection.ltr,
layoutDirection: TextDirection.ltr,
location: location,
color: bannerConfig.bannerColor,
),
child: ClipPath(
clipper: _TriangleClipper(location: location),
child: Container(
width: _kSizeTriangle,
height: _kSizeTriangle,
color: Colors.transparent,
),
),
);
}
}
class BannerConfig {
final String bannerName;
final Color bannerColor;
BannerConfig({required this.bannerName, required this.bannerColor});
}
class _TriangleClipper extends CustomClipper<Path> {
_TriangleClipper({
required this.location,
});
BannerLocation location;
@override
Path getClip(Size size) {
final path = Path();
if (location == BannerLocation.topStart) {
path.lineTo(size.width, 0);
path.lineTo(size.width, Platform.isIOS ? 0 : size.height / 4);
path.lineTo(0, size.height);
path.lineTo(0, 0);
}
if (location == BannerLocation.topEnd) {
path.lineTo(size.width, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, Platform.isIOS ? 0 : size.height / 4);
path.lineTo(0, 0);
}
if (location == BannerLocation.bottomStart) {
path.lineTo(0, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.lineTo(0, 0);
}
if (location == BannerLocation.bottomEnd) {
path.lineTo(size.width, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.lineTo(size.width, 0);
}
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return false;
}
}
Step 2: Create a mains file for every environment you’ve created
main_prod.dart
import 'main.dart';
import 'utils/flavor_config.dart';
void main() async {
FlavorConfig(
flavor: Flavor.prod,
values: FlavorValues(
apiBaseUrl: "https://apibaseurl-prod.com",
appIcon: "assets/images/app_icon_prod.png",
appName: "Application Production",
),
);
// Call your main method in here
initializeApp();
}
main_staging.dart
import 'utils/flavor_config.dart';
import 'main.dart';
void main() async {
FlavorConfig(
flavor: Flavor.staging,
values: FlavorValues(
apiBaseUrl: "https://apibaseurl-staging.com",
appIcon: "assets/images/app_icon_staging.png",
appName: "Application Staging",
),
);
// Call your main method in here
initializeApp();
}
main_dev.dart
import 'utils/flavor_config.dart';
import 'main.dart';
void main() async {
FlavorConfig(
flavor: Flavor.dev,
values: FlavorValues(
apiBaseUrl: "https://apibaseurl-dev.com",
appIcon: "assets/images/app_icon_dev.png",
appName: "Application Dev",
),
);
// Call your main method in here
initializeApp();
}
and your original main.dart
will looks like this
import 'package:flutter/material.dart';
import 'package:flutter_flavor_implementation_example/presentations/home_page.dart';
import 'package:flutter_flavor_implementation_example/utils/flavor_banner.dart';
import 'package:flutter_flavor_implementation_example/utils/flavor_config.dart';
void initializeApp() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: FlavorConfig.instance.values.appName,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
builder: (_, child) {
return FlavorBanner(
child: child ?? const SizedBox(),
);
},
home: HomePage(title: FlavorConfig.instance.values.appName),
);
}
}
and your home_page.dart
will looks like this
import 'package:flutter/material.dart';
import 'package:flutter_flavor_implementation_example/utils/flavor_config.dart';
class HomePage extends StatefulWidget {
final String title;
const HomePage({super.key, required this.title});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You currently running the app using ${FlavorConfig.instance.name} env',
),
Text(
"\nYour current environment base url is \n${FlavorConfig.instance.values.apiBaseUrl}",
textAlign: TextAlign.center,
),
],
),
),
);
}
}
Make sure, after following all the steps above, you have all the required files and your project structure looks like this:
Step 3: Setting up flavor in iOS by configuring the schemes
In this step, you will configure iOS bundle schemes and bundle identifier
- Open your project in Xcode (ios/Runner.xcworkspace).
- In your
Runner
Project, add-production
suffix in the current configuration. To rename the configuration, simply double-click the configuration and make sure it looks like this: - Now, you need to duplicate every configuration for every flavor you have by selecting the
Add (+)
icon and duplicating the configuration. Make sure you duplicate the correct configuration. In order to makeDebug-staging
you need to duplicate it fromDebug-production
notRelease-production
orProfile-production
and it applies to all configurations. After you duplicate all the configuration for every flavor, make sure your configuration looks like this: - Now in your
Runner
Targets, go toBuild Settings
, search forProduct Bundle Identifier
, and edit your bundle identifier by adding.staging
and.dev
suffix to your bundle identifier. We will exclude production because the production environment doesn't need the suffix. - After that, if you wanted to make your application name different between every flavor, you needed to go to
Runner
Targets, then go toBuild Settings
, and findProduct Name
. Change it to your desired application name. - After you change your
Product Name
, to apply the changes to your app name, go to yourInfo.plist
. (Runner
>Runner
>Info
). Update theBundle Display Name
value to$(PRODUCT_NAME)
. - Now, you need to rename your current scheme to production and also create two more schemes (staging and dev). You can rename your current scheme by opening
Product
>Scheme
>Manage Schemes
from the menu And then duplicate yourRunner
scheme. Make sure you name itproduction
with lowercase because it is case-sensitive. After you duplicate your current scheme, you can safely delete it by using theRemove (-)
icon in theleft-bottom
- Now, for the
staging
anddev
you can create a new scheme by selectingProduct
>Scheme
>New Scheme
from the menu. Make sure it targets theRunner
, and is also named with lowercase. - After that, you need to manage your
staging
anddev
schemes so they target the correct configuration. To do that, you need to go toProduct
>Scheme
>Manage Schemes
from the menu. Select thestaging
scheme, and select theBuild Configuration
to correct the configuration you created. For example, inRun
Menu, you need to change theBuild Configuration
toDebug-staging
, and then in theProfile
Menu, you need to change it toProfile-staging
and apply it to all the menus. Do the same with thedev
scheme.
Step 4: Setting up flavor in Android by configuring the bundle
- Inside your Flutter project, navigate to
android/app/build.gradle
. - Create a
flavorDimension
to group your added product flavors.Gradle
doesn’t combine product flavors that share the samedimension
. - Add a
productFlavors
object with the desired flavors along with values fordimension
,resValue
, andapplicationId
orapplicationIdSuffix
.
defaultConfig {
...
}
buildTypes {
release {
...
}
}
// Add these lines of code.
flavorDimensions "app"
productFlavors {
production {
dimension "app"
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
resValue "string", "app_name", "Flavor"
}
staging {
applicationIdSuffix ".staging"
dimension "app"
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
resValue "string", "app_name", "Flavor Staging"
}
dev {
applicationIdSuffix ".dev"
dimension "app"
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
resValue "string", "app_name", "Flavor Dev"
}
}
Then, in your AndroidManifest.xml
update your android:label
to @string/app_name
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vincwestley.flutter_flavor_implementation_example">
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
Step 5: Setting up your IDE launch configurations
5.1: Android Studio with UI
- In your Android Studio
Menu Bar
, selectEdit Configuration
- Update your
main.dart
configuration tomain_prod.dart
, also change theDart entrypoint
to yourmain_prod.dart
file location, and change theBuild flavor
value toproduction
. - For
staging
, duplicate yourmain_prod.dart
configuration. - Update all the configuration to target
staging
values:name
,entrypoint
, andbuild flavor
. - Do the same for the
dev
configuration.
5.2: Android Studio Manually
In the root directory of your project, add a folder called .run
. Inside the .run
folder, create a file named main-prod.dart.run.xml
, main-staging.dart.run.xml
, and main-dev.dart.run.xml
.
main-prod.dart.run.xml
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main-prod.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="buildFlavor" value="production" />
<option name="filePath" value="$PROJECT_DIR$/lib/main_prod.dart" />
<method v="2" />
</configuration>
</component>
main-staging.dart.run.xml
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main-staging.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="buildFlavor" value="staging" />
<option name="filePath" value="$PROJECT_DIR$/lib/main_staging.dart" />
<method v="2" />
</configuration>
</component>
main-dev.dart.run.xml
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main-dev.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="buildFlavor" value="dev" />
<option name="filePath" value="$PROJECT_DIR$/lib/main_dev.dart" />
<method v="2" />
</configuration>
</component>
5.3 Visual Studio Code
In the root directory of your project, add a folder called .vscode
, Inside the .vscode
folder, create a file named launch.json
. In the launch.json
file, add a configuration object for each flavor. Each configuration has a name
, request
, type
, program
, and args
key.
{
"version": "0.2.0",
"configurations": [
{
"name": "prod",
"request": "launch",
"type": "dart",
"program": "lib/main_prod.dart",
"args": ["--flavor", "production", "--target", "lib/main_prod.dart" ]
},
{
"name": "staging",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"args": ["--flavor", "staging", "--target", "lib/main_staging.dart" ]
},
{
"name": "dev",
"request": "launch",
"type": "dart",
"program": "lib/main_dev.dart",
"args": ["--flavor", "dev", "--target", "lib/main_dev.dart" ]
}
],
"compounds": []
}
By following the steps above, you’ve implemented a Flutter flavor that will make it easier for us to do application management with multiple dynamic data points. It will also make it easier for us to do maintenance and provide flexibility as well as efficiency in the development process.
🔥 App Demo
You can check out the full source code here: Github
You will have three different apps for the singular code with different data and configurations.
⭐ Conclusion
Flutter flavor makes mobile application development more simple and seamless, as well as the solution to the problem of efficiency and speed in the complex development of mobile applications that need to implement multiple environments. Implementing Flutter Flavor will not only speed up and simplify the development process, but it will also reduce the risk of errors that will occur if you separate ingredients manually.
Top comments (0)