As we mentioned earlier, Flutter uses a BasicMessageChannel for native communication, which fully decouples interfaces and communicates via protocols. However, the problem with Flutter is that multiple terminals need to maintain a set of protocol specifications, which inevitably leads to communication costs for collaborative development. Therefore, One solution that Flutter officials have come up with is Pigeon.

Pigeon was invented to deal with the development costs of multiterminal communication. The idea is that a protocol can be used to generate multiple bits of code, so that multiple bits only need to maintain one set of protocols, and Pigeon can automatically generate all the other bits of code, so that multiple bits can be unified.

The official document is shown below.

Pub. Flutter – IO. Cn/packages/PI…

The introduction of

We need to introduce Pigeon into dev_dependencies:

Dev_dependencies: pigeon: ^ 1.0.15Copy the code

Dart next, create a.dart file, such as schema.dart, in the folder equivalent to the lib folder of Flutter. This is the communication protocol file.

For example, we need a unified entity: Book, as shown below.

import 'package:pigeon/pigeon.dart'; class Book { String? title; String? author; } @HostApi() abstract class NativeBookApi { List<Book? > getNativeBookSearch(String keyword); void doMethodCall(); }Copy the code

This is our protocol file, where @hostAPI stands for calling native methods from the Flutter side, or @FlutterAPI stands for calling native methods from the Flutter side.

generate

The following command would have Pigeon generate code based on the Pigeon protocol. This configuration would have required some file directory, package name, etc. We could save it to an SH file so that after updating, we could just execute the sh file.

flutter pub run pigeon \
  --input schema.dart \
  --dart_out lib/pigeon.dart \
  --objc_header_out ios/Runner/pigeon.h \
  --objc_source_out ios/Runner/pigeon.m \
  --java_out ./android/app/src/main/java/dev/flutter/pigeon/Pigeon.java \
  --java_package "dev.flutter.pigeon"
Copy the code

Dart file, as the protocol, and specify output paths for dart, iOS, and Android code.

Normally, the generated code is ready to use.

Pigeon generated code in Java and OC, mainly to make it compatible with more projects. You can convert it to Kotlin or Swift.

use

Using this example, let’s take a look at cross-terminal communication based on code that Pigeon generated.

First, in the Android code, an interface with the same protocol name, NativeBookApi, is generated, corresponding to the protocol name of the HostApi annotation tag above. In the FlutterActivity’s descendant class, create an implementation class for this interface.

private class NativeBookApiImp(val context: Context) : Api.NativeBookApi { override fun getNativeBookSearch(keyword: String?) : MutableList<Api.Book> { val book = Api.Book().apply { title = "android" author = "xys$keyword" } return Collections.singletonList(book) } override fun doMethodCall() { context.startActivity(Intent(context, FlutterMainActivity::class.java)) } }Copy the code

By the way, the engine is created as a FlutterEngineGroup. Otherwise, the engine object can be fetched in a different way.

class SingleFlutterActivity : FlutterActivity() {

    val engine: FlutterEngine by lazy {
        val app = activity.applicationContext as QDApplication
        val dartEntrypoint =
            DartExecutor.DartEntrypoint(
                FlutterInjector.instance().flutterLoader().findAppBundlePath(), "main"
            )
        app.engines.createAndRunEngine(activity, dartEntrypoint)
    }
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        Api.NativeBookApi.setup(flutterEngine.dartExecutor, NativeBookApiImp(this))
    }

    override fun provideFlutterEngine(context: Context): FlutterEngine? {
        return engine
    }

    override fun onDestroy() {
        super.onDestroy()
        engine.destroy()
    }
}
Copy the code

The core method for initializing Pigeon is the setup method in the NativeBookApi, passing in an implementation of the engine and protocol.

Now, let’s look at how to call this method in a Flutter. Before Pigeon, we used to communicate with a Channel, creating a protocol name of type String. Now with Pigeon, these error-prone strings are hidden and all become normal method calls.

In Flutter, Pigeon automatically creates the NativeBookApi class, instead of the Android interface, in which methods defined in the protocols getNativeBookSearch and doMethodCall have been generated.

List<Book? > list = await api.getNativeBookSearch("xxx"); setState(() => _counter = "${list[0]? .title} ${list[0]? .author}");Copy the code

It is very convenient to call with await. It can be seen that after Pigeon was encapsulated, cross-end communication was completely encapsulated by the protocol, and the processing of various strings was also hidden, thus further reducing the possibility of manual error.

To optimize the

In practical use, Flutter calls native methods to get data, and the native side processes the data and passes it back to Flutter. So in Pigeon’s Android code, the implementation of the protocol function is a method with a return value, as shown below.

