Add Flutter to your Production Android App

TruongSinh Tran-Nguyen
HackerNoon.com
9 min readApr 16, 2019

--

There are several instructions on how to add Flutter to your existing Andriod App, including the official wiki, but none of it has been done on a real Production app, thus when we were doing so, there were more challenges than we had anticipated, such as code organizing with submodules (so that we can reuse for iOS app later), support for AndroidX and product flavors, passing data back-and-forth similar to what you do with intent#putExtra and startActivityForResult, support plugins on which this module depends, support different routes while caching. We have extracted our experiences into step-by-step instruction with code example.

0. Prepare Flutter and the Host App

This tutorial is done on Flutter version 1.5.2. As of this moment, 1.5.x is already on dev channel (no need to use master channel). As Flutter releases beta monthly, it is expected that 1.5.x is going to be on beta early May.

UPDATE: By the time you read this, 1.5.4-hotfix-2 has been named stable.

This is your current production Android app. For the purpose of this article, a new app is created to mimic your production app, but in reality, it should be your real one.

Notice that we choose to “Use AndroidX artifacts”, this reflected some of the real challenges we encountered while doing this, but you don’t need to worry, as by default, Flutter’s “AddToApp” works well with AppCompat.

Also, to make this sample app more realistic to a production one, make sure you have build flavors

Sample host app code at this step is available on my Github.

1. Create the `flutter module`

Anywhere in your local system (including inside your host app), run the command

  • -t module is NEEDED so that this package is a Flutter module (and not and app, pakcage or plugin).
  • -i swift is OPTIONAL, but currently does not seem to take effect. It is just for this specific module, any iOS related code will be in Swift (it is my preference, if you ignore this option, iOS related code, as of now, will be in Objective C). Same for -a kotlin with Android and Kotlin (for this specific module only, and has NOTHING to do with whether your host app is in Java, Kotlin, or the mixture both).
  • --org [your org prefix] is OPTIONAL, but RECOMMENDED, and should be straight forward if you have the background in Android and/or iOS app development.
  • flutter_embedded is NEEDED, and is both the name comes after [your org prefix] in the bundle name, and the folder on this template code is generated.

Commit and push the code on a git repo (such as your company’s private self-hosted git repo, or Github), and remember the git URL to be used in the following step. Even though it is also possible to commit this directory directly under your host app code, it is recommended to have it as a git submodule so that we can share this flutter module with an iOS app later as well.

Then to support AndroidX, you need to make the following workaround: include most files under the directory .android, and migrate Java files to AndroidX

Embedding module code at this step is available on my Github.

2. Make this `flutter module` a `git submodule` of your host app

Inside your host app, run the command to add that flutter module to be a git submodule :

flutter_embedding is a recommended name for the directory name of this submodule to avoid namespace clash with flutter itself, while self-explanatory enough. Notice that if you are working with iOS as well at this point, you will have to partially re-generate the module as .ios directory is ignored.

If you inspect your directory underflutter_embedded , you will see a handful of files, but with git diff, you will only see the git URL of the submodule, and the current hash commit this submodule is pointing to.

Host app’s code at this step is available on my Github.

3. Call FlutterEmbeddingActivity from native app activity

To be able to call FlutterEmbeddingActivity from native app activity, you will need to register flutter module with host app Gradle scripts

In the directory where you store all of your activities, create a new activity FlutterEmbeddingActivity.

This activity has a static method init to initialize and create a cached flutter engine for better performance, as well as being used later for flutter plugin registration. Note that this activity must extend io.flutter.embedding.android.FlutterActivity, not io.flutter.app.FlutterActivity, otherwise, we cannot override the IntentBuilder . This note is also important for later steps when we register flutter plugin.

Now that we have the new activity, we can register it in AndroidManifest.xml and call it from other activities

Whether/Where to call FlutterEmbeddingActivity.init(this), is a tradeoff decision. You have 3 options

  • Call it in the onCreate of your entry activity. Pros: better performance when transitioning from native activity to Flutter embedding activity (the first time), cons: worse performance at app launch.
  • Call it in the onCreate of the activities the immediately lead to Flutter embedding activity. Pros: better performance when transitioning from native activity to Flutter embedding activity (the first time), cons: worse performance when transitioning from a native app activity to another native app activity in which init is called (the first time).
  • Do not call FlutterEmbeddingActivity.init(this) at all. Pros: better performance at app launch or among native app activities, cons: worse performance when transitioning from native activity to Flutter embedding activity (the first time)

What you cannot do, however, is to call FlutterEmbeddingActivity.init(this) in another thread, as it will crash with

If you start debugging now, you will see that the app crashes immediately after launching with the following log

It is a known bug, and the workaround is to add the following lines to app/build.gradle

The above workaround will fix issues for all product flavors and build types (such as Debug, Profile, Release). Now start debugging your app again, and tap on the FabButton, you should see these lines in the log (indicating that we are indeed using cached Flutter engine)

