Flutter桌面开发 - windows插件开发

通过此篇文章,你将了解到:

  1. Flutter插件的基本介绍;
  2. windows插件开发的真实踩坑经验。

⚠️本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

我们都知道,Flutter的定位更多是作为一个跨平台的UI框架,对于原生平台的功能,开发过程中经常需要插件来提供。不幸的是Windows的生态又极其不完整,插件开发必不可少。但网上windows的文章少之又少,所以本篇文章,我们一起来聊聊插件开发的一些技巧。

插件介绍

Flutter的插件主要分两种:package和plugin。

  • Package是纯dart代码的库,不涉及原生平台的代码;
  • Plugin是原生插件库,是一种特殊的Package。Plugin需要开发者分别在各原生平台实现对应的能力。

其中Plugin是我们要着重讲的,既然是原生平台实现,那跟dart层就势必需要通讯。Flutter Plugin的通讯主要有:methodChannel、eventChannel、basicMessageChannel。

  • MethodChannel:同步调用的通道,调用后可以通过result返回结果。可以 Native 端主动调用,也可以Flutter主动调用,属于双向通信。这种通信方式是我们日常开发中为最常用的方式, 关键点是Native 端的调用需要在主线程中执行
  • EventChannel:异步事件通知的通道,一般是Native端主动发出通知,Flutter接收通信信息。
  • BasicMessageChannel:长链接的通道,双端可以随时发出消息,对方收到消息后可以使用reply进行回复。一般常用于需要双向通信可不知道何时需要发送的场景。

windows插件编写

Flutter Android的生态算是比较完整的,而且网上95%的插件文章,都是以移动端为主,对于不熟悉Windows开发的同学极度不友好。因此本篇文章我们不讲Android端的实现,重点讲Windows端的实践,不过我也不是C++技术栈的,只能浅浅分享我踩过的坑。

  1. 如何创建通信通道?
// MethodChannel
void XXXPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrarWindows* registrar) {
        // 创建一个MethodChannel
    auto channel =
        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
            registrar->messenger(), "usb_tool",
            &flutter::StandardMethodCodec::GetInstance());
        // 创建插件对象
    auto plugin = std::make_unique<XXXPlugin>();
        // 把通道设置给插件,同时传入消息的处理入口
    channel->SetMethodCallHandler(
        [plugin_pointer = plugin.get()](const auto& call, auto result) {
        plugin_pointer->HandleMethodCall(call, std::move(result));
    });
}
// EventChannel

// 创建事件流处理对象
auto eventHandler = std::make_unique<
StreamHandlerFunctions<EncodableValue>>(
    [plugin_pointer = plugin.get()](
        const EncodableValue* arguments,
        std::unique_ptr<EventSink<EncodableValue>>&& events)
        -> std::unique_ptr<StreamHandlerError<EncodableValue>> {
            return plugin_pointer->OnListen(arguments, std::move(events));
    },
    [plugin_pointer = plugin.get()](const EncodableValue* arguments)
        -> std::unique_ptr<StreamHandlerError<EncodableValue>> {
            return plugin_pointer->OnCancel(arguments);
    });
// 创建EventChannel对象
auto eventChannel = std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
    registrar->messenger(), eventChannelName,
    &flutter::StandardMethodCodec::GetInstance());
// 把通道设置给插件
eventChannel->SetStreamHandler(std::move(eventHandler));

最后我们还需要把插件注册进项目中

registrar->AddPlugin(std::move(plugin));
  1. 如何处理消息? 在上面创建的过程中,其实已经把处理方法的传递给插件了。
// MethodChannel的处理
// result即通信的对象
void XXXPlugin::HandleMethodCall(
    const flutter::MethodCall<flutter::EncodableValue>& method_call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
        // 匹配通信的接口
    if (method_call.method_name().compare("getPlatformVersion") == 0) {
        std::ostringstream version_stream;
        version_stream << "Windows ";

        if (IsWindows10OrGreater()) {
            version_stream << "10+";
        }
        else if (IsWindows8OrGreater()) {
            version_stream << "8";
        }
        else if (IsWindows7OrGreater()) {
            version_stream << "7";
        }
                // 通过result->Succes回复消息
        result->Success(flutter::EncodableValue(version_stream.str()));
    } else {
        result->NotImplemented();

    }
}
// 主动向Flutter端发送消息
std::unique_ptr < flutter::StreamHandlerError<flutter::EncodableValue>> XXXPlugin::OnListen(const flutter::EncodableValue* arguments,
    std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events) {
    // 主动发送
    events_.reset(events.release());
    return nullptr;
}

