zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

Flutter 和 Android 通讯 Pigeon 类型安全

Androidflutter安全 类型 通讯
2023-09-14 09:00:05 时间

本文地址


目录

Pigeon 简介

Pigeon is a code generator tool to make communication between Flutter and the host platform type-safe, easier and faster.

使用 MethodChannel 在 host 和 client 之间进行通信,并不是类型安全的。为了正确通信,host 和 client 必须声明相同的参数和数据类型。Pigeon 可以用作 MethodChannel 的替代品。

特性

  • generate code that sends messages in a structured typesafe 结构化类型安全 manner
  • no need to match strings between host and client for the names and datatypes of messages
  • supports nested classes 支持嵌套类
  • supports grouping messages into APIs 支持分组(即支持以 class 为单位分组)
  • generation of asynchronous wrapper code and sending messages in either direction 双向通讯
  • generated code is readable -- 支持使用 Builder 模式组装数据
  • guarantees 保证 no conflicts between multiple clients of different versions -- 可持续迭代

命令参数

flutter pub run pigeon \
  --input pigeons/message.dart \
  --dart_out lib/pigeon.dart \
  --java_out pigeons/Pigeon.java \
  --java_package "com.bqt.test.pigeon"

目前只支持生成 Java 文件,而不支持生成 Kotlin 文件

  • --input:原始的 .dart 文件路径
  • --dart_out:转换后的 .dart 文件保存路径
  • --java_out:转换后的 .java 文件保存路径
  • --java_package:包名
  • --no-dart_null_safety:生成 non-null-safe 的代码

空安全

  • 支持生成空安全 null-safe 代码:
    • Nullable and Non-nullable class fields
    • Nullable return values
    • Nullable method parameters
  • 不支持可为空的泛型类型参数 Nullable generics type arguments
    • 例如,仅支持 List<int>,不支持 List<int?>,因为 Java 中根本没有等价的类型
    • 虽然声明时泛型参数 <String?> 可为空,但生成的代码仍是不可为空的

The default is to generate null-safe code but in order to generate non-null-safe code run Pigeon with the extra argument --no-dart_null_safety.

使用步骤

  • 执行命令 dart pub add pigeon 添加依赖
    • 也可手动在 pubspec.yaml 中添加依赖:pigeon: ^3.1.0
    • 还需要在开发过程中依赖即可,即以 dev_dependencies 方式依赖
  • lib 目录外定义一个新的目录,用于存放定义通讯接口 .dart 文件,例如 pigeons/message.dart
  • pigeons/message.dart 中定义数据结构、声明通讯接口
  • 执行 flutter pub run pigeon xx 命令生成 Flutter 所需的 .dart 文件及客户端所需的 .java 文件
  • 将生成的 .dart 文件移动到 lib 目录,将客户端所需的 .java 文件移动到正确的包名目录下
  • 在客户端实现 @HostApi 中声明的方法,在 Flutter 端实现 @FlutterApi 中声明的方法
  • 在客户端和 Flutter 端调用静态方法 xxx.setup 注册 method channel
  • 在合适的时机调用 method channel 即可

Pigeon 使用案例

定义数据结构及声明接口

import 'package:pigeon/pigeon.dart';

// ------------------------------------- 定义数据类型 -------------------------------------
class Book {
  int? id; // Initialization isn't supported for fields in Pigeon data classes
  String? title; // 不支持初始值,所以只能使用 nullable 类型,也即默认值都是 null
  Author? author; // 支持嵌套类
}

class Author {
  String? name;
  bool? male;
  StateEnum? state; // 支持枚举
}

enum StateEnum { success, error, unknown } // 枚举类

// ------------------------------------- 定义 native 方法 -------------------------------------

@HostApi() // 使用注解 @HostApi 修饰的方法,是在 native 中实现的,可以被 Flutter  调用的方法
abstract class TestBookApi {
  Book? search(String? keyword); // 支持可为空的参数或返回值
  List<Book> searchList(String keyword); // 也支持不可为空的参数或返回值
  List<Book?> searchList2(List<String?> keys); // 虽然定义的泛型参数 <String?> 可为空,但生产的代码仍是不可为空的
  void testNoArguments(); // 也支持没有参数或没有返回值
}

@HostApi()
abstract class TestAsyApi {
  @async
  String calculate(int key); //默认生成同步的 handlers,可以使用 @async 注解异步响应消息
}

@HostApi()
abstract class TestTaskQueueApi {
  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
  int add(int x, int y);
}
// ------------------------------------- 定义 Flutter 方法 -------------------------------------

@FlutterApi() // 使用注解 @FlutterApi 修饰的方法,是在 Flutter  中实现的,可以被 native 调用的方法
abstract class TestFlutterApi {
  String getYourName(int key);
}

Android 端的同步 HostApi

object TestBookApiImpl : Pigeon.TestBookApi {
    override fun search(keyword: String?): Pigeon.Book = Pigeon.Book().apply {
        println("search $keyword isMainThread: ${isMainThread()}") // 默认回调在主线程
        id = null // 参数、返回值、属性都有明确的 @Nullable 或 @NonNull 注解,但是属性都是 @Nullable 的,默认值都是 null
        title = "《我的$keyword》"// 支持调用 set/get 方法,也支持使用 Builder 模式
        author = Pigeon.Author.Builder()
            .setName("白乾涛")
            .setMale(true)
            .setState(Pigeon.StateEnum.success)
            .build()
    }

