The previous articles introduced the basics of Flutter development, including the Navigator component, Flex layout, image loading, and Widget lifecycle. The article links are as follows:
- Using the Navigator Component in Flutter
- Detailed Explanation of Flex Layout in Flutter
- Detailed Explanation of Image Loading in Flutter
- Widget Lifecycle in Flutter
Today, I will introduce the Flutter hybrid development mode and how to add Flutter modules to existing Android projects, with the main content as follows:
- Flutter Hybrid Development Mode
- Creating Flutter Modules
- Various Ways to Add Flutter
- Adding a Single Flutter Page
- Adding FlutterFragment
- Flutter and Android Navigation
Flutter Hybrid Development Mode#
Flutter hybrid development generally has two approaches:
- Directly using the native project as a subproject of the Flutter project, where Flutter will automatically create the android and ios project directories;
- Creating a Flutter Module as a dependency to be added to the existing native project.
The second approach is more decoupled compared to the first, especially for existing projects, which incurs lower transformation costs.
Creating Flutter Modules#
There are two ways to create a Flutter Module:
- Create a Flutter Module using the command
flutter create -t module --org com.manu.flutter flutter_module_one
- Create a Flutter Module using Android Studio
In Android Studio, select File->New->New Flutter Project, and choose Flutter Module to create a Flutter Module subproject, as shown below:
Various Ways to Add Flutter#
The addition methods here refer to the second approach, adding the Flutter module to the existing project as a Flutter Module. There are two ways to add Flutter to an existing Android project:
- Integrating as an AAR in the existing Android project:
After creating the Flutter Module, it needs to be compiled into AAR format, which can be done with the following command:
// cd to the root directory of the Flutter Module
cd flutter_module
flutter build aar
In Android, AAR can also be compiled using Android Studio by selecting Build->Flutter->Build AAR.
Then, follow the prompts to configure the relevant settings in the main project’s build.gradle file, as shown below:
repositories {
maven {
url 'G:/xxx/flutter_module_one/build/host/outputs/repo'
}
maven {
url 'https://storage.googleapis.com/download.flutter.io'
}
}
buildTypes {
profile {
initWith debug
}
}
dependencies {
debugImplementation 'com.manu.flutter.flutter_module_one:flutter_debug:1.0'
profileImplementation 'com.manu.flutter.flutter_module_one:flutter_profile:1.0'
releaseImplementation 'com.manu.flutter.flutter_module_one:flutter_release:1.0'
}
- Integrating as a Flutter module into the existing Android project:
Configure the Flutter module in the settings.gradle file as follows:
include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir,
'flutter_module_two/.android/include_flutter.groovy'
))
Then, add the Flutter module dependency in the build.gradle file as follows:
dependencies {
implementation project(':flutter')
}
Adding a Single Flutter Page#
Create an Activity that extends FlutterActivity and declare it in the AndroidManifest.xml file:
<activity
android:name=".AgentActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
</activity>
So how do you start this FlutterActivity? As follows:
// Default route /
myButton.setOnClickListener {
startActivity(
FlutterActivity.createDefaultIntent(this)
)
}
// Custom route
myButton.setOnClickListener {
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("/my_route")
.build(this)
)
}
The above code will internally create its own FlutterEngine instance. Each FlutterActivity creates its own FlutterEngine, which means that starting a standard FlutterActivity will have a brief delay when the interface is visible. You can choose to use a pre-cached FlutterEngine to reduce this delay. In fact, it will first check if a pre-cached FlutterEngine exists; if it does, it will use that FlutterEngine; otherwise, it will continue to use a non-pre-cached FlutterEngine. The source code judgment is as follows:
/* package */ void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");
// 1. Check for cached FlutterEngine
String cachedEngineId = host.getCachedEngineId();
if (cachedEngineId != null) {
flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId);
isFlutterEngineFromHost = true;
if (flutterEngine == null) {
throw new IllegalStateException(
"The requested cached FlutterEngine did not exist in the FlutterEngineCache: '"
+ cachedEngineId
+ "'");
}
return;
}
// 2. Check for a custom FlutterEngine
// Second, defer to subclasses for a custom FlutterEngine.
flutterEngine = host.provideFlutterEngine(host.getContext());
if (flutterEngine != null) {
isFlutterEngineFromHost = true;
return;
}
Log.v(
TAG,
"No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this FlutterFragment.");
// 3. Create a new FlutterEngine
flutterEngine =
new FlutterEngine(
host.getContext(),
host.getFlutterShellArgs().toArray(),
/*automaticallyRegisterPlugins=*/ false);
isFlutterEngineFromHost = false;
}
The usage of the pre-cached FlutterEngine will not be elaborated here; you can refer to the official website.
Adding FlutterFragment#
Similarly, to add a FlutterFragment to the existing Android project, for easier communication later, you should also customize a Fragment that extends FlutterFragment and then add it to an Activity, as follows:
class AgentActivity2 : FragmentActivity() {
private val flutterFragmentTag = "flutter_fragment_tag"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_agent2)
val fragmentManager = supportFragmentManager
var flutterFragment = fragmentManager.findFragmentByTag(flutterFragmentTag)
if (flutterFragment == null){
// flutterFragment = FlutterFragment.createDefault()
flutterFragment = MFlutterFragment
.withNewEngine()
?.build()
if (flutterFragment != null) {
fragmentManager.beginTransaction()
.add(R.id.ff_container,flutterFragment,flutterFragmentTag)
.commit()
}
}
}
}
To navigate to the Activity that adds the FlutterFragment, use the Intent as follows:
// Navigate to the Activity that adds the Fragment
val intent = Intent(this@LaunchActivity,AgentActivity2::class.java)
startActivity(intent)
Flutter and Android Navigation#
Flutter and Android can navigate to each other. The previous sections mainly discussed native Android navigating to FlutterActivity or adding FlutterFragment. So how does a Flutter page navigate to a native Activity?
This involves the communication mechanism between Flutter and native, mainly including MethodChannel, EventChannel, and BasicMessageChannel. This content is quite extensive, and a small section cannot cover it all. Here, I will briefly introduce the use of MethodChannel. MethodChannel is mainly used to pass method calls, allowing Flutter pages to call methods provided by the Android native API.
I will mainly introduce how to use MethodChannel to implement navigation from Flutter to native Android. Whether it is a single Flutter page or a FlutterFragment, you need to extend FlutterActivity and FlutterFragment respectively and override the configureFlutterEngine method, as follows:
// FlutterActivity
class AgentActivity : FlutterActivity() {
private val tag = AgentActivity::class.java.simpleName;
private val channel = "com.manu.startMainActivity"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.d(tag,"configureFlutterEngine")
// Register MethodChannel to listen for method calls from the Flutter page
MethodChannel(flutterEngine.dartExecutor, channel)
.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
if ("startMainActivity" == methodCall.method) {
MainActivity.startMainActivity(this)
result.success("success")
} else {
result.notImplemented()
}
}
}
companion object{
/**
* Recreate NewEngineIntentBuilder to ensure effectiveness
*/
fun withNewEngine(): MNewEngineIntentBuilder? {
return MNewEngineIntentBuilder(AgentActivity::class.java)
}
}
/**
* Custom NewEngineIntentBuilder
*/
class MNewEngineIntentBuilder(activityClass: Class<out FlutterActivity?>?) :
NewEngineIntentBuilder(activityClass!!)
}
// Similarly for FlutterFragment
// Omitted ...
Be sure to override the withNewEngine method; otherwise, Flutter navigation to the native Activity will fail. The Flutter page invokes the method for the method call, as follows:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Flutter Page"),
centerTitle: true,
),
body: PageWidget()
),
routes: <String,WidgetBuilder>{
},
);
}
}
/// Stateful Widget
class PageWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _PageState();
}
}
/// State
class _PageState extends State<PageWidget> {
MethodChannel platform;
@override
void initState() {
super.initState();
platform = new MethodChannel('com.manu.startMainActivity');
}
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
_startMainActivity();
},
child: Text("Flutter to Android"),
);
}
/// Navigate to the native Activity
void _startMainActivity(){
platform.invokeMethod('startMainActivity').then((value) {
print("value:startMainActivity");
}).catchError((e) {
print(e.message);
});
}
}
In addition, the communication mechanism between Flutter and native will be introduced in detail in subsequent articles.