Import multiple Flutter modules in a native app
Hello guys,
I'm Matheus Romão, software engineer at ArcTouch. Here, we are going with Flutter as the technology choice for an important project but recently hit a major roadblock. The principal difficulty relates to using different Flutter modules on different projects. Further explanation and proposal can be found on this issue.
TL;DR: We would like to import different modules in the same project, so we could reuse them in different projects. Today if we try that, we won't pass from Project Sync on Android or pod install on iOS.
Use case
Following the add-to-app guide we can import a Flutter module to an existing native app. However, there's no way to import multiple modules and access each module unitarily.
For example, if we have the following structure:
- flutter-modules
- login
- register
- (...) other modules/features
- android-project
- ios-project
- (...) other projects
It would be nice if there were a way to import login and register modules depending on the needs of each project, so we could reuse them.
Android
Following the "depend on the module's source code" method, I was able to "import" both modules (login and register, in the example) leaving one of the include_flutter.groovy script as it is and changing the other, renaming :flutter to :register. I'm no specialist in gradle/groovy, but I'm pretty sure that was a workaround to make the script work in project sync phase because if you change :flutter to another name in both scripts, the project sync won't work, it will raise an exception:
> Failed to notify project evaluation listener.
> assert flutterProject != null
| |
null false
[...]
Anyway, the project sync works if you leave one script as it is and change the other. However, if you try to run the app, even without using Flutter inside of it, the build fails with the following exception:
Task :app:mergeDebugNativeLibs FAILED
More than one file was found with OS independent path 'lib/x86/libflutter.so'
Which makes sense for me as I'm trying to import Flutter twice.
If I try to import the modules following the "Depend on the Android Archive (AAR)" method, the project sync works. But when you try to run the app the build fails with the following message:
Execution failed for task ':app:checkDebugDuplicateClasses'.
> 1 exception was raised by workers:
java.lang.RuntimeException: Duplicate class io.flutter.BuildConfig found in modules flutter.jar (com.arctouch.login:flutter_debug:1.0) and flutter.jar (com.arctouch.register:flutter_debug:1.0)
[...]
Same problem with duplicates.
iOS
I wasn't able to import the modules using a Podfile. First, install_all_flutter_pods accepts only one module and if we try to call that twice in the same target, we get an error:
Invalid `Podfile` file: Script phase with name `Run Flutter Build Script` name already present for target `iOS Native`.
But I went to podhelper.rb to check the reference and found install_flutter_application_pod, so I tried to run the following Podfile:
login = '../flutter_modules/login'
register = '../flutter_modules/register'
load File.join(login, '.ios', 'Flutter', 'podhelper.rb')
load File.join(register, '.ios', 'Flutter', 'podhelper.rb')
target 'iOS Native' do
install_flutter_engine_pod
install_flutter_application_pod(login)
install_flutter_application_pod(register)
end
But I still got the same error "script phase already present for target".
Workaround
At last, discussing all of that with my colleagues, they suggested trying to import all the modules in an umbrella-like project and then import in the native app. So, I tried to import both modules as packages of a third module, here called umbrella:
umbrella/pubspec.yaml:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
login:
path: ../flutter_modules/login
register:
path: ../flutter_modules/register
That works, but you have to create the routes in the umbrella module to map the login and register widgets.
Well, clearly this is not desirable and it feels pretty hacky. In addition, it adds one more layer to the import process and depending on the method you use, you'll have to repeat some steps as the need for modules change.
Proposal
We should be able to import each module and access its features individually.
For Android, it would be great if we could import Flutter engine and then the modules:
implementation project(:flutter)
implementation project(:login)
implementation project(:register)
After that, we could specify which module and route we want to use in the Flutter view/fragment/activity (facade or Android embedding). Also, the same idea applies to ".aar" method.
For iOS, we could do something like the Podfile above, calling install_flutter_application_pod for each module we want to import or even use varags to pass an array of modules on install_all_flutter_pods.
Thank you guys for taking your time to read this (sorry for the long issue)!
@xster
Thanks for describing in detail. This concept is on our mind but likely not something we'll support in an MVP this year. The main work here is to split our deliverables from being 2 pieces (libflutter.so + libapp.so or Flutter.framework + App.framework) into 3 pieces:
1- the Flutter C++ engine with small bits in Java/Objective-C (1) 2- the Flutter Dart framework AOTs (1) 3- your Flutter module AOT (n)
and a bunch of tooling dealing with mismatches.
In the meantime, you'll likely need to create a glue project to bring in all your Flutter modules together in CI or manually before importing like you suggested.
Copying in from #40343
From @Vanethos:
Objective
Create a native library for iOS and Android that has Flutter bundled in so that native developers can use this library without the need of installing flutter
Steps taken
- Create Android Project
- Create Android library
- Create Flutter Module
- Add necessary code to Android Library
- Try to build project
This gives the following outcome:
Could not find com.vanethos.add2appflutter:flutter_debug:1.0.
Searched in the following locations:
- https://dl.google.com/dl/android/maven2/com/vanethos/add2appflutter/flutter_debug/1.0/flutter_debug-1.0.pom
- https://dl.google.com/dl/android/maven2/com/vanethos/add2appflutter/flutter_debug/1.0/flutter_debug-1.0.aar
- https://jcenter.bintray.com/com/vanethos/add2appflutter/flutter_debug/1.0/flutter_debug-1.0.pom
- https://jcenter.bintray.com/com/vanethos/add2appflutter/flutter_debug/1.0/flutter_debug-1.0.aar
Required by:
project :app > project :flutter_embbebed
The instruction followed were the ones in the following link: adding flutter aar file to an android app
What is noticed is that if the same steps are used to add flutter to an android host, as is detailed in the guide, then the code can compile and it all works out in an emulator.
I've also created a repository with a minimal example to reproduce the issue: https://github.com/Vanethos/modulated-add-to-app
flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.9.1+hotfix.2, on Mac OS X 10.14.5 18F203, locale en-PT)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 10.2.1)
[✓] Android Studio (version 3.4)
[✓] VS Code (version 1.38.0)
[✓] Connected device (3 available)
• No issues found!
As requested, I'll tag @blasten in this issue
@xster thank you for merging the issue
As I understand you said that this will not have any new updates during this year?
Right. Unless we see signs of this being a more common pattern, there are more pressing issues we're currently focused on this year.
Well, unfortunately, that means that I'll have to port my codebase to native :(
Can you accomplish the same thing by creating and re-using Flutter plugins, and importing them into your existing app with a single module?
@jmagman the problem is that we are doing this to create a native library to be used in Android and iOS.
Our clients don't have flutter installed in their machines and some of them will not be able to. So we need to be able to compile a sort of .aar and pod that has native code, flutter code and the flutter engine
Since we need this to work before the end of the year, unfortunately, I'm already porting our codebase to swift and kotlin (3 code bases to manage,yay!...)
@Vanethos The aar work is done, instructions here: https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps#1-depend-on-the-android-archive-aar The iOS framework work is being tracked here: https://github.com/flutter/flutter/issues/36968
@arctouch-matheusromao Could you implement the login and register modules instead with two different plugins? Then you would only need one module per app, and the plugins could be reused between them?
It looks like this issue may need to have the errors you documented split out into several trackable issues.
Hey @jmagman,
Those modules were here just for the sake of the explanation. Our work environment revolves around various products, some new and other already existent. We would like to be able to share different modules across different projects. Some of those modules could be plugins, but unfortunately, others can't, as they would have UI flows. Actually, we do have a use-case that we could implement plugins and consume that inside one module. But, that's just one module.
Just to be more clear, let's say we have a restaurant and we already have an Android app for our waiters to handle the orders. But our business is growing and we are already midway through the iOS native app development for our users to pay. Finally, we do have plans to build another app to handle all the back-office operations.
So, for our restaurant we have three projects:
- Waiters: existent app in Android
- Users: only iOS (plans to build an Android app in the future)
- Managers: Flutter app
At this point, we already have designs for our brand and we want to preserve that across the apps. So, there are a lot of features that could be shared between those projects. For example, we have login and register that could be reused on the user app (both Android and iOS), but for the manager app we don't need the register module, as we already have the registered users and we don't want any external users accessing this app. Another example would be a news feed. If we ever want to share any news on our platform, that could be made in a module and reused in all apps.
I hope this example helps to clarify why some business may choose to use Flutter. This would be an incredible opportunity to get work done faster than in any other framework and help a lot of businesses.
@jmagman but, as I have discussed in my issue, my problem is not adding the aar to an app but it is a problem adding an aar to a module.
Unfortunately the native implementation has already started.. :'(
@xster and @matthew-carroll, do you guys know if it's possible to send messages through a Platform Channel between a Flutter module and a Flutter app?
In our context here (explained in this issue), I'm exposing the umbrella module to an iOS app, which consumes all the events from our modules using a channel. That is working nicely. Now we need to import those modules inside a Flutter app. I fell like I don't need to import the umbrella and just import directly the modules that I need.
In the example above, say I want to import the login module inside my Flutter app, but the login module already has an implemented channel, which sends user events. How can I listen to that inside the Flutter app?
I thought I could just do that in the Flutter app:
final channel = MethodChannel(channelName);
channel.setMethodCallHandler(_handler)
Where channelName is the same for the login module.
However, it doesn't work. Here's the output:
I/flutter ( 6503): Overflow on channel: flutter/lifecycle. Messages on this channel are being discarded in FIFO fashion. The engine may not be running or you need to adjust the buffer size if of the channel.
E/flutter ( 6503): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: MissingPluginException(No implementation found for method navigateTo on channel [...]
Is there a way that I can use the same binaryMessenger for the app and the module? Or it doesn't matter? Do we have a registrar or something similar to a plugin inside the application? So I can pass the binaryMessenger to the channel creation.
I know I can just pass a handler method to the module, but that would also mean changes in the implementation inside the module. Right now we are trying to avoid this path.
Thanks!
It sounds like the issue with the BinaryMessenger is more about having multiple isolates. You will have one BinaryMessenger per isolate, and they can't be the same.
I'm curious, if you have componentized capabilities that involve Dart and platform behavior, why not turn those into local plugins and then consume those plugins in your Flutter app?
I'm curious, if you have componentized capabilities that involve Dart and platform behavior, why not turn those into local plugins and then consume those plugins in your Flutter app?
Yeah I was curious, too. I don't exactly see why this won't work for the originator. From my comment above:
@arctouch-matheusromao Could you implement the
loginandregistermodules instead with two different plugins? Then you would only need one module per app, and the plugins could be reused between them?
And the response:
Some of those modules could be plugins, but unfortunately, others can't, as they would have UI flows.
Thanks, @matthew-carroll! I went with a hybrid solution, having a package responsible to use channels for the bridge between modules and existent native apps and a Function for the Flutter app.
About your question, our biggest problem is that each componentized capability has UI flows in Flutter and we need to integrate that in different native apps, that's why we don't use plugins over modules.
@jmagman, is it possible to have UI (TextField, Buttons, etc) in plugins? If we can have that, we can use multiple plugins inside one module to be imported by a native app. But this doesn't change the fact that we need an umbrella/glue project to import them, as @xster mentioned above.
What I meant with "Some of those modules could be plugins" is that we can separate things like the business logic for the login and register, handling authentication and all of that (following the example). However, that's not a sufficient reason to import Flutter inside a native app. If we still need to create the UI in the native project, why import Flutter in the first place? We would like to import Flutter and use it like a "black box" sending inputs to modules, so they can handle everything inside of it (based on its purpose) and when the flow is done, we can come back to the native app with some output of the process.
Perhaps @jmagman or @xster will be able to provide the solution, but I'm afraid I'm getting a bit lost between umbrella project and module projects and native projects. Would it be possible for you to post a simple diagram that shows what you have now, and then perhaps another diagram of where you're trying to get to? If not, that's fine, but I'm not sure I'll be able to follow the goal.
Hi @arctouch-matheusromao I have the same issue with you.
That works, but you have to create the routes in the umbrella module to map the login and register widgets.
I don't how to create routes in the umbrella module when call from native. Can you share solution? Thanks,
Hey @ngminhduong, follow the solution we've followed:
UmbrellaModule:
void main() => runApp(UmbrellaApp(route: window.defaultRouteName));
class UmbrellaApp extends StatelessWidget {
final String route;
UmbrellaApp(this.route);
@override
Widget build(BuildContext context) {
switch (route) {
// UmbrellaModule is class holding static strings.
case UmbrellaModules.login: // UmbrellaModules.login = 'login_module'
return LoginModule();
case UmbrellaModules.register:
return RegisterModule();
default:
return ErrorRoute();
}
}
}
iOS-native app:
let flutterViewController = UmbrellaViewController()
flutterViewController.setInitialRoute("oobe_module")
self.present(flutterViewController, animated: false, completion: nil)
Where:
class UmbrellaViewController: FlutterViewController
Hey @arctouch-matheusromao It work for me But I have another issue. Splash screen always show before Flutter viewController appear. Do you know what the problem? Do you have any solution for this. Thanks,
Hi @arctouch-matheusromao, @matthew-carroll When I try to create iOS ViewController from Flutter by this way:
let flutterViewController = UmbrellaViewController()
flutterViewController.setInitialRoute("oobe_module")
self.present(flutterViewController, animated: false, completion: nil)
Splash screen always shows before Flutter viewController appears.
Issue not happened if we init view controller by this way:
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterViewController = UmbrellaViewController(engine: flutterEngine, nibName: nil, bundle: nil)
self.present(flutterViewController, animated: false, completion: nil)
But setInitialRoute not work.
Do you have any solution for this issue?
Thanks,
@arctouch-matheusromao I'm having an issue with the Umbrella work around. My assets (images) are not loading in my imported modules. Everything else is working just seems that the assets from their respective pubspec.yaml files are not being loaded. Wondering if you've had a run in with this?
Hey @mikhail-karan, you have to be careful with the package property of Image.asset. You will have to set the package of each module, otherwise umbrella won't find your assets.
Hey @arctouch-matheusromao I did that now and it seemed to solve my issue with the umbrella but now when I launch my module on it's own it throws errors and won't display the images because they have the package property. Is there a way to get around this or do I essentially still have to maintain two separate code bases, one with the package property on the images and one without? Thanks!
any updates on this from flutter team ?
Hey. Any updates on this issue? We are stuck big-time with this. We are not able to do reasonable modularity.
Updates from Flutter team will be of great help.
Yes, waiting for an update (:.
wait for update.
@gaaclarke In light of https://github.com/flutter/flutter/issues/72009 and https://github.com/flutter/samples/tree/master/add_to_app/multiple_flutters can you update this issue with what additional work this is tracking (if any?)
@jmagman My understanding of this issue is that it isn't related to multiple flutters directly. Multiple flutters allows you to have multiple entry points into your module at low memory / time cost. This has to do with having multiple modules on disk you want to load up. The multiple-flutters work should help that effort but I imagine there is some tooling / documentation we need to support multiple modules on disk still. For example, one of the issues they were running into was the build failing because it was finding multiple libflutter.so files.