Flutter Ready to Go (flavors, connectivity and more)

Julio Henrique Bitencourt
Mar 16 · 11 min read

The idea behind this article is to present you guys and girls a project (GitHub) that might be useful as a basic organization architecture with lots of resources that are used on real development in Flutter, taking into consideration a more professional approach during development, mainly to work in a team environment. Therefore, it would take a simple fork and some adaptations to use it in any project. So, let’s just get to the point.

Table of Contents

  1. Flavors
  2. Flavors in Dart
  3. Visually identifying each flavor
  4. Identifying device info with the Banner
  5. Flavors in Android
  6. App icons based on flavor
  7. App name based on flavor
  8. Running flavors with the IDE
  9. Using flavor values
  10. Controlling connection errors

This post was originally posted on blog.juliobitencourt.com and to a better experience is advised to go there, since medium is a little limited.

Flavors:

At companies we usually work as a team, we have a well-defined process lifecycle of building software, where the new features go through the most different stages, as the development team, the test team, the QA team (depending on the company, naming might change), and finally goes to the production. And each one of these stages might need something different, like a URL from an API used for development, another one to QA and consequently another to production.

This is the concept of Android Flavors (or schemes on iOS)! Where we would make available a flavor (a version) of our App that in some situations behaves differently. Imagine the situation where our app has a free and a paid version, each one could be a flavor. But in our case, we’ll take into consideration only the app lifecycle stages.

The Flutter SDK by itself has the build modes, in which we could do something different based only on the current running mode, they are:

  • Debug: It is the only mode we can run on emulators, are known by the ‘debug’ banner shown while running the app. The assertions are enabled, as well as the observatory. It has a bigger final package size, once it is optimized just for development. To run it only takes a flutter run.
  • Profile: It still keeps some debug functionalities, but it can only be executed on physical devices to maintain a real performance. To it, we can use flutter run --profile.
  • Release: It also runs only on physical devices, it is the mode we use to generate the final package to publish at the stores, after all, it’s optimized to execution and final binary size, it can be used with flutter run --release or flutter build.

It turns out that when we want to make the app available to tests, or the QA team, it is interesting for it to be on release mode, so that the performance will be optimized and the same as the final user would experience. So a combination of flavors and build modes would fit perfectly in this case.

Flavors in Dart:

To define our flavors in dart and make it available everywhere in our code, we’ll start with flavor_config.dart:

In Flavor enum we declare the flavors we’ll use, and FlavorValues will be responsible of keeping all the specific values from each flavor, like urls or database names, I do not recommend you to use it to store sensitive data as access tokens, for that you may use something like flutter_secure_storage.

The last one, FlavorConfig is the one we’ll use to configure our flavor. It could be defined as an InheritedWidget, but the idea is to be able to easily access it from anywhere of our app to validate which one is the current flavor, in that case, we would be breaking an abstraction rule by accessing a widget on bloc, or even difficult more our lives having to pass it through parameter to every layer of the app. That’s why I set up it to be a singleton, easily accessible everywhere.

Now for each flavor, we create the main file, used to start up the app

  • main_dev.dart : Our development flavor.
  • main_qa.dart : Our flavor to qa team.
  • main_production.dart: Our flavor to the final product.

In main_qa.dart above we can use the FlavorConfig created previously to define a flavor, a custom color, and some values. The same must be done with main_dev.dartand main_production.dart.

In app.dart we create MyApp that simply calls the HomePage. And this one uses FlavorConfig to print the current flavor. To run our app we must choose the flavor desired:

  • flutter run -t lib/main_qa.dart
  • flutter run -t lib/main_dev.dart
  • flutter run -t lib/main_production.dart

The command above will run our app in debug mode, with -t or --target we define the starter point, the main file.

Visually identifying each flavor:

Wouldn’t be amazing to visually identify which is the current flavor running, without the need of a text printing its name, occupying space on screen? Well, we’ve got a really good example with debug build mode, where a banner is shown on the very top-right corner of our app. Why not use the same concept with our flavors? Let’s get our hands dirty.

