跳到主要内容

Dart 平台适配

· 阅读需 8 分钟

本文主要探讨Dart平台相关的适配问题

  • 分平台导入与导出
  • 多平台适配

如何使用条件导入和导出来实现支持多个平台

通过查看Conditionally importing and exporting library files基本写法如下:

export 'src/hw_none.dart' // Stub implementation
if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
if (dart.library.js_interop) 'src/hw_web.dart'; // package:web implementation
  • 在可以使用 dart:io 的应用程序(例如命令行应用程序)中,导出 src/hw_io.dart
  • 在可以使用 dart:js_interop (Web 应用程序)的应用程序中,导出 src/hw_web.dart
  • 在其他情况下,导出 src/hw_none.dart 作为空实现,以防止编译错误。
提示
  • dart.library.io 表示当前平台是Dart VM的IO平台,即支持Dart VM的命令行、命令行参数、文件系统等功能。
  • dart.library.js_interop 表示当前平台是JavaScript平台,即支持Dart VM的JavaScript运行时。
  • 你或许会看到别的写法,比如dart.library.html 请修改为 dart.library.js_interop

基于条件导出的基本实现

实际案例:drift数据库实现跨平台

例如,在 src/hw_io.dart 中,我们可以定义一个 printMessage() 函数,该函数在命令行中输出一条消息:

void printMessage() {

print('Hello from IO platform');
}

src/hw_web.dart 中,我们可以定义一个 printMessage() 函数,该函数在 Web 页面中输出一条消息:

void printMessage() {

print('Hello from Web platform');
}

然后,我们在 lib/hw.dart 中导入这两个实现:

export 'src/hw_none.dart'
if (dart.library.io) 'src/hw_io.dart'
if (dart.library.js_interop) 'src/hw_web.dart';

最后,我们在 main() 函数中调用 printMessage() 函数,并传入不同的参数:

void main() {
printMessage();
}

基于条件导入的多态实现

参考这篇如何实现多平台导入适配

其核心思路是:

  • stub 实现,即在不支持的平台上,提供一个空实现。
  • 平台实现,即在支持的平台上,提供具体的实现。
  • 条件导入
import 'key_finder_stub.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'package:flutter_conditional_dependencies_example/storage/shared_pref_key_finder.dart'
// ignore: uri_does_not_exist
if (dart.library.html) 'package:flutter_conditional_dependencies_example/storage/web_key_finder.dart';

其基本要求是各自实现中需要同名类或者同名函数,然后通过条件导入,导入对应的实现。

在此基础上,我们可以进一步思考,是否可以将不同平台的实现分离,并通过一个统一的接口来访问,从而实现跨平台的功能。

由于Dart 并不存在接口,只存在抽象类,所以我们可以借助抽象类来实现。当然抽象类实现的本质也是借助于各自实现的同名“工厂函数”

step1: 创建接口

import 'key_finder_stub.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'package:flutter_conditional_dependencies_example/storage/shared_pref_key_finder.dart'
// ignore: uri_does_not_exist
if (dart.library.html) 'package:flutter_conditional_dependencies_example/storage/web_key_finder.dart';

abstract class KeyFinder {

// some generic methods to be exposed.

/// returns a value based on the key
String getKeyValue(String key) {
return "I am from the interface";
}

/// stores a key value pair in the respective storage.
void setKeyValue(String key, String value) {}

/// factory constructor to return the correct implementation.
factory KeyFinder() => getKeyFinder();
}

step2: web实现

import 'dart:html';

import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';

Window windowLoc;

class WebKeyFinder implements KeyFinder {

WebKeyFinder() {
windowLoc = window;
print("Widnow is initialized");
// storing something initially just to make sure it works. :)
windowLoc.localStorage["MyKey"] = "I am from web local storage";
}

String getKeyValue(String key) {
return windowLoc.localStorage[key];
}

void setKeyValue(String key, String value) {
windowLoc.localStorage[key] = value;
}
}

KeyFinder getKeyFinder() => WebKeyFinder();

step3: 原生实现

import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SharedPrefKeyFinder implements KeyFinder {
SharedPreferences _instance;

SharedPrefKeyFinder() {
SharedPreferences.getInstance().then((SharedPreferences instance) {
_instance = instance;
// Just initializing something so that it can be fetched.
_instance.setString("MyKey", "I am from Shared Preference");
});
}

String getKeyValue(String key) {
return _instance?.getString(key) ??
'shared preference is not yet initialized';
}

void setKeyValue(String key, String value) {
_instance?.setString(key, value);
}

}

