Dart 平台适配
本文主要探讨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