Use Flutter in existing Android Apps
You might know the pain: An App should get a new Feature which is UI-wise heavily demanding, but Androids build-in capabilities are limited or rather complex to use.
So what can be done here? Invest a lot of time building the UI and possibly spend way more time later to maintain the implementation?
Quickly it comes to mind if it would be possible to use Flutter as rescue. It has sophisticated powers especially when it comes to UI. A really nice addition would be that an iOS App also could use that UI component without having to write it twice.
Disclaimer: I did not have a look on the iOS part, but as Flutter also compiles for iOS it should be possible without much effort.
For my journey I want to use the PageTransformer Flutter Example and integrate it as a library into a native Android App. For more easy reading I will name the App CustomerApp
and the Example PageTransformerLib
from now on.
My goal is to showcase:
- Starting the
PageTransformerLib
as an Activity in theCustomerApp
- Display the
PageTransformerLib
as a View embedded into a native Android Layout in theCustomerApp
- Modify the
PageTransformerLib
so we can embed it into a native Android Layout in theCustomerApp
and replace the shown Items
Project Setup
Most of the Apps I develop at work are already some days old. Their repositories have their own structure and can not be changed without much effort and risk. Beside that we usually have distinct GIT repositories for Android and iOS.
This plays against the Flutter tooling which requires its very own repository structure:
- android
- ios
- lib
- test
so we have to split the CustomerApp
from the PageTransformerLib
to satisfy both needs. As a starting point I used Add-Flutter-to-existing-Apps as guideline to migrate the default Android Flutter App template to an Android Flutter Library project.
Since the guide was written, 731ed1b got already merged and is included in the current latest Flutter beta release. Nice, less manual work!
The PagerTransformerLib
build.gradle
were adjusted quickly to build a library without including all the properties which are only needed for an App. In addition to the guide I renamed the MainActivity
to PageTransformerActivity
to not mix it up later with the CustomerApp
MainActivity
.
So far the preparations of the PageTransformerLib
. To keep the repositories separate I added the PageTransformerLib
as Git submodule to the CustomerApp
.
In future versions of Flutter you can create a library project directly by a Flutter module template. It is already included in Flutter’s
master
branch but not yet available in the Beta. So I have not used it while writing this post.
Visit https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps for more information.
Starting as an Activity
Including the PageTransformerLib
in the CustomerApp
is the typical, rather simple process.
First, we have to reference the PageTransformerLib
Gradle Project as dependency in the settings.gradle
of theCustomerApp
:
include 'page-transformer'
project(":page-transformer").projectDir = file("page-transformer/android/app")
And add it as a normal dependency to the build.gradle
in the CustomerApp:
implementation project(':page-transformer')
Now, it is a piece-of-cake to start the PageTransformerActivity
anywhere from the CustomerApp
by the usual
val intent = Intent(this, PageTransformerActivity::class.java)
startActivity(intent)
When deploying the
CustomerApp
to the emulator I ran into the installation errorINSTALL_FAILED_NO_MATCHING_ABIS
. Some poking around led the to flutter.gradle#137 which explicitly states that Flutter only includes its x86 native libraries into builds of type debug. I worked around the issue by changing flutter.gradle#206 and flutter.gradle#299 so that it include the x86 native libs for all builds.It should not cause issues if your release build later on does not ship x86. All x86 devices I know are shipped with an arm -> x86 emulation.
Embed as View
Embedding a Flutter Widget/App as a View can be achieved by using FlutterView
and starting the right Dart entry point on it. Also we have to initialize the Flutter Runtime at the right point.
As our CustomerApp
has no direct dependency to the flutter.jar
it is not possible to use FlutterView
directly without breaking the separation to the PageTransformerLib
. So I decided to wrap the FlutterView
in a custom PageTransformerView
.
The flutter_view Example sets an FlutterApplication
in the Manifest which takes care to start the initialization of Flutter by calling FlutterMain.startInitialization(appContext)
. With the separation of the CustomerApp
from the Flutter lib, again we can not use the FlutterApplication
class directly. Beside that, the CustomerApp
might have already an custom Application
class set.
My solution was to simply wrap the call in a FlutterHelper
object in the PageTransformerLib
and call it from the CustomerApp
s onCreate()
method. Such helper object becomes also handy as we also have to call FlutterMain.ensureInitializationComplete(appContext, args)
before running some Flutter in the FlutterView
. In my example it gets called in the Activity.onCreate()
.
The mentioned
args
are arguments which might have to be forwarded to the Flutter runtime. You can find some examples in the initialization of the flutter_view_ Example.
After all, we can implement a method in the PageTransformerView
which invokes the Flutter main()
method in the FlutterView
by
private val flutterView = findViewById<FlutterView>(R.id.flutterView)start() {
val bundlePath = FlutterMain.findAppBundlePath(appContext)
flutterView.runFromBundle(bundlePath, null);
}
Putting everything together:
- Add our
PageTransformerView
to an Activity layout - Call
FlutterHelper.ensureInit(appContext, args)
inActivity.onCreate()
- Call
PageTransformerView.start()
in the Activity
Caution!
FlutterView
has to be bound to the Android Lifecycle so it can clean up some resources and manage some internal callbacks. Make sure you callFlutterView.onPostResume()
andFlutterView.onPause()
in the right Lifecycle methods.
Setting the Items
Flutter provides an Android <-> Flutter communication by sending messages through aMethodChannel
or a MessageChannel
. All messages send though these channels will pass the Java-Native barrier, so only primitive data types are supported. For our mission this means we have to serialize the items before sending them to Flutter.
Again, to leave the flutter.jar
dependency encapsulated in the PageTransformerLib
we implement the serialization in PageTransformerView.setItems()
.
setItems(items: List<IntroItem>) {
val jsonList = items.map {
mapOf(
"title" to introItem.title,
"category" to introItem.category,
"imageUrl" to introItem.imageUrl
)
}
mItemChannel.send(jsonList);
}
We are using a
Map<String, Any>
here as the Flutter build-in JSON serializing does not support custom objects by default.
On the Flutter side we have to listen for those item messages and update the Flutter App State with the received items.
_channel.setMessageHandler((items) async {
var introItems = new List<IntroItem>();
for (var value in items) {
introItems.add(
new IntroItem(
title: value["title"],
category: value["category"],
imageUrl: value["imageUrl"])
);
}
setState(() {
items = introItems;
});
});
And we are already done with providing an option to update the items from the native Android App at the Flutter Widget.
Tl;Dr
Possible:
- a whole Feature written in Flutter as new Activity
- Including a Single Flutter Widget or Subfeature in Flutter as a View in an native Android App
Drawbacks:
- Testing on Emulator is wonky up-to impossible without adjusting
flutter.gradle
or explicitly using debug mode - The Flutter Project configuration has to be manually setup/adjusted to compile to a Library instead of an App (will get fixed soon)
- Communication Android <-> Flutter requires quite some code and ceremony
- Starting the Flutter Runtime takes some time which you should hide from the user
Conclusion:
It is a nice option to write a new feature in Flutter and than embed it into the existing App. Especially the UI features like Animations and Material Design support are amazing. But be aware that you have only two options:
- Decouple the new Feature from the Rest of the App so that it might run as a standalone App
- Invest quite some time and coding effort to establish a connection between the Android and Flutter App part.
So every use case have to be evaluated if it is really worth the time and effort. With including Flutter you also got one more tooling you have to maintain and keep up-to-date. This should also be in mind that this will increase the App complexity and maintenance costs.