KeyFinder getKeyFinder() => SharedPrefKeyFinder();

step4: 空实现

import 'key_finder_interface.dart';

KeyFinder getKeyFinder() => throw UnsupportedError(
'Cannot create a keyfinder without the packages dart:html or package:shared_preferences');

基于插件实现多平台适配

Flutter插件是一种扩展Flutter功能的机制。通过插件,你可以将自己的代码打包成可供其他开发者使用的库。

插件可以帮助你实现跨平台适配,例如,你可以编写一个插件,它可以帮助你实现不同平台的适配。

插件的基本结构如下:

  • 一个pubspec.yaml文件,用于定义插件的名称、版本、依赖等信息。
  • 一个lib/文件夹,用于存放插件的源代码。
  • 一个example/文件夹,用于存放插件的示例代码。
  • 一个android/文件夹,用于存放Android平台的实现。
  • 一个ios/文件夹,用于存放iOS平台的实现。
  • 一个macos/文件夹,用于存放macOS平台的实现。
  • 一个linux/文件夹,用于存放Linux平台的实现。
  • 一个windows/文件夹,用于存放Windows平台的实现。
// my_plugin.dart
import 'my_plugin_platform_interface.dart';

class MyPlugin {
Future<String?> getPlatformVersion() {
return MyPluginPlatform.instance.getPlatformVersion();
}
}

// my_plugin_platform_interface.dart
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'my_plugin_method_channel.dart';

abstract class MyPluginPlatform extends PlatformInterface {
/// Constructs a MyPluginPlatform.
MyPluginPlatform() : super(token: _token);

static final Object _token = Object();

static MyPluginPlatform _instance = MethodChannelMyPlugin();

/// The default instance of [MyPluginPlatform] to use.
///
/// Defaults to [MethodChannelMyPlugin].
static MyPluginPlatform get instance => _instance;

/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [MyPluginPlatform] when
/// they register themselves.
static set instance(MyPluginPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}

Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

// my_plugin_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'my_plugin_platform_interface.dart';

/// An implementation of [MyPluginPlatform] that uses method channels.
class MethodChannelMyPlugin extends MyPluginPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('my_plugin');

@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
// my_plugin_web.dart
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:web/web.dart' as web;

import 'my_plugin_platform_interface.dart';

/// A web implementation of the MyPluginPlatform of the MyPlugin plugin.
class MyPluginWeb extends MyPluginPlatform {
/// Constructs a MyPluginWeb
MyPluginWeb();

static void registerWith(Registrar registrar) {
MyPluginPlatform.instance = MyPluginWeb();
}

/// Returns a [String] containing the version of the platform.
@override
Future<String?> getPlatformVersion() async {
final version = web.window.navigator.userAgent;
return version;
}
}
name: my_plugin
description: "A new Flutter plugin project."
version: 0.0.1
homepage:

environment:
sdk: '>=3.4.1 <4.0.0'
flutter: '>=3.3.0'

dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
web: ^0.5.1
plugin_platform_interface: ^2.0.2

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
web:
pluginClass: MyPluginWeb
fileName: my_plugin_web.dart

# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware

# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

可以看到,基本实现是通过调用MyPluginPlatform.instance.getPlatformVersion()来实现具体的功能,而我们只需要将instance设置为不同的实现即可。 从web 实现中可以看出registerWith方法是替换instance的关键步骤。

plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
web:
pluginClass: MyPluginWeb
fileName: my_plugin_web.dart

有了初步的了解,我们可以继续深入探索插件的实现。


插件的实现方式有两种:

  • 基于平台的实现:插件可以提供不同的实现,例如,你可以提供一个Android实现和一个iOS实现。
  • 联合实现:插件可以提供一个通用实现,然后通过插件的依赖关系,将不同的实现提供给不同的平台。

基于平台的实现

基于平台的实现,即插件可以提供不同的实现,例如,你可以提供一个Android实现和一个iOS实现。

例如,你有一个插件,它可以帮助你实现不同平台的适配。

插件的pubspec.yaml文件如下:

name: platform_adapter
description: A new Flutter plugin project.
version: 0.0.1
author: Flutter Team <<EMAIL>>
homepage: https://flutter.dev

environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.20.0"


dependencies:
flutter:
sdk: flutter


dev_dependencies:
flutter_test:
sdk: flutter

endorsed federated implementations

Writing custom platform-specific code