// Flutter取消监听时触发
std::unique_ptr < flutter::StreamHandlerError<flutter::EncodableValue>> UsbToolPlugin::OnCancel(const flutter::EncodableValue* arguments) {
    return nullptr;
}

BasicMessageChannel我暂时还没有用过,这里就不做记录了。但是看C++的api,还是很简单就能找到的。至于Flutter端的,无需多言。只要通信层连通了,其他想怎么玩都可以。

Windows插件的一些坑

这是本篇文章的重点。我们都知道Flutter是单线程的机制,来到原生平台也一样,Platform是运行在Flutter的主线程的,自然是不能做任何耗时的,不然会卡住主线程,系统会把我们认为无响应的应用,从而杀死应用。

我们经常会在使用windows插件时,感觉点击卡顿,其实就是很多插件没有做这个处理,导致事件队列等待调度。这主要是因为在windows的开发习惯上,耗时操作会丢到子线程异步执行,然后主线程如何等待执行结果?使用while一直去查询是否执行完成,这在windows上成为挂起。

不过一个有趣的现象是:当有耗时操作的时候,Flutter的动画是可以流程播放的,但是点击事件却卡住了,这时候C++的同学就会扯,你看动画都是流程的,问题肯定出在Flutter上?其实是因为动画在Flutter中属于微任务,它的优先级是高于事件队列的。而while也是分配到事件队列中,所以动画优先执行,点击却需要一直等到while结束。

在Android中,为了避免这个问题,我们一般会使用协程,把耗时操作丢给协程,让系统帮我们进行任务调度,通过await拿到执行完之后的结果,再把结果返回给dart层。整个机制其实还是保留了flutter的单线程机制,从而避免了卡顿问题。

在Windows端,其实也有协程这个概念,比如WinRT、C++都有提供协程的能力。但问题在于协程这个东西,对于C++来说太新了,同时C++的历史包袱实在太重,到现在还是用着很老版本的库。这就导致很多C++的库没办法迁移到协程这种方式,至少在我现在的业务中,切换成本极高,几乎没办法完成。

但问题总得解决,目前我们主要使用异步通知的方式,来解决这个问题。此异步是真异步,非flutter单线程任务调度的异步。我们会把耗时的操作丢给子线程,但是我们不再通过while进行异步转同步,而是在子线程中,主动通过channel去通知会Dart层。

if (*method == "getAsync") {
            async_pipe_stream_->Get(request, std::bind(&XXXPlugin::OnResponse, this, std::placeholders::_1, *uuid));
            // 直接返回true,但真正的执行结果再OnResponse中主动返回
            result->Success(EncodableValue(true)); 
            return;
        }

在插件的dart代码中,我们需要主动创建一个MethodChannel的接收器,异步接收到后,通过执行业务端传入的回调通知回去。

class NativePlugin {
  static const MethodChannel _channel =
      MethodChannel('com.open.flutter/xxx/xxx');

  static NativePlugin? _instance;

  // 获取实例,单例
  static NativePlugin getInstance({String defaultToken = _token}) {
    _instance ??= NativePlugin._internal(defaultToken);
    return _instance!;
  }

  // 私有命名构造函数,做一次初始化
  NativePlugin._internal(String defaultToken) {
    _defaultToken = defaultToken;

    _channel.setMethodCallHandler((MethodCall call) async {
      if (call.method == 'onResponse') {
        final arguments = Map<String, dynamic>.from(call.arguments);
        // 执行业务端传入的回调
        await _onResponse(arguments);
      }
    });
  }

插件的Flutter层需要接收/维护回调列表,不过此方式有隐患,传入的回调容易造成闭包问题,增加一些内存泄露的风险;但是对于没办法使用协程的C++插件来说,此方案确实可以解决不少问题。亲测可用的!

写在最后

这篇文章,适合熟悉Flutter插件开发,但是想接触C++的同学学习讨论。
此专栏从窗口管理、分辨率适配、桌面小工具、项目框架、插件编写;下次我们讲讲如何进行打包!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容