跳到主要内容

4 篇博文 含有标签「dart」

查看所有标签

利用Stream实现Flutter的组件解耦

· 阅读需 5 分钟

Stream 是什么?

本质上,Stream 是一个异步数据队列,具有先进先出(First In First Out,FIFO)的特性。借助它,组件之间可以彻底解耦——数据生产者只管往流里推数据,消费者只关心如何订阅,两者互不感知,数据流动因此更加灵活、可控。

Stream 和 Future 的区别

简单说:Future 只返回一次结果,Stream 可以连续返回多个结果。

Stream 的分类

Stream 分为两种:

  • 单订阅流(single-subscription):只能有一个订阅者,也就是只能有一个消费者。
  • 多订阅流(broadcast):可以有多个订阅者,也就是可以有多个消费者。用 StreamController.broadcast() 创建,或对已有的流调用 .asBroadcastStream() 转换。

Stream 的使用

单订阅流的构造

import 'dart:async';

void main() {
createSingleStream();
}

Future<void> createSingleStream() async {
// 1) Stream.periodic:每隔一秒产生一个递增整数。
// 它是无限流,永远不会自己结束,这里用 take(3) 截取前三个。
final Stream<int> periodic = Stream<int>.periodic(
const Duration(seconds: 1),
(i) => i,
).take(3);

// 2) Stream.fromFuture:把一个 Future 包装成 Stream
final Future<int> future = Future.delayed(const Duration(seconds: 1), () => 1);
final Stream<int> fromFuture = Stream<int>.fromFuture(future);

// 3) Stream.fromFutures:把多个 Future 的结果依次推入同一个 Stream
final Future<String> hello = Future.delayed(const Duration(seconds: 1), () => 'Hello');
final Future<String> world = Future.delayed(const Duration(seconds: 2), () => 'World');
final Stream<String> fromFutures = Stream.fromFutures([hello, world]);

// 4) Stream.fromIterable + Stream.merge:把两个有限流合并成一条
final Stream<int> left = Stream.fromIterable([1, 2, 3]);
final Stream<int> right = Stream.fromIterable([4, 5, 6]);
final Stream<int> merged = Stream.merge([left, right]);

// await for 按到达顺序逐个取出合并后的数据并打印
await for (final int i in merged) {
print(i);
}
}
为什么使用 await for?

await for 是 Dart 2.0 引入的异步迭代语法:它从 Stream 里逐个取数据,取到一次就执行一次循环体;没有数据时就挂起等待,数据到达才继续。它把「订阅 → 收数据 → 处理 → 完成」的整个过程,写成一段像普通 for 循环一样线性的代码。

把它和基于回调的 listen() 对比更好理解:

  • stream.listen(onData):数据到达时触发回调,回调里再套回调,逻辑一多就不太好读。
  • await for:把同样的逻辑展开成顺序代码,读起来、改起来都更直观。底层依旧是非阻塞的,等待数据时不会卡住 UI 线程。

一个容易踩的坑:Stream 上的 error 事件不会被 await for 吞掉,而是作为异常重新抛出。需要处理错误时,用 try/catch 包住循环体捕获即可。

Stream 的基本使用流程

一个 Stream 的完整生命周期分三步:

  1. 创建 Stream:用 StreamController() 创建一个可控制的流,或用 Stream.periodic()Stream.fromIterable() 等从已有数据直接构造。
  2. 订阅 Stream:用 Stream.listen() 监听数据;在 Flutter 里则常把流交给 StreamBuilder,让它在数据到达时自动重建 UI。
  3. 发布数据:通过 StreamController.sink.add(data) 往流里推数据,所有订阅者都会收到并作出响应。

综合示例:CounterBloc + StreamBuilder

// 数据源:对外暴露只读 stream,通过 sink 发布数据
class CounterBloc {
final _controller = StreamController<int>();

Stream<int> get stream => _controller.stream;

void increment() => _controller.sink.add(1);

void dispose() => _controller.close();
}
// 页面:用 StreamBuilder 订阅流,数据到达时自动重建
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = CounterBloc();

