草庐IT

Flutter 用 pigeon 写一个原生插件

法的空间 2023-03-28 原文

前言

在有 pigeon | Dart Package (flutter-io.cn) 之前,想写一个原生交互,是相当麻烦的一件事情。项目里面用 pigeon 来写了几个原生插件,用下来感觉还是很方便的,但是在开始使用的时候,我发现这方面的文章几乎没有,单端选手(几乎都是经常安卓端)很多,而且常常只是介绍 Flutter 端到原生端的通信,而原生端到 Flutter 端的几乎看不到介绍。

做项目的时候遇到用 RepaintBoundary 无法给 Webview 截屏的问题,查了下,是个已知的问题。
RepaintBoundary Cannot take Screenshot of Platform Views

然后上 Dart packages (flutter-io.cn) 试用了几个,不能说不能用,而是完全不能用。

最后还是决定自己写好了。

创建一个插件

flutter create -i objc -a java --org com.fluttercandies.plugins --template plugin --platforms ios,android ff_native_screenshot

使用 pigeon

添加引用

dev_dependencies:
  pigeon: ^3.1.0

增加生成类

  1. @HostApi() 标记的,是用于 Flutter 调用原生的方法。
  2. @FlutterApi() 标记的,是用于原生调用 Flutter 的方法。
  3. @async 如果原生的方法,是异步回调那种,你就可以使用这个标记
  4. 只支持 dart 的基础类型
/// Flutter call Native
@HostApi()
abstract class ScreenshotHostApi {
  @async
  Uint8List? takeScreenshot();

  void startListeningScreenshot();
  void stopListeningScreenshot();
}

/// Native call Flutter
@FlutterApi()
abstract class ScreenshotFlutterApi {
  void onTakeScreenshot(Uint8List? data);
}

命令脚本

我习惯放在脚本里面执行,免得每次都要手打。

执行 ./pigeon.sh 脚本内容如下:

flutter pub run pigeon \
  --input pigeons/ff_native_screenshot.dart \
  --dart_out lib/src/ff_native_screenshot.dart \
  --objc_header_out ios/Classes/ScreenshotApi.h \
  --objc_source_out ios/Classes/ScreenshotApi.m \
  --objc_prefix FLT \
  --java_out android/src/main/java/com/fluttercandies/plugins/ff_native_screenshot/ScreenshotApi.java \
  --java_package "com.fluttercandies.plugins.ff_native_screenshot"

iOS端实现

修改之前:

FfNativeScreenshotPlugin.h

#import <Flutter/Flutter.h>

**@interface** FfNativeScreenshotPlugin : NSObject<FlutterPlugin>

**@end**

FfNativeScreenshotPlugin.m

#import "FfNativeScreenshotPlugin.h"

**@implementation** FfNativeScreenshotPlugin

+ (**void**)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {

  FlutterMethodChannel* channel = [FlutterMethodChannel

      methodChannelWithName:@"ff_native_screenshot"

            binaryMessenger:[registrar messenger]];

  FfNativeScreenshotPlugin* instance = [[FfNativeScreenshotPlugin alloc] init];

  [registrar addMethodCallDelegate:instance channel:channel];

}


- (**void**)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {

  **if** ([@"getPlatformVersion" isEqualToString:call.method]) {

    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);

  } **else** {

    result(FlutterMethodNotImplemented);

  }

}

**@end**

ScreenshotHostApi

FfNativeScreenshotPlugin.h 增加继承 FLTScreenshotHostApi

#import <Flutter/Flutter.h>

#import "ScreenshotApi.h""


**@interface** FfNativeScreenshotPlugin : NSObject<FlutterPlugin,FLTScreenshotHostApi>

**@end**

FfNativeScreenshotPlugin.m

#import "FfNativeScreenshotPlugin.h"


**@implementation** FfNativeScreenshotPlugin

+ (**void**)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {

    FfNativeScreenshotPlugin* instance = [[FfNativeScreenshotPlugin alloc] init];

    FLTScreenshotHostApiSetup(registrar.messenger,instance);
}



- (**void**)startListeningScreenshotWithError:(FlutterError * **_Nullable** **__autoreleasing** * **_Nonnull**)error {

}



- (**void**)stopListeningScreenshotWithError:(FlutterError * **_Nullable** **__autoreleasing** * **_Nonnull**)error {

}



- (**void**)takeScreenshotWithCompletion:(**nonnull** **void** (^)(FlutterStandardTypedData * **_Nullable**, FlutterError * **_Nullable**))completion {

}


**@end**

ScreenshotFlutterApi

我们需要注册并且保存 ScreenshotFlutterApi,用于原生给 Flutter 发送消息

FfNativeScreenshotPlugin.m

#import "FfNativeScreenshotPlugin.h"

**static** FLTScreenshotFlutterApi *screenshotFlutterApi;

