Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
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, 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 the Host App
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 Flutter module (and not and app, pakcage or plugin).
- -i swift is OPTIONAL. 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 :
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 app/build.gradle, line 21–23 is optional for AndroidX support.
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
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 run ./gradlew :flutter:copyFlutterAssetsDebug manually whenever you clean the project or make any changes in your flutter_embedding. 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
Gist here
Host app’s 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.
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 register path_provider package’s platform channels in Android’s side. To do so, usually in the activity’s onCreate we call GeneratedPluginRegistrant.registerWith(this). However, we painfully find out that we cannot use that strategy with our current FlutterEmbeddingActivity, we cannot do that, because 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
Gist here
Now
Gist here
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 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.
How to Add Flutter to your Production Android App was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.