    override fun searchList(keyword: String): MutableList<Pigeon.Book> = mutableListOf(Pigeon.Book()).also {
        println("searchList $keyword isMainThread: ${isMainThread()}")
    }

    override fun searchList2(keys: MutableList<String>): MutableList<Pigeon.Book> = mutableListOf(Pigeon.Book()).also {
        println("searchList2 $keys isMainThread: ${isMainThread()}")
    }

    override fun testNoArguments() = println("testNoArguments isMainThread: ${isMainThread()}")
}

Android 端的异步 HostApi

object TestAsyApiImpl : Pigeon.TestAsyApi {
    override fun calculate(key: Long, result: Pigeon.Result<String>?) {
        println("calculate $key isMainThread: ${isMainThread()}") // 注意,这里也是回调在主线程
        if (Random.nextBoolean()) {
            Thread {
                Thread.sleep(1000 * 3) // 可以在子线程中回调
                result?.success("白乾涛") // 异步调用 native 时,native 不是直接返回,而是以回调的形式响应
            }.start()
        } else {
            result?.error(Throwable("异常了")) // Flutter 端需要捕获异常
        }
    }
}

Android 端的 TaskQueueApi

object TestTaskQueueApiImpl : Pigeon.TestTaskQueueApi {
    override fun add(x: Long, y: Long): Long {
        println("add $x $y isMainThread: ${isMainThread()}") // false
        // 一定要注意了,使用 serialBackgroundThread 时是回调在【子线程】,而其他情况都是回调在【主线程】
        return x + y
    }
}

Android 端的核心逻辑

fun isMainThread() = Looper.getMainLooper().thread == Thread.currentThread()
fun Handler.postDelayed(delay: Long, runnable: Runnable) = postDelayed(runnable, delay)
class MyFlutterActivity : FlutterActivity() {
    var mFlutterApi: Pigeon.TestFlutterApi? = null

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        val binaryMessenger: BinaryMessenger = flutterEngine.dartExecutor.binaryMessenger

        Pigeon.TestBookApi.setup(binaryMessenger, TestBookApiImpl)
        Pigeon.TestAsyApi.setup(binaryMessenger, TestAsyApiImpl)
        Pigeon.TestTaskQueueApi.setup(binaryMessenger, TestTaskQueueApiImpl)
        mFlutterApi = Pigeon.TestFlutterApi(binaryMessenger)

        callFlutterMethod()
    }

    private fun callFlutterMethod() {
        val handler = Handler(Looper.getMainLooper())
        (0..20)
            .map { it.toLong() * 100 }
            .forEach {
                handler.postDelayed(it) {
                    mFlutterApi?.getYourName(it) { value -> // 必须在主线程中调用
                        println("从 Flutter 获取到的值是:$value ,isMainThread:${isMainThread()}") // true,回调在主线程
                    }
                }
            }
    }
}

Flutter 端的 FlutterApi

定义在 lib/flutter_api.dart

import 'package:flutter/material.dart';
import 'package:qt_flutter_module/pigeon.dart';

class TestFlutterApiImpl extends TestFlutterApi {
  @override
  String getYourName(int key) {
    debugPrint("Flutter 收到 native 的请求:$key");
    return "结果$key";
  }
}

Flutter 端的核心逻辑

import 'package:flutter/material.dart';
import 'flutter_api.dart';
import 'pigeon.dart';

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  TestBookApi api = TestBookApi(); // 注意,引用的是 lib/pigeon.dart 下的类,而不是 pigeons 目录下的
  TestAsyApi asyApi = TestAsyApi();
  TestTaskQueueApi taskQueueApi = TestTaskQueueApi();

  @override
  void initState() {
    super.initState();
    TestFlutterApi.setup(TestFlutterApiImpl()); // 同样需要在使用前注入 method channel
  }

  void _incrementCounter() {
    setState(() => _counter++);
    callNativeMethod(); // 在合适的时机调用 method channel
  }

  void callNativeMethod() {
    if (_counter % 5 == 0) {
      api.search("哈哈").then((book) { // 都是异步回调
        if (book != null) {
          debugPrint("查询结果:${book.id} - ${book.title}");
          Author? author = book.author;
          if (author != null) {
            debugPrint("作者信息:${author.name} - ${author.male} - ${author.state}");
          }
        }
      });
    } else if (_counter == 1) {
      api.searchList("哈哈哈").then((list) => debugPrint("返回数量 ${list.length}"));
    } else if (_counter == 2) {
      api.searchList2(["啊", "哈"]).then((list) => debugPrint("返回数量 ${list.length}"));
    } else if (_counter == 3) {
      api.testNoArguments();
      taskQueueApi.add(2, 3).then((value) => debugPrint("求和结果:$value"));
    } else {
      asyApi.calculate(10088).then((value) => debugPrint("返回值为 $value"));
    }
  }

  @override
  Widget build(BuildContext context) { }
}

2022-6-3