**@implementation** FfNativeScreenshotPlugin

+ (**void**)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    screenshotFlutterApi = [[FLTScreenshotFlutterApi alloc] initWithBinaryMessenger: registrar.messenger ];

}

**@end**

安卓端实现

修改之前:


public class FfNativeScreenshotPlugin implements FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private MethodChannel channel;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "ff_native_screenshot");
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else {
      result.notImplemented();
    }
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}

ScreenshotHostApi

我们需要将 MethodCallHandler 替换成 ScreenshotHostApi, 移除无用代码


public class FfNativeScreenshotPlugin implements FlutterPlugin, ScreenshotApi.ScreenshotHostApi {
  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    ScreenshotApi.ScreenshotHostApi.setup(flutterPluginBinding.getBinaryMessenger(), this);
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    ScreenshotApi.ScreenshotHostApi.setup(binding.getBinaryMessenger(), null);
  }

  @Override
  public void takeScreenshot(ScreenshotApi.Result<byte[]> result) {

  }

  @Override
  public void startListeningScreenshot() {

  }

  @Override
  public void stopListeningScreenshot() {

  }
}

ScreenshotFlutterApi

我们需要注册并且保存 ScreenshotFlutterApi,用于原生给 Flutter 发送消息

private ScreenshotApi.ScreenshotFlutterApi screenshotFlutterApi;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
  screenshotFlutterApi = new ScreenshotApi.ScreenshotFlutterApi(flutterPluginBinding.getBinaryMessenger());
}

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
  screenshotFlutterApi = null;
}

剩下的工作就是对这些方法进行实现,感谢 googlepub.dev上的各个插件大佬, 让我能学习(白嫖)到原生知识,也感谢群里大佬们对原生部分代码的检查和建议。

封装和使用

封装

我们可以做一些封装,将 lib/ff_native_screenshot.dart 修改为如下:

library ff_native_screenshot;

import 'dart:typed_data';
import 'src/ff_native_screenshot.dart';

export 'src/ff_native_screenshot.dart';

/// The util of NativeScreenshot
class FfNativeScreenshot {
  factory FfNativeScreenshot() => _ffNativeScreenshot;
  FfNativeScreenshot._();
  static final FfNativeScreenshot _ffNativeScreenshot = FfNativeScreenshot._();
  final ScreenshotHostApi _flutterScreenshotApi = ScreenshotHostApi();

  /// take screenshot by native
  Future<Uint8List?> takeScreenshot() => _flutterScreenshotApi.takeScreenshot();

  /// ScreenshotFlutterApi setup
  void setup(ScreenshotFlutterApi api) => ScreenshotFlutterApi.setup(api);

  bool _listening = false;

  /// whether is listening Screenshot
  bool get listening => _listening;

  /// start listening Screenshot
  void startListeningScreenshot() {
    _listening = true;
    _flutterScreenshotApi.startListeningScreenshot();
  }

  /// stop listening Screenshot
  void stopListeningScreenshot() {
    _listening = false;
    _flutterScreenshotApi.stopListeningScreenshot();
  }
}

使用

pubspec.yaml 中加入引入

dependencies:
  ff_native_screenshot: any

截图

Uint8List? data = await FfNativeScreenshot().takeScreenshot();

监听系统截图

我们需要去实现 ScreenshotFlutterApi,在 onTakeScreenshot 方法中获取到系统截图时候返回的字节流。


  @override
  void initState() {
    super.initState();
    FfNativeScreenshot().setup(ScreenshotFlutterApiImplements());
    FfNativeScreenshot().startListeningScreenshot();
  }

  @override
  void dispose() {
    FfNativeScreenshot().stopListeningScreenshot();
    super.dispose();
  }

  class ScreenshotFlutterApiImplements extends ScreenshotFlutterApi {
    ScreenshotFlutterApiImplements();
    @override
    Future<void> onTakeScreenshot(Uint8List? data) async {
     // if it has something error
     // you can call takeScreenshot 
     data ??= await FfNativeScreenshot().takeScreenshot();
    }
  }

结语

实际上,插件只要你第一次双端实现好了,之后想加方法,改方法,都是很方便的事情。比起来需要手写 if else, 我更喜欢pigeon 的多端接口一致性,对于不太懂原生的开发,还是很友好的。

当然,写一个 Flutter 的原生插件,你将需要掌握:

  • dart
  • java
  • object-c
  • kotlin
  • swift

别问为什么这么多,你在 google 上面搜索答案的时候就会发现,大佬们的答案往往是各种语言都有 o(╯□╰)o。

一次开发,5倍快乐,快来一起写 Flutter 原生插件吧!

ff_native_screenshot | Flutter Package (flutter-io.cn) 有需求的童鞋自取。

有关Flutter 用 pigeon 写一个原生插件的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  5. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

    我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

  6. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  7. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  8. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  9. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  10. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

随机推荐