banner
jzman

jzman

Coding、思考、自觉。
github

Flutter Series: Hybrid Development for Android

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:

Today, we will introduce the Flutter hybrid development mode and how to add Flutter modules to existing Android projects, with the main content as follows:

  1. Flutter Hybrid Development Mode
  2. Creating Flutter Modules
  3. Various Ways to Add Flutter
  4. Adding a Single Flutter Page
  5. Adding FlutterFragment
  6. Mutual Navigation Between Flutter and Android

Flutter Hybrid Development Mode#

Flutter hybrid development generally has two approaches:

  1. Directly using the native project as a subproject of the Flutter project, where Flutter will create the android and ios project directories by default.
  2. Creating a Flutter Module and adding it as a dependency to the existing native project.

The second approach is more decoupled compared to the first, especially for existing projects, resulting in lower transformation costs.

Creating Flutter Modules#

There are two ways to create a Flutter Module:

  1. Create a Flutter Module using the command
flutter create -t module --org com.manu.flutter flutter_module_one
  1. 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:

image

Various Ways to Add Flutter#

The methods of adding refer to the second approach, where the Flutter module is added to the existing project as a Flutter Module. There are two ways to add Flutter to an existing Android project:

  1. Integrating as an AAR into 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, you can also compile the AAR using Android Studio by selecting Build->Flutter->Build AAR.

Then, according to the prompts, configure the relevant settings in the main project’s build.gradle file, as follows:

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'
}
  1. 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 we 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. Internally, 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 pre-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. Is there 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 check the official website for details.

Adding FlutterFragment#

Similarly, to add a FlutterFragment to an existing Android project, it is also advisable to 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 = 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 following Intent:

// Navigate to the Activity that adds the Fragment
val intent = Intent(this@LaunchActivity, AgentActivity2::class.java)
startActivity(intent)

Mutual Navigation Between Flutter and Android#

Flutter and Android can navigate to each other. The previous sections mainly discussed native Android navigating to FlutterActivity or Activities that add FlutterFragment. So how does a Flutter page navigate to a native Activity?

This involves the communication mechanism between Flutter and native, which mainly includes MethodChannel, EventChannel, and BasicMessageChannel. This content is quite extensive, and it cannot be fully covered in a small section. Here, we 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.

We will mainly introduce how to use MethodChannel to achieve navigation from Flutter to native Android. Whether it is a single Flutter page or a FlutterFragment, both 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 ...

Make sure to override the withNewEngine method; otherwise, navigation from Flutter to the native Activity will fail. The Flutter page can invoke methods using invokeMethod, as shown below:

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);
    });
  }
}

Additionally, the communication mechanism between Flutter and native will be introduced in subsequent articles.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.