Flutter里built-in的native开发

Flutter自己, 和React Native一样, 仅仅是一个UI结构罢了. 这跟Android, iOS系统还是差了许多. 也便是说, 当涉及到: 指纹, 地理方位, 设备文件, 拍照, … 等等许多非UI的作业时, Flutter自己是处理不了的.

可是Flutter供给了一个跨渠道结构, 叫做MethodChannel, 来下沉这种非UI的作业到native渠道去. 说人话便是, Flutter自己不支撑拍照功用, 但Android, iOS支撑啊. 于是当用户在Flutter中想拍照时, Flutter就告知Android或iOS, 说”请你拍照, 拍完了告知我”. 这样当native渠道干完了活, 就告知Flutter完结成果(成功的数据, 或失利的原因). 这种便是所谓的”下沉到native渠道”的作业.

原生的MethodChannel还蛮麻烦的, 我举个例子哦, 来看下为了支撑某一功用, Android端要这样接纳来自Flutter端的恳求:

    val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "CHANNEL_NAME")
    methodChannel.setMethodCallHandler {
      // This method is invoked on the main thread.
      call, result ->
      if (call.method == "getBatteryLevel") {
        val batteryLevel = getBatteryLevel()
        if (batteryLevel != -1) {
          result.success(batteryLevel)
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null)
        }
      } else {
        result.notImplemented()
      }
    }

有经验的开发, 一眼就能看了这个代码最大的问题: 扩展性太差! 要是以后有一堆的native功用需求通过MethodChannel来供给, 那代码就会慢慢地变得臃肿, 比如像这样:

methodChannel.setMethodCallHandler { call, result ->
    if(call.emthod == "getBatteryLevel") {...}
    else if(call.emthod == "getLocation") {...}
    else if(call.emthod == "takePicture") {...}
    else if(call.emthod == "fingerprintVerify") {...}
    else if(call.emthod == "getScreenLighting") {...}
    else if(call.emthod == "getOSLanguage") {...}
    else if(call.emthod == "getAppCachePath") {...}
    ... ...
    ...
}

不必说这样一长串的if-else chain是很不利于保护, 也简单犯错的. 在代码分工上, 咱们需求代码解耦合, 各自只负责自己的部分, 而不是让上面一个类越来越大, 什么都管.

另一个MethodChannel的问题便是, 不区分数据类型. 便是说, 数据类型在Flutter, Android, iOS中是不一样的. 但由于跨渠道了, 所以MethodChannel并没有供给一个机制来查验你传来的到底是不是String, 到底是不是MyResponse类型. 这样当类型不对时就会crash. 更好的体验则是, 像java等强验证言语一样, 在dev编码时就告知你这个类型不对, 而不是比及运转时了让用户crash.

Pigeon的作业原理(简略版)

所以Pigeon便是为了处理这两个问题而产生的. Pigeon其实便是让你自己界说好了你这次作业的接口, 然后Pigeon帮你生成MethodChannel的代码. 这样整体代码看起来简洁, 分工好, 并且是type safe.

比如说当你界说了一个接口:

import 'package:pigeon/pigeon.dart';
class Book {
  String? title;
  String? author;
}
@HostApi()
abstract class BookApi {
  List<Book?> search(String keyword);
}

— 这样每个不同的功用, 便是独立一个Api类, 这样是不是比长长的if-else chain要解耦得多

而你运转一个脚本(下面会讲), 就会为咱们在Flutter端, 在Android端, 在iOS端生成代码. 以Android端的为例, 它便是这样的底子结构:


data class Book (
  val title: String? = null,
  val author: String? = null
)
interface BookApi {
  fun search(keyword: String): List<Book?>
  companion object {
    fun setUp(binaryMessenger: BinaryMessenger, api: BookApi?) {
        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.BookApi.search", codec)
        if (api != null) {
          channel.setMessageHandler { message, reply ->
            ...
            reply.reply(wrapped)
          }
        } 
    }
}

看到了这个生成的代码, 就知道Pigeon其实也不是什么黑科技, 便是底层使用了MethodChannel罢了. (补白: MethodChannel与BasicMessageChannel是相似的东西, 只不过BasicMessageChannel还有相关的解码编码集罢了. 你能够了解二者是近似的)

补白: 其实Android程序员应该很熟悉. MethodChannel便是相似OkHttp, Pigeon便是相似Retrofit (也是界说好接口, 为开发生成代码, 终究底层都是MethodChannel/OkHttp在作业)

Pigeon开发前的准备作业

下面就开始是本篇文章的重点了. 首要Flutter与pub.dev上的文档是相互抵触, 或是底子就不齐全的.

