Add Flutter to your Production Android App
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
flutter create -t module -i swift -a kotlin --org [your org prefix] flutter_embedding
-t module
is NEEDED so that this package is a Fluttermodule
(and not andapp
,pakcage
orplugin
).-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 specificmodule
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
:
git submodule add [your git URL of flutter embedding module in step 1] flutter_embedding
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 whichinit
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
java.lang.IllegalStateException: startInitialization must be called on the main thread
If you start debugging now, you will see that the app crashes immediately after launching with the following log
E/flutter: [ERROR:flutter/runtime/dart_vm_data.cc(19)] VM snapshot invalid and could not be inferred from settings.
[ERROR:flutter/runtime/dart_vm.cc(237)] Could not setup VM data to bootstrap the VM from.
[ERROR:flutter/runtime/dart_vm_lifecycle.cc(81)] Could not create Dart VM instance.
A/flutter: [FATAL:flutter/shell/common/shell.cc(218)] Check failed: vm. Must be able to initialize the VM.
A/libc: Fatal signal 6 (SIGABRT), code -6 in tid 12928 (utter_host.free)
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)
D/FlutterFragment: Deferring to attached Activity to provide a FlutterEngine.
D/FlutterView: Initializing FlutterView
Internally creating a FlutterSurfaceView.
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 pop
ed 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
E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: MissingPluginException(No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider)
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
D/Flutter example: requestCode: 42, resultCode: 24, data {returnArg1=/data/user/0/pro.truongsinh.flutter.android_flutter_host.free/app_flutter, returnArg2=2}
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
andstartActivityForResult
- 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?