The first time the app transitioning from native activity to Flutter embedding activity, you will see a small lag, but subsequent transitions are much smoother.

Also, the internal state of Flutter embedding activity is persistent, meaning that you tap the “back button” to go back to native app activity, and then click on FabButton, you will see counter number remains the same (and not reset to 0).

At this point, you can also attach Android Studio to the Flutter activity to debug your Dart code, do hot reload and hot restart.

Host app’s code at this step is available on my Github.

4. Open different Flutter screens

When your app grows on the Flutter side, you will need the embedding activity to display more than one screen/route, and in this example, we will have the “counter” screen, and another “hello world” screen.

According to the current official wiki, you can achieve this with either/both createBuilder().initialRoute and createBuilder().dartEntrypoint. However, we found out that once we use the cached flutter engine, those methods are no longer working. What we have to do now is to convert the top-most widget (MyApp) into a stateful widget, and listen to the platform channel (event channel) and build the widget accordingly.

In flutter_embedding/lib/main.dart, convert the top-most widget into a stateful widget, listen to the event channel and set state accordingly. Notice that at line 32, we also determine if the route is init, we will return to native Android activity immediately. This is done for caching purpose only.

In FlutterEmbeddingActivity, we need to send a message via platform channel to Dart’s side whenever this activity is onCreate.

Again for caching purpose, if the init is called NOT from FlutterEmbeddingActivity, we will present the Flutter View (which will be poped immediately anyway from Dart’s side)

Finally, in any other activity, create a Flutter Intent with initialRoute, and start that activity.

You will see that tapping different buttons will lead you to different Flutter screen. Furthermore, the state of “counter” is preserved if you will go back and forth to that Flutter screen only, but is reset (to 0) if you visit another screen, and go back to “counter” screen.

Code at this step is available on my Github.

5. Intent and ReturnIntent extras

Having background in Android development, it is no doubt that you have at least once used intent#putExtra to pass data to the next activity, and startActivityForResult andreturnIntent#putExtra to pass data back to the previous activity. It is possible to do that with Flutter activity/screen as well, again with the help of platform channel (this time, both event channel and method channel). When doing this, you should be aware of what kind of data structures that are eligible to be passed across the bridge between Dart’s side and Android’s side.

In Dart side, we get extra data from the same event

In FlutterEmbeddingActivity.kt, we convert data receive from Dart’s side to Intent’s extra, and use setResult and finish to pass data back to previous activity

In the calling activity, we replace startActivity with startActivityForResult, and listen to the result with onActivityResult

Host app’s code at this step is available on my Github.

6. Using Flutter plugin

Growing your production app, it is again no doubt that at some point you will need your Flutter activity to access the platform’s specific data, such as camera, audio or sensor. You can either implement all the platform channels to handle these data by yourself, or you can just DRY and use available Flutter plugins available on https://pub.dartlang.org/flutter. It is, of course, recommended that you choose the latter approach so that later when the Flutter activity outgrows the native app and become the app itself, you don’t need to maintain Kotlin/Java codebase.

Let’s take an example of path_provider package, let’s say that we want to getApplicationDocumentsDirectory, and pass the data back using ReturnIntent extras as implemented in the previous step. We define the dependency in pubspec.yml, and call the method to get the data (in String format)

However, if you run and test the app now, you will get the following error

It is because we have not registered path_provider plugin’s platform channel on Android’s side. To do so, usually in the activity’s onCreate we call GeneratedPluginRegistrant.registerWith(this). However, we painfully found out that we cannot use that strategy with our current FlutterEmbeddingActivity. The reason is that our activity extends from io.flutter.embedding.android.FlutterActivity, not io.flutter.app.FlutterActivity. Further looking into GeneratedPluginRegistrant, it seems that this code needs only 2 things, the reference to the current activity, and the messenger, which in our case is cachedFlutterEngine.dartExecutor. Having learned that, we can have a workaround, albeit a little bit verbose.

This time, test your app, we can see that it works as expected

Host app’s code at this step is available on my Github.

Conclusion

Even though Flutter’s Add2App is in preview, it can already be used on a production app, with a little bit tweak to support some of the common use cases, such as

  • AndroidX
  • Product flavors
  • Different routes while using cached Flutter engine
  • Passing data back-and-forth similar to what you do with intent#putExtra and startActivityForResult
  • Flutter’s plugins on which this module depends

The FlutterEmbeddingActivity’s code sample in this article is designed such that some of its code may eventually end up in the Flutter Engine’s code itself, reducing boilerplate code. Meanwhile, if you want to get started with Flutter and your production app, it is recommended to embed Flutter to your current app, rather than rebuild the whole Flutter app, reducing the risk and feedback loop.

This article is free, and so is your clap👏. Did you know you can press the clap👏 button 50 times?

--

--

TruongSinh Tran-Nguyen
HackerNoon.com

Google Developer Expert (GDE), Engineering Director@ Inspectorio, Nordic Startup Award — People’s choice CTO 2016, Agile Evangelist — PSM