  • Flutter官网的 Writing custom platform-specific code
  • Pigeon自己的ReadMe与Exampel.

特别是要去运转iOS端, 就会碰到一些xcode相关问题, 这个真的是坑, 所以要一一讲解下这些坑以及怎么绕过这些坑.

step 1. 装置Pigeon

$ dart pub add pigeon
// 2023.03年装置了 pigeon v9.1.3(现现已支撑swift与kotlin了)

以前我用pigeon v8.x时, 还只支撑生成java与objc代码. 现在v9.x年代现已支撑swift与kotlin了.

step 2. Flutter端 – 界说接口文件

// BookIn.dart
import 'package:pigeon/pigeon.dart';
class Book {
  String? title;
  String? author;
}
@HostApi()
abstract class BookApi {
  List<Book?> search(String keyword);
}

step3. Android端

若你的Android端的主包是a.b.c, 而你想Pigeon生成后的代码放到a.b.c.pigeons包里, 那很简单, 你去Android Studio等IDE新建目录(directory)或是新建包(package)都行, 总之便是要先建立好pigeons这个包. 否则下面的脚本会报错, 说方针目录不存在, 无法新建文件

step4. iOS端 (坑1)

**这一步是Flutter与pub.dev官网上都没讲到的一步. 不加这一步, 就编译iOS端会失利. 所以这算是官网没讲清楚, 导致失利的第一个坑. **

你的iOS端默许便是把swift文件放到Runners目录下的, 如下面的AppDelegate.swift文件一样.

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

天然, 为了办理起来更便利, 咱们不可能什么都放到Runners目录下. 相似Android中的package, iOS端的包叫做group.

1). 咱们在xcode中翻开Flutter工程的iOS目录中的Runners.xcworkspace:

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

2). 在Runners上新建group, 取名叫pigeons:

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

3). 生成group后, 在pigeons这个gorup上右击, 选择新建swift文件, 新建一个BookGenerated.swift文件

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

总结: 也便是说, 在运转脚本前, 咱们必须得

  • 在Android端建立好方针目录(即package)
  • 在iOS端建立好方针目录(即group), 以及方针文件!!!

至于原因, 那便是和Android程序大大的不同, xcode工程有一个索引文件.

  • Android工程中, 你新加一个txt, gradle会知道在build时不参加这个txt文件到build过程中来. 若你在src目录中新加了个java文件, gradle会主动参加这些java文件到build过程中来
  • iOS工程则不是. Xcode彻底没这么智能, 它底子不会主动地把所有swift与objc文件(.h, .m)参加到编译过程中来. Xcode会去找一个叫project.pbxporj的文件. 只需在这个project.pbxporj里的文件与目录, 才会参加到编译过程中来.

也便是说, 官网上说的脚本主动生成swift文件后, xcode底子不认识这个新文件, 从而会导致终究编译失利, 说找不到BookApi这个类型. 处理办法便是上面的: 你自己去xcode中找开iOS工程, 新建group与swift文件. 这样一来, project.pbxporj就会把刚刚新建的group与swift文件参加进来. p.s. 后面你运转脚本后, swift文件的内容会被脚本的生成所覆盖, 这个不必忧虑.

同时留意, 在git提交中也提交这个文件哦:

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

题外话: xcode小常识

xcodeproj文件与xcworkspace文件都能够翻开一个iOS工程. 可是一般使用xcworkspace. 原因是:

  • 若xcode新建一个iOS工程, 它会主动生成一个xcodeproj文件, 你双击它就能够翻开工程
  • 但你要是用了cocoapod(一个相似Android端的Gradle的依赖办理东西), pod就会主动帮咱们生成xcworkspace. 这时为了能使用你在cocoapod中声明的第三方库, 就得用xcworkspace来翻开iOS工程才行

step 5. 运转脚本

# flutter pub run pigeon \ 
--input lib/pigeons/demo/BookIn.dart \ 
--dart_out lib/pigeons/demo/BookOut.dart \ 
--swift_out ios/Runner/pigeons/BookGenerated.swift \
--kotlin_out ./android/app/src/main/kotlin/ca/six/readerf/reader_flutter/pigeons/BookGenerated.kt \  
--experimental_kotlin_package "ca.six.readerf.reader_flutter.pigeons"

我的BookIn.dart现已在step2中界说好了. 方位就在: lib/pigeons/demo/BookIn.dart