Our FlavorBanner uses a Stack to position the banner above a child widget, and in case we’re running on production, obviously, it won’t show a banner. BannerConfighas a label and color, and with the help of a CustomPaint, we can use BannerPainter(the same one used on debug banner) to draw our banner at the very top-left corner of our app.

Enveloping the Scaffold from Homepage with the banner, we’ll get the result:

Identifying device info with the Banner

Wouldn’t it be even more amazing, if we clicked our banner, and it opens a dialog with some info of the device running our app? Of course, it would! With that our test or QA team can gather more precise information in case of any different behavior among the smartphones used. For that, we’ll use a plugin called device_info.

Our class DeviceUtils has 3 utility methods, androidDeviceInfo() and iosDeviceInfo() will basically get the device information based on the platform with the plugin help. And currentBuildMode() will help us identify in which build mode we’re running, so, if the environment has a flag dart.vm.product it means we’re on RELEASE, if we’re able to run and validate an assert(), then, we’re on DEBUG, on the contrary, we’ll be running on PROFILE.

With DeviceInfoDialog we’ll use Platform.isAndroid and Platform.isIOS to validate which one we’re running, based on the result we can use the corresponding method from DeviceUtils to load the device info. It’s worth mentioning that, the info we get from the Android platform, is different from iOS, in the code above I omitted the iOS part for simplicity, but you can see the full source code on github.

Now the setup is done, we can add a GestureDetector on our FlavorBanner to identify a longPress and open our dialog.

And here comes the magic:

FlavorBanner Dialog.

That’s it, with a simple long touch on the banner, we can identify if it is a physical device, the platform, model, manufacturer, OS version, among other stuff. This may not seem so useful, but remember, our app can be tested by a complete different QA team, the way they want it, and in any case, we as devs can only request a print to see de dialog info.

Flavors in android:

Even if the dart flavors are quite handy, it may be interesting to also define platform flavors, this way we could set a personalized icon or app name, based only on the flavor. So let’s change android/app/build.gradle to add the productFlavors:

The dimension name can be anyone, as long as it is the same to all flavors, in dev and qa we can also define an id and version suffix. To run our app now we must use the following commands:

Running each flavor on DEBUG mode:

  • flutter run –flavor qa -t lib/main_qa.dart
  • flutter run –flavor dev -t lib/main_dev.dart
  • flutter run –flavor prod -t lib/main_production.dart

Running each flavor on PROFILE mode:

  • flutter run –profile –flavor qa -t lib/main_qa.dart
  • flutter run –profile –flavor dev -t lib/main_dev.dart
  • flutter run –profile –flavor prod -t lib/main_production.dart

Running each flavor on RELEASE mode:

  • flutter run –release –flavor qa -t lib/main_qa.dart
  • flutter run –release –flavor dev -t lib/main_dev.dart
  • flutter run –release –flavor prod -t lib/main_production.dart

Sometimes between changing flavors is necessary a flutter clean to clean our app build files.

App icons based on flavor:

To dynamically change the app icon is simple, just go to android/app/src and create a directory to each flavor, like {flavor}/res, except for the production one, this will use the default main directory. Inside each one’s res folder, create the mipmaps just like the ones from main/res, now inside each mipmap folder we put the corresponding flavor icon. All icons must have the same names, I recommend to keep the default name provided, ic_launcher. The final structure should look like this:

When running the different flavors, we’ll have:

Different icons to each flavor.

App name based on flavor:

We can use the same logic to naming, inside each res directory we create a valuesfolder with one file named strings.xml inside. To each one of these, we define a string app_name with a custom name.

Now inside android/app/src/main/AndroidManifest.xml, we reference this new string as a parameter to android:label:

With that done, each flavor will have a custom name:

Different names to each flavor

To generate icons of different sizes, you can use a package called flutter_launcher_icons

Running flavors with the IDE:

Even though I still prefer to run the app using the IDE’s terminal, in case you use IntelliJ or Android Studio, it may be interesting to create custom running configurations to each build mode + flavor. For example:

Edit the running configurations and create a new one for Flutter.

Then just give it a name, choose the corresponding main file, the initialization args (for profile and release only), and the flavor defined on build.gradle. With this, we could achieve something like the following:

Using flavor values:

