2021-05-16
Building a Production Ready Flutter App - Product Flavor
Product flavors represent different versions of your project that you expect to co-exist on a single device, the Google Play store, or repository. For example, you can configure 'demo' and 'full' product flavors for your app, and each of those flavors can specify different features, device requirements, resources, and application ID's--while sharing common source code and resources. So, product flavors allow you to output different versions of your project by simply changing only the components and settings that are different between them.
In the past two months I have been working with Flutter, my first interaction with Flutter and Dart; I have to say, it was easier than I thought.
Dart is simple, super readable, null-safe, and typed, providing all features that you can find in some of the main languages such as Kotlin, Java and Scala.
Nowadays, time is gold, and platforms such as Flutter and React Native are a way for you to build a single application and delivery to multiple platforms at the same time.
But, we need to remember, with simplicity comes the maintainability, since we have the flexibility to write the code in different ways we can also overcomplicate things, and what I mean with it is how to handle application state, complex UI view, or dependency between different screens.
In this post, I'm going to share some of my learnings, and best practices that have helped me to improve my app.
Custom Environment Support
When you're building an application, sometimes, you need to connect an external service and during your development you don't want to use the wrong API token and affect your production database, or you want to introduce a new A/B testing for a specific feature, but in a dev environment they must be all enabled? So how to deal with it?
Let's see what it would look like with Product Flavor.
Product Flavours are a set of rules/values that can be customised for different development environments or release types.
Step 1: Let's create our contract, what we will be exposing:
File: app_flavor_values.dart
/// Application Flavor
class AppFlavorValues implements FlavorValues {
final Map<String, bool> Function() features;
final Map<String, String> Function() configurations;
@override
final String baseUrl;
final String? anotherUrl;
const AppFlavorValues(
this.baseUrl, {
required this.features,
required this.configurations,
this.anotherUrl,
});
}
In the code above we defined a flavor that we will be able to define a set of features and configurations but now we need to load the contact of this contract.
Step 2: Retrieving/Loading Flavor values
Here's how the configuration class looks like:
File: configurations.dart
abstract class Configurations {
static ValueNotifier<Map<String, String>> _configurations = ValueNotifier({});
static const dev = <String, String>{
'config.api_token': 'sandbox_1',
};
static const qa = <String, String>{
'config.api_token': 'sandbox_2',
};
static const prod = <String, String>{
'config.api_token': 'real_environment_1',
};
static Map<String, String> get devRemote {
_remoteConfig(dev);
return _configurations.value;
}
static Map<String, String> get qaRemote {
_remoteConfig(qa);
return _configurations.value;
}
static Map<String, String> get prodRemote {
_remoteConfig(prod);
return _configurations.value;
}
static Future<void> _remoteConfig(Map<String, String> env) async {
_configurations = ValueNotifier(env);
// TODO: Here you could use the Firebase Remote config to retrieve your settings.
}
}
File: features.dart
abstract class Features {
static ValueNotifier<Map<String, bool>> _features = ValueNotifier({});
static const dev = <String, bool>{
'feature.feature_1': true,
'feature.feature_2': true,
};
static const qa = <String, bool>{
'feature.feature_1': false,
'feature.feature_2': true,
};
static const prod = <String, bool>{
'feature.feature_1': true,
'feature.feature_2': false,
};
static Map<String, bool> get devRemote {
_remoteConfig(dev);
return _features.value;
}
static Map<String, bool> get qaRemote {
_remoteConfig(qa);
return _features.value;
}
static Map<String, bool> get prodRemote {
_remoteConfig(prod);
return _features.value;
}
static Future<void> _remoteConfig(Map<String, bool> env) async {
_features = ValueNotifier(env);
// TODO: Here you could use the Firebase Remote config to retrieve your features.
}
}
File: constants.dart
abstract class Constants {
static AppFlavorValues get flavor => FlavorConfig.values();
static final flavorDev = AppFlavorValues(
localhost,
features: () => Features.devRemote,
configurations: () => Configurations.devRemote,
);
static final flavorQa = AppFlavorValues(
localhost,
features: () => Features.qaRemote,
configurations: () => Configurations.qaRemote,
);
static final flavorProd = AppFlavorValues(
localhost,
features: () => Features.prodRemote,
configurations: () => Configurations.prodRemote,
);
static const localhost = 'http://10.0.2.2:1337';
}
AppFlavorValues get flavor => Constants.flavor;
Now when launching your application you're able to define which set of rules you should use:
/// main_dev.dart
void main() {
/// For more details InheritedWidget class: https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
setApplicationSettings(Constants.flavorDev);
}
/// main_prod.dart
void main() {
/// For more details InheritedWidget class: https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
setApplicationSettings(Constants.flavorProd);
}
The example above allows you to introduce different configuration to your application allowing you show and do what is needed.
To keep it short I'm going to break down this post in a few parts, now that we introduced Product Flavor concept to our app, the next step is responsiveness, how to ensure your app is supporting devices with different sizes.
References:
https://developer.android.com/studio/build/build-variants#product-flavors