而生成的文件, 分别在dart, android, iOS端:

  • lib/pigeons/demo/BookOut.dart
  • ios/Runner/pigeons/BookGenerated.swift
  • android/app/src/main/kotlin/ca/six/readerf/reader_flutter/pigeons/BookGenerated.kt

最终成果如下:

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

step 6. Android端连接好Pigeon

import io.flutter.embedding.android.FlutterActivity
// 下面三行import要新加
import io.flutter.embedding.engine.FlutterEngine 
import ca.six.readerf.reader_flutter.pigeons.Book
import ca.six.readerf.reader_flutter.pigeons.BookApi
class MainActivity : FlutterActivity() {
    // 不再重写onCreate(), 改为重写configureFlutterEngine()办法
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        val messenger = flutterEngine.dartExecutor.binaryMessenger
        BookApi.setUp(messenger, BookApiImpl()) // 留意是setUp()办法, 不是setup()
    }
}
class BookApiImpl : BookApi {
    override fun search(keyword: String): List<Book?> {
        val book = Book("android-$keyword", "szw2")
        return listOf(book)
    }
}

step 7. iOS端连接好Pigeon


// 新加了此类
class BookApiImpl: NSObject, BookApi {
  func search(keyword: String) -> [Book?] {
    let result = Book(title: "\(keyword)'s Life", author: keyword)
    return [result]
  }
}
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    // 新加了这两行
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let api = BookApiImpl()
    BookApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

step 8. Dart端调用native端干活

import 'package:reader_flutter/pigeons/demo/BookOut.dart';
  Future<void> nativeSearchBook() async {
    BookApi api = BookApi();
    List<Book?> books = await api.search("from12");
    print('szw reply: ${books[0]}');
  }

咱们在Flutter里只需调用下这个nativeSearchBook()办法就能得到book列表了.

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

完美打通了三端. 上面的其实都是我探索中的正常代码, 下面我就来讲下官网上的各种过错, 免得其它人也碰到相似的问题

Pigeon的坑 — iOS端

坑1

xcode中不会主动把你生成的BookGenerated.swift放到project.pbxproj文件里去, 所以在运转脚本前咱们要先去xcode中新建好group与BookGenerated.swift

坑2

Swift Compiler Error (Xcode): Type 'BookApiImpl' does not conform to protocol 'BookApi'
/Users/../ios/Runner/AppDelegate.swift:4:6

处理办法便是严格依照生成的protocol(相似Android中的interface)来, 所以只需把下面的左侧代码, 改为右侧代码就行:

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案
(坑就坑在, 左侧是官网上的代码, 你一copy就会编译失利)

坑3

Flutter埋坑 Pigeon使用过程中的多个坑与解决方案

即pigeon的ReadMe上讲的:

let api = BookApiImpl()
BookApiSetup.setUp(getFlutterEngine().binaryMessenger, api)

是现已过时了的操作, 要想取得binaryMessenger, 就得用:

let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let api = BookApiImpl()
BookApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)

Pigeon的坑 — Android端

坑4

留意要加import哦

import android.os.Bundle
import ca.six.readerf.reader_flutter.pigeons.Book
import ca.six.readerf.reader_flutter.pigeons.BookApi

这个也是官网上没有讲的, 也要自己小心

坑5

Pigeon官网上说的是BookApi.setup() 但实际上生成的代码却是: BookApi.setUp(), 这个点也要小心

坑6

仍是getBinaryMessenger()办法现已被删除了. 所以官网上的代码会编译失利:

class MainActivity: FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ✯ 坑5: 官网上写是setup(), 但其实应该是 setUp(), 这个真是初级过错, 文档写得太差了
        // ✯ getBinaryMessage()没了, 这是坑6        
        BookApi.setUp(getBinaryMessenger(), BookApiImpl()) 
    }
}

真实的写法在上面现已讲过了, 便是:


class MainActivity : FlutterActivity() {
    // 不再重写onCreate(), 改为重写configureFlutterEngine()办法
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        val messenger = flutterEngine.dartExecutor.binaryMessenger
        BookApi.setUp(messenger, BookApiImpl()) // 留意是setUp()办法, 不是setup()
    }
}

总结

两个官网的介绍相互抵触, 导致咱们开发用起Pigeon来很痛苦. 另外一个点就同xcode工程要用project.pbxproj来办理源文件, 这一点也让许多从Android过来的Flutter开发由于不了解这特性而导致xcode编译失利. 上面6个坑都填好后, 咱们就能成功地运转Android与iOS了