Now we’ll simulate the following scenario:

  • In case we’re running on dev: I want my repository to use a local json, to develop and test my app quickly, without depending on an API return.
  • In case we’re running on qa: I want my repository to make a real request REST, in an API used to testing.
  • In case we’re running on production: I want my repository to make a real REST request, in an API optimized to production.

So let’s create some files

PersonJson is a Singleton that will contain our json to be used locally, in case we’re running on dev. To qa and production, we’ll use the GitHub’s raw URLs from person_qa.json and person_production.json, simulating and API address.

On our Person bean, with the help of json_serializable we can generate a person.g.dart to transform our json into an object.

The BaseApi will help us doing the REST calls, in this class, we get the baseUrldefined to the flavor, and also put a timeout of 5 seconds on each call, throwing an exception if it takes longer. We could add other methods in this class, like a post, put, among others, even private calls with authentication headers.

Our provider will extend from BaseApi, make the API call and deserialize the json into a Person. Notice that if we’re on dev, instead of making a request, we take the local json and simulate a delay of 2 seconds.

Repository is also a singleton, responsible for knowing where to get the data from. In our example we only make an API call, but we could create some cache strategies for example, getting data from a database.

Now it’s time to create a Bloc, to fully understanding it (including the implementation I used), I really recommend reading Didier.

The BlocProvider is a combination of StatefulWidget and InheritedWidget to give us the bloc instance directly from the widget tree.

HomeBloc is our bloc with business logic from HomePage, we make available a stream to be listened to, getting a person, so, in fetchData() we call Repository and add the result into streamController. If any error occurs by now, we’re only printing on the console.

Now we modify MyApp to add a Bloc, as well as the HomePage to print some info on the person returned by request.

With only that we achieved the desired result, our API behavior is changing based on the flavor.

Controlling connection errors:

For those apps that have the necessity of making API requests (or any type of internet connection), it is essential we handle any possible errors on these calls, and of course, have the knowledge of what kind of connection our user is currently using, or even if they are really connected, mainly to make decisions based on this information, ensuring the best experience while using our app, preventing the user from an unexpected error.

To help us with this task, let’s use a plugin from flutter team called connectivity, which will allow us to identify the user’s connectivity status. It’s worth emphasizing that the user may be using a VPN or a hotel WiFi, which does not guarantee internet connection, that’s why we’ll also guard our code against timeouts.

ConnectivityHandler is an abstract class that we’ll use to trigger connection callbacks.

We changed fetchData() from HomeBloc to receive a delegate (a class that implements ConnectivityHandler), and in case of any error during a request, we trigger it trough delegate.onError(). But the thing is, besides identifying possible errors, it would be interesting to be warned when the user changes his connection status, but lucky for us, the plugin already provides a stream for us to listen to these changes.

This time we created the ApplicationBloc, this one will keep a list of _delegates and trigger a delegate.onConnectivityChanged() to them, on each connectivity’s status change. I chose to use a different Bloc because this one can manage more general application rules, like the business logic to login and authentication, for example.

We changed MyApp to include the ApplicationBloc above all the app’s widget tree. In our rout, we’ve registered the homePage to listen to the bloc’s connectivity stream.
Now the HomePage needs to implement ApplicationBloc to be able to manage connectivity errors and status.

Now in case of any error or change of connectivity status, we’ll show a snackBar on screen with the info.

Notification working when changing the connection

If any timeout occurs, this would be the behavior:

Error notification on request’s timeout

Notice by the gif that after the error, if the status is changed, we’ll make a new API request.

That’s it, in this article we’ve seen some aspects of code architecture and organization that for sure, will help our Flutter codes to be more clean and professional. The final package structure will be this one:

Final considerations:

I didn’t include iOS schemes on this article, because at the moment I’m not able to compile to iOS, but as soon as I get my hands on a Mac I’ll update this article. It is on my roadmap to also include on the project a complete authentication flow.

Have you found it useful? Leave some 👏 and a ⭐️ on the Github repo ;)

Flutter Community

Articles and Stories from the Flutter Community

Julio Henrique Bitencourt

Written by

juliobitencourt.com

Flutter Community

Articles and Stories from the Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade