likes
comments
collection
share

Flutter web兼容性处理总结

作者站长头像
站长
· 阅读数 57

本篇文章主要讲述Flutter web在实际落地的过程中遇到的兼容性问题以及解决方式,同时也会提及如何在编写Flutter插件时如何处理兼容性问题

Flutter是一个能够使代码跑在多个平台的跨端UI框架,甚至可以开发桌面客户端和web页面,但正是因为这是个跨端框架,在涉及底层实现的地方,往往会出现兼容性问题,其中Flutter web就是兼容性的“重灾区”,如果需要将现有的UI给复用到web,需要注意诸多问题,我把这些问题简单地分为了编译时兼容性问题运行时兼容性问题,当然,在众多Flutter实践中,通常是推荐把这种底层实现给抽象成插件,再把接口暴露给Flutter层,在开发UI时抹除平台的底层实现差异,然而提取成插件也是需要处理兼容性的细节,所以也需要一套跨端插件的最佳实践

一、编译时兼容性问题

编译时产生的兼容性问题主要指的是引入依赖导致的报错,运行一个没有考虑web兼容性的Flutter项目,往往有可能出现以下类似的报错:

[ +282 ms] ../../../.pub-cache/hosted/pub.dartlang.org/flutter_osk-0.1.0/lib/windows_osk.dart:1:8: Error: Not found: 'dart:ffi'
[        ] import 'dart:ffi';
[        ]        ^
[+1082 ms] ../../../.pub-cache/hosted/pub.dartlang.org/win32-2.5.0/lib/src/callbacks.dart:9:8: Error: Not found: 'dart:ffi'
[        ] import 'dart:ffi';
[        ]        ^

这种报错指的是在web中是找不到dart:ffi这个包的,因为web不能使用ffi,造成这种报错的原因一般是项目的某些依赖没有对web和其他native平台做好实现隔离,直接粗暴的引入了ffi,导致编译时的报错,直接影响了项目的运行.

二、运行时兼容性问题

运行时的兼容性问题主要指的是dart:iodart:isolate这些库,在Flutter web项目中引入这些库,在编译期间通常不会出现问题,但是一旦运行到对应语句,就会出现报错,比如:

ErrorUnsupported operation: _Namespace

通常,碰到这种报错,就是因为引入dart:io,并使用了其中与web不兼容的用法,比如FilePlatform.isWindows等等,

对于Platform判断造成的报错,可以采取以下简单的方法来解决

// Bad
import 'dart:io';
if (Platform.isWindows) // ❌ Error: Unsupported operation: _Namespace

// Good
import 'dart:io';
import 'package:flutter/foundation.dart';

if (!kIsWeb && Platform.isWindows) // ✅

通过这种写法,如果在web环境下运行到kIsWeb就不会再往后执行到Platform.isWindows,避免了运行时的报错

三、跨端插件的最佳实践

那么该如何解决以上两种常见的兼容性错误呢?最好的方法当然是将跟平台实现有关的部分提取成插件,一般来说,涉及io、网络、硬件信息和权限等部分的话就需要提取成插件,当然,如果你的实现非常简单,感觉没必要提取成插件,那么可以试试条件导入隔离来将你的实现抽离到业务代码的某处。接下来将介绍跨端插件最佳实践的三种方式:Conditional ImportPlatform ChannelFederated Plugin

3.1 Conditional Import - 条件导入隔离

对于引入依赖导致的编译型报错该如何解决呢?官方给出了对应的解决方案,也就是conditional import 条件导入,如果你的库需要支持web以及native平台,在库的入口文件中最好是类似下图的导出写法

// lib/hw_mp.dart
export 'src/hw_none.dart' // stub implementation
  if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
  if (dart.library.html) 'src/hw_html.dart'; // dart:html implementation

上述代码的意思是:

  • 如果在可以使用dart:io的应用中(比如命令行工具),则导出src/hw_io.dart
  • 如果在可以使用dart:html的应用中(比如web app),则导出src/hw_html.dart
  • 否则,则导出src/hw_none.dart

hw_none.dart中需要定义好默认方法的实现,抛出平台未实现的错误

// lib/src/hw_none.dart

void alarm([String? text]) => throw UnsupportedError('hw_none alarm');

String get message => throw UnsupportedError('hw_none message');

hw_iohw_html中需要实现对应方法,需要注意的是,hw_iohw_htmlhw_none需要对外暴露一样的接口,保证项目不管在什么平台下都能使用里面的所有方法,避免编译报错。

// lib/src/hw_io.dart 

import 'dart:io';

void alarm([String? text]) {
  stderr.writeln(text ?? message);
}

String get message => 'Hello World from the VM!';
// lib/src/hw_html.dart 

import 'dart:js';

void alarm([String? text]) {
  context['console'].callMethod('log', ['alarm', text]);
}

String get message => 'Hello World from the Web!';

通过条件导入编写好库后,在项目中就可以直接引用入口文件开始使用

import 'package:hw_mp/hw_mp.dart';

void main() {
  print(message);
}

3.2 Platform Channel - 通道通信

如果在Dart层已经不能满足一定的需求时,那么基本上是需要在Native层做一些处理,甚至需要编写原生native代码,为了使得Native代码能跟Dart代码交互,Flutter提供了Platform channel的方式来通信 Flutter web兼容性处理总结 在Flutter层,我们需要以下这种方式去调用对应注册名的通道,得到通道的实例,再通过

class _MyHomePageState extends State<MyHomePage> {
  // 通过通道名获取对应的platform实例
  static const platform = MethodChannel('samples.flutter.dev/battery');
  // Get battery level.
// Get battery level.
String _batteryLevel = 'Unknown battery level.';

Future<void> _getBatteryLevel() async {
  String batteryLevel;
  try {
    // 通过platform实例调用对应的方法
    final int result = await platform.invokeMethod('getBatteryLevel');
    batteryLevel = 'Battery level at $result % .';
  } on PlatformException catch (e) {
    batteryLevel = "Failed to get battery level: '${e.message}'.";
  }

  setState(() {
    _batteryLevel = batteryLevel;
  });
}

但是,在获取platform实例和调用方法之前,我们都需要在对应native平台代码中去注册对应的方法的,在Flutter的官方文档中对于Android的platform注册ios的platform注册都有详细的说明,这里也就不在赘述了

但是如果以上过程都用自己手写的方式来实现的话,未免太过麻烦,所以Flutter官方提供了一系列方便的cli命令来创建插件并且给插件添加对应平台的实现代码

// 通过-a能指定对应平台的实现语言
flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello

// 通过以下命令可以给现有项目添加web的支持
flutter create --template=plugin --platforms=web .

3.3 Federated Plugin - 联邦插件

但是以上的方式虽然都能很好的实现对应平台的逻辑并且做到一定程度的实现隔离,但一个插件总是会存在多人协作开发的情况,如果某个开发人员在某次开发时没有考虑好兼容性,代码隔离没做好,就有可能再次导致报错,所以为了避免这种情况,官方推荐使用Federated Plugin 联邦插件的方式来编写插件。

那么,什么是联邦插件呢?官方的url_launcher就是很好的实践范例,整个项目的根路径文件结构就是一个分package的形式,一个主入口的package和多个对应平台的package

├── url_launcher // 统一入口
├── url_launcher_android // android实现
├── url_launcher_ios // ios实现
├── url_launcher_platform_interface // 平台接口统一实现(一般采用methodChannel先统一写invoke)
├── url_launcher_web // web实现
└── url_launcher_linux // linux实现
└── url_launcher_macos // macos实现

业务项目里引用的就是主入口的package,然后再通过主入口的yaml来让项目知道对应平台应该引入哪种package

# pubspec.yaml

name: url_launcher
# ..

environment:
  sdk: ">=2.14.0 <3.0.0"
  flutter: ">=2.8.0"

flutter:
  plugin:
    platforms:
      android:
        default_package: url_launcher_android
      ios:
        default_package: url_launcher_ios
      linux:
        default_package: url_launcher_linux
      macos:
        default_package: url_launcher_macos
      web:
        default_package: url_launcher_web
      windows:
        default_package: url_launcher_windows

这种联邦插件的写法相比Conditional Import条件导入分的粒度要更加的细,隔离的更加彻底,总结的好处主要以下几点:

  • 隔离各平台实现,适合多人协作的复杂插件,各平台人员各司其职
  • 通过platform_interface规范各平台插件的API,并且还有methodChannel兜底未实现的平台
  • 各平台可以复用对应平台的依赖,而不用担心依赖导入不兼容的问题

但这种联邦插件的多package结构如果要自己手动搭建,未免过于麻烦,所以这里我推荐使用very_good_cli的命令来直接搭建联邦插件

Create a new Flutter plugin named my_flutter_plugin (all platforms enabled)
very_good create my_flutter_plugin -t flutter_plugin --desc "My new Flutter plugin"Create a new Flutter plugin named my_flutter_plugin (some platforms disabled)
very_good create my_flutter_plugin -t flutter_plugin --desc "My new Flutter plugin" --windows false

当然,这个cli工具也可以搭建app、cli、package等Flutter项目,并且搭出来的项目都已经配置好了常用的依赖,相当的方便。

但需要注意的是,搭出来的较为规范的联邦插件往往是一个平台一个package,这往往会出现代码冗余的情况,比如android和ios可能都会直接使用Platform channel来通信,代码几乎一模一样。碰到这种情况,可以自己手动调整插件的文件结构,来保证项目足够精简,比如可以把_ios_android合并成_mobile这个package,再在yaml里调整ios和android的default_package即可

总结

跨端框架虽然好,但跨端就意味着需要处理好兼容性,不然跨了还不如不跨,不过只要我们有足够的分层意识,让UI和底层逻辑分离,在底层逻辑处理好兼容性,那么业务代码里其实也就只需要处理好业务逻辑即可,如果业务代码里充斥着对各种平台的判断,或许也就该好好审视下项目的分层结构了

转载自:https://juejin.cn/post/7135737121863630879
评论
请登录