return Scaffold(
appBar: AppBar(title: const Text('Counter Page')),
body: StreamBuilder<int>(
stream: bloc.stream,
builder: (context, snapshot) {
return Text(
snapshot.hasData ? '${snapshot.data}' : 'Waiting for data...',
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: bloc.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

这个例子把前面的三步串了起来:CounterBloc 内部用一个私有的 StreamController<int> 作为数据源,对外暴露只读的 stream getter;increment() 通过 sink.add(1) 发布数据。CounterPageStreamBuilder 订阅 bloc.stream——每当数据到达,builder 就会拿最新的 snapshot 重新构建 UI,计数立刻刷新;右下角的按钮点击后触发 increment(),完成一次「发布 → 订阅 → 更新」的闭环。

简化起见,示例把 CounterBloc 直接建在了 build() 里;真实项目中应由外层管理它的生命周期(例如放进 StatefulWidget 或依赖注入框架),并在页面销毁时调用 dispose() 关闭 StreamController,避免资源泄漏。

总结

Stream 是 Flutter 中非常重要的异步编程工具:生产者只管发布数据,消费者只关心订阅,两者互不感知,组件因此实现了解耦。利用这一特性,可以让代码更模块化、状态流转更可控——消息通知、收藏列表、表单校验这类「一处产生、多处响应」的场景,都是 Stream 的用武之地。

Flutter Web Service Worker

· 阅读需 3 分钟

Flutter Web Service Worker

在官方文档中,Flutter Web Service Worker的介绍如下:

Service workers are a new feature in web browsers that enable developers to create reliable and fast web applications. Service workers can intercept and handle network requests, cache responses, and manage push notifications. Service workers can also be used to create progressive web applications (PWA) that work offline and provide a seamless user experience.

Flutter Web supports Service workers, which can be used to cache data and provide a seamless user experience. This can help improve the performance of your web application and provide a better user experience.

To use Service workers in your Flutter Web application, you need to follow these steps:

  1. Enable Service worker support in your Flutter Web application.
  2. Define a Service worker file.
  3. Register the Service worker file in your index.html file.
  4. Use the Service worker to cache data and provide a seamless user experience.

Enable Service worker support in your Flutter Web application

To enable Service worker support in your Flutter Web application, you need to add the following code to your pubspec.yaml file:

flutter:
assets:
- assets/

Define a Service worker file

To define a Service worker file, you need to create a new file named service_worker.dart in the lib folder of your Flutter Web application.

Here's an example of a Service worker file:

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

void main() {
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
'flutter_web_worker',
(int viewId) => new ServiceWorkerController(),
);
}

class ServiceWorkerController {
ServiceWorkerController() {
print('Service worker initialized.');
}

Future<void> fetchHandler(FetchEvent event) async {
// Handle fetch events
}

Future<void> messageHandler(MessageEvent event) async {
// Handle message events
}

Future<void> pushHandler(PushEvent event) async {
// Handle push events
}

Future<void> syncHandler(SyncEvent event) async {
// Handle sync events
}
}

In this example, we define a ServiceWorkerController class that handles different types of events. For example, the fetchHandler method is called when a fetch event is triggered, and it can be used to handle network requests.

Register the Service worker file in your index.html file

To register the Service worker file in your index.html file, you need to add the following code to the <head> section of your index.html file:

<script type="application/javascript" src="service_worker.dart.js"></script>

Make sure to replace service_worker.dart.js with the actual name of your Service worker file.

Use the Service worker to cache data and provide a seamless user experience

To use the Service worker to cache data and provide a seamless user experience, you need to call the appropriate methods in your code.

For example, to cache data, you can use the cache.addAll method:

Future<void> cacheData() async {
final cache = await caches.open('my-cache');
await cache.addAll([
'https://example.com/data.json',
'https://example.com/images/logo.png',
]);
}

To provide a seamless user experience, you can use the skipWaiting method:

Future<void> updateUserExperience() async {
final controller = ServiceWorkerController();
await controller.skipWaiting();
}

In this example, we call the skipWaiting method on the ServiceWorkerController class to activate the new version of the Service worker. This ensures that the new version of the Service worker takes over and handles all future requests.

在二级域名下,service worker失效的问题

dart与js的互操作(一)

· 阅读需 1 分钟

https://dart.dev/interop/js-interop/past-js-interop


@js.JSExport()
class _MyAppState extends State<MyApp> {
final _streamController = StreamController<void>.broadcast();
DemoScreen _currentDemoScreen = DemoScreen.counter;
int _counterScreenCount = 0;

@override
void initState() {
super.initState();
final export = js.createJSInteropWrapper(this);
js.globalContext['_appState'] = export;
js.globalContext.callMethod('_stateSet'.toJS); // 调用js方法,将Dart实例化
}

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