override fun getNativeBookSearch(keyword: String?) : MutableList<Api.Book> { val book = Api.Book().apply { title = "android" author = "xys$keyword" } return Collections.singletonList(book) }Copy the code

There is nothing inherently wrong with this method. If it is a network request, it can be handled using OKHttp’s Success and fail callbacks, but what about using coroutines?

Since coroutines broke callbacks and couldn’t be used in a function that Pigeon generated, you’d need to modify the protocol to add an @async annotation to the method to mark it as an asynchronous function.

We modify the protocol and regenerate the code.

@HostApi() abstract class NativeBookApi { @async List<Book? > getNativeBookSearch(String keyword); void doMethodCall(); }Copy the code

At this point, you’ll notice that the NativeBookApi’s implementation functions that return values have been changed to void and provide a result variable to handle the passing of the return values.

override fun getNativeBookSearch(keyword: String? , result: Api.Result<MutableList<Api.Book>>?)Copy the code

It’s easy to use, just stuff the return value back as result.

With this approach, we were able to take Pigeon and coroutine and use them together, and develop experiences that went up in an instant.

private class NativeBookApiImp(val context: Context, val lifecycleScope: LifecycleCoroutineScope) : Api.NativeBookApi { override fun getNativeBookSearch(keyword: String? , result: Api.Result<MutableList<Api.Book>>?) { lifecycleScope.launch { try { val data = RetrofitClient.getCommonApi().getXXXXList().data val book = Api.Book().apply { title = data.tagList.toString() author = "xys$keyword" } result? .success(Collections.singletonList(book)) } catch (e: Exception) { e.printStackTrace() } } } override fun doMethodCall() { context.startActivity(Intent(context, FlutterMainActivity::class.java)) } }Copy the code

So coroutine +Pigeon YYDS.

This is just an introduction to the situation where the Flutter calls Android. In fact, Android calls the Flutter in a different direction. The code is similar. I write Flutter, what’s the matter with iOS?

dismantling

Now that we know how Pigeon was used, let’s take a look at what the Pigeon actually did.

At a macro level, both the Dart side and the Android side generate three things.

  • Data entity classes, such as the Book class above
  • StandardMessageCodec, which is the BasicMessageChannel transport encoding class
  • Protocol interface \ classes, such as the NativeBookApi above

In Dart, data entities automatically generate encode and decode code for you, so that the data you retrieve is not a Channel Object type, but a protocol defined type, a great convenience for developers.

class Book { String? title; String? author; Object encode() { final Map<Object? , Object? > pigeonMap = <Object? , Object? > {}; pigeonMap['title'] = title; pigeonMap['author'] = author; return pigeonMap; } static Book decode(Object message) { final Map<Object? , Object? > pigeonMap = message as Map<Object? , Object? >; return Book() .. title = pigeonMap['title'] as String? . author = pigeonMap['author'] as String? ; }}Copy the code

In Android, a similar operation is done, which can be interpreted as a Java translation.

The following is Codec. StandardMessageCodec is the standard Codec for BasicMessageChannel. The data transmitted needs to implement its writeValue and readValueOfType methods.

class _NativeBookApiCodec extends StandardMessageCodec { const _NativeBookApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { if (value is Book) { buffer.putUint8(128); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } } @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: return Book.decode(readValue(buffer)!) ; default: return super.readValueOfType(type, buffer); }}}Copy the code

Similarly, Dart and Android code are almost identical, and it’s easy to understand that it’s a protocol and the rules are the same.

So here’s Pigeon’s core. Let’s take a look at how the protocol is implemented. First, let’s take a look at Dart. And process the data it returns.

If you are familiar with Channel usage, this code should be fairly clear.

Let’s take a look at the implementation in Android. The Android side is the event handler, so we need to implement the specific content of the protocol, this is the interface we implemented before, in addition, we also need to add setMessageHandler to deal with the specific protocol.

What’s interesting here is the encapsulation of that Reply class.

public interface Result<T> {
  void success(T result);
  void error(Throwable error);
}
Copy the code

So we said that in Pigeon you could make an asynchronous interface with @async, and the implementation of that asynchronous interface, that’s actually what was done here.

So now you can almost see how Pigeon actually works, which is basically just build_runner generating this code, eating all the dirty work, and what we’re actually seeing is implementations and calls of concrete protocol classes.

digression

So Pigeon is not a very sophisticated thing, but one of the very important ideas in the mix of Flutter, or the guiding idea of the Flutter team, is to generate relevant code through “protocols” and “templates”, as well as JSON parsing examples, and in fact, this is the case.

More on that, can decoupling and modularization between Android modules actually be handled in this way? So, the road is simple, the road leads to the same end, software engineering to the end, in fact, the idea is similar, everything changes, only the thought is eternal.

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit