直接开局。上一篇我们梳理了 Dart 的基础语法、类与对象、以及一部分函数式编程的玩法,那些是搭地基。这一篇要往上盖楼,把所有能让你写出高效、健壮、可维护代码的 Dart 特性全翻出来:异步、并发、Mixin、泛型、Extension、流式处理、空安全进阶、以及面向 Flutter for OpenHarmony 开发时的实战组合技。
先说个背景,我现在做 Flutter for OpenHarmony 适配相关的工作,Dart 是躲不开的主语言。虽然 Flutter 框架本身替你封装了绝大部分 UI 和状态管理,但一旦遇到网络请求、文件读写、大量图片解码、复杂状态共享这些场景,Dart 的高级特性就直接决定了你的 App 是流畅还是卡顿、代码是能维护还是变成屎山。这篇笔记里的每段代码,都来自我实际调试过的某跨平台消息应用,有踩坑记录,有取舍理由,照着敲一遍、改一改,你会比单独看官方文档记得牢得多。
1. 异步编程:从 Future 到 Stream 的实战理解
Dart 是单线程模型,但这不代表它不能处理并发。它靠的是一个事件循环(Event Loop)加上异步 API,用同步的写法去写非阻塞代码。很多人刚学 Dart 时被 async / await 绕晕,本质上是因为没搞懂事件循环的执行顺序。
1.1 事件循环与 Future 的执行顺序
先看这段代码,试试你能不能写出打印顺序:
void main() { print('A'); Future(() => print('B')); Future.microtask(() => print('C')); scheduleMicrotask(() => print('D')); Future.sync(() => print('E')); print('F'); }我第一次跑这个程序时,预期是 A、F、E、C、D、B,结果实际输出是 A、F、E、D、C、B。原因在于事件循环的队列优先级:
- 微任务队列(microtask queue)优先级高于事件队列(event queue)。
- Future 构造函数创建的 Future 默认走事件队列,但 Future.microtask 和 scheduleMicrotask 走微任务队列。
- Future.sync 会在当前同步代码执行阶段立刻执行,所以 E 排在 C、D 之前。
这个执行顺序不是考试题,它直接影响 UI 响应。比如你在 Flutter for OpenHarmony 上做一个图片懒加载,如果用了多条Future(...)直接把任务全部丢进事件队列,用户快速滑动时会产生大量待执行的解码任务,表现就是列表掉帧。而把低优先级任务放到微任务里也不对,因为微任务会阻塞后续事件的处理。这两个队列必须配合使用,读官方文档时多花十分钟搞清楚这部分,后面排查诡异 Bug 会省下大把时间。
1.2 Stream 与 async*:处理连续事件的核心姿势
Future 适合一次性异步结果,Stream 则适合连续的数据流。在 Flutter 里,Stream 的身影随处可见:手势事件、数据库查询结果、WebSocket 消息、文件读取进度。
写 Stream 最爽的姿势是 async*,它能够让你用同步代码风格生成异步序列:
Stream<int> countDown(int from) async* { for (var i = from; i >= 0; i--) { yield i; } } void main() async { await for (final value in countDown(5)) { print(value); // 5 4 3 2 1 0 } }yield 关键字跟 return 有点像,但区别在于它会保留函数状态,下次调用时继续执行。这个特性在实现“加载更多”下拉分页时非常实用:你可以把一页页的数据拉取逻辑写成一个 async* 的 Stream,然后用 await for 消费,代码比回调嵌套好读一百倍。
踩过的坑:Stream 默认是单订阅的,也就是说同一个 Stream 不能被两个地方同时监听。如果你在 UI 层和业务层各监听了一次,第二次监听会直接抛异常。解决办法是用StreamController.broadcast()创建广播流,或者用asBroadcastStream()转换。我做消息推送模块时,WebSocket 消息要同时更新未读角标和消息列表,就是靠 broadcast 流解决的。
还有一个容易被忽略的点:Stream 是需要关闭的。StreamController用完后记得调用close(),否则资源一直挂着。这个在桌面端或内存敏感的 IoT 设备上特别明显,我在某跨平台系统的日志收集模块里跑过长时间连接,忘关 Stream 导致内存涨了大概 30%,排查了很久才定位到。
2. Isolate:绕过单线程限制的正确姿势
前面说 Dart 是单线程的,但这不等于它只能用一个 CPU 核心。Isolate 是 Dart 的并发模型,每个 Isolate 有独立的内存和事件循环,真正做到并行计算。Flutter 里的compute()函数就是基于 Isolate 封装出来的简易工具。
2.1 创建 Isolate 与消息通信机制
创建 Isolate 使用Isolate.spawn,通信靠 SendPort / ReceivePort。下面是一个标准例子:
import 'dart:isolate'; void heavyWork(SendPort sendPort) { var result = 0; for (var i = 0; i < 100000000; i++) { result += i; } sendPort.send(result); } void main() async { final receivePort = ReceivePort(); final isolate = await Isolate.spawn(heavyWork, receivePort.sendPort); final result = await receivePort.first; print('计算结果: $result'); isolate.kill(priority: Isolate.immediate); }关键点:传给Isolate.spawn的参数必须是可发送的,基本类型、List、Map、SendPort 都可以,但不能直接传一个闭包或包含闭包的对象。如果非要传复杂对象,你需要自己做序列化,最省事的方式是拆成多个参数,或者用 JSON 字符串打包。这在 Flutter for OpenHarmony 上写 JSON 解析模块时很常见:大 JSON 解析不能放主 Isolate,丢给后台 Isolate 解析完只把 Map 发回来。
内存隔离带来的好处是:没有共享内存,就不会出现多线程编程里的竞态条件和锁问题。代价是:通信有成本,每次 SendPort.send 都是一次复制(对象需要跨 Isolate 传输时会被拷贝)。所以不要为了几毫秒的小计算去开 Isolate,开销可能比计算本身还大。
2.2 Flutter 里的 compute 函数与它的局限
Flutter 提供了compute函数,它在底层自动创建 Isolate、执行任务、返回结果、销毁 Isolate,用起来非常简洁:
final result = await compute(expensiveWork, input); String expensiveWork(String input) { // 模拟耗时操作,比如图像处理、JSON 解码、大列表排序 return input.toUpperCase(); }强烈建议所有的耗时操作都丢到compute里。Flutter for OpenHarmony 的 UI 渲染和 Dart 事件循环共享主 Isolate,如果你在主 Isolate 里做超过 16ms 的同步计算,连续两帧之间就会产生卡顿,用户感知非常明显。
但 compute 有两个坑:第一,它每次都会新建 Isolate,任务结束后销毁,频繁调用会反复付出 Isolate 启动和销毁的开销。如果你有一批任务要处理,最好自己维护一个 Isolate 池,或者至少用Isolate.run()替代,它是近几个版本提供的轻量级工厂函数,底层照样是 Isolate,但写起来更简短:
final result = await Isolate.run(() { // 耗时任务 return 42; });第二,compute 的参数和返回值也受到可发送限制,不能传闭包或不可序列化对象。实际开发中我通常把所有需要的数据整理成 Map 或 String 再传进去,返回值也是简单类型,这样最稳妥。
这部分的思考顺序应该是:先在主 Isolate 测试任务性能,确认超过阈值再决定是否引入 Isolate。如果你在 Flutter 里上来就把所有计算都发到 Isolate,代码复杂度会指数级上升,后期不好维护。我在消息搜索功能里就犯过这个错误,本来一个简单的过滤操作,硬是搞成了 ReceivePort 管理,后来发现列表过滤根本不到 1ms,纯属自找麻烦,又改回主 Isolate 直接跑。
3. Mixin 与扩展机制:代码复用的最高效手段
Dart 是单继承语言,但提供了 Mixin 这种水平复用的方案。这是我从 Java 转过来时最爽的一个特性:它能在不改变继承结构的情况下,把可复用的行为注入到类里,比接口的默认方法好用得多。
3.1 Mixin 的约束与冲突处理
Mixin 的简单定义方式:
mixin Loggable { String tag = 'Loggable'; void log(String message) { print('[$tag] $message'); } } class NetworkService with Loggable { void fetchData() { log('开始请求'); } }with关键字表示把 Loggable 里的成员混合进 NetworkService。注意,Mixin 可以声明字段,这是接口做不到的,因为接口不能持有实例状态。
Mixin 还有一种高级玩法:用on关键字限制它的使用范围。比如你希望某个 Mixin 只能混入特定的父类:
class Animal { void breathe() => print('呼吸'); } mixin Walker on Animal { void walk() => breathe(); } class Dog extends Animal with Walker { }这里 Walker 只能在 Animal 的子类中使用,它可以安全调用 Animal 的方法。这在维护大型业务模块时特别有用,它能强制性地保证 Mixin 的上下文环境,避免你把一个依赖特定状态的 Mixin 混入到完全无关的类里。
冲突处理:如果多个 Mixin 和类都有同名方法,调用顺序取决于with关键字后的声明顺序,后面的会覆盖前面的。我刚开始使用时不理解这一点,导致日志打到了错误的对象上。建议规则是:把最通用的 Mixin 放最前面,最具体的放最后面,这样具体行为的优先级高。
3.2 Extension:不修改原类也能添加方法
Extension 是另一个强大的特性,它能给已有类型动态扩展方法,包括第三方库的类。
extension StringDateExtension on String { DateTime toDate() => DateTime.parse(this); } void main() { final date = '2024-06-01'.toDate(); print(date.year); }这段代码给 String 类扩展了 toDate 方法,直接调用就行。在 Flutter for OpenHarmony 的国际化模块里,我写了不少 Extension 来处理日期格式化、货币符号、复数形式,避免到处散落静态函数,代码组织清晰了很多。
但注意,Extension 是静态解析的,不是动态分发。什么意思?当它和类型本身的同名方法冲突时,以类型自身的方法为准,即便调用上下文已经导入 Extension。另外,Extension 不能访问私有成员,所以它本质上只是语法糖,无法突破封装边界。
还有一个隐藏的坑:同名 Extension 方法一旦都导入到同一个文件,调用时会直接编译报错(ambiguous import)。我在某个业务模块里导入了两个工具库,都定义了toInt()的 Extension,排查了半天才找到原因。现在我的习惯是:一个 Dart 文件里尽量少用package:xxx这种整体导入,多用import as别名,或者干脆用show/hide精确控制导入内容。
3.3 泛型方法:为静态检查留一条后路
Dart 的泛型不只是集合的标配,它也能用于方法级约束。下面这个例子展示了如何在方法上保持类型安全:
T? firstWhereOrNull<T>(List<T> items, bool Function(T) test) { for (final item in items) { if (test(item)) return item; } return null; } void main() { final numbers = [1, 2, 3]; final found = firstWhereOrNull(numbers, (v) => v > 2); print(found); // 3 }这里 T 代替任何类型,返回值 T? 意味着可能为 null,参数bool Function(T)让外部传入的 lambda 参数类型自动匹配泛型。如果你不用泛型,而是写成bool Function(dynamic),那在集合内部就容易出现类型转换错误,等到运行时才暴露。泛型方法的作用,就是把这些错误提前到编译期。
另外,Dart 泛型还有一个细节:运行时会保留泛型类型,也就是说你可以用is检查泛型参数。这一点在序列化和反序列化场景里很关键。我在做某跨平台系统的持久化层时,所有模型都实现了fromJson映射,但读取时只能用as强转才能获得具体类型。运行时泛型的存在意味着你可以写一个类型安全的 Repository ,让读取结果自动转型,省掉多数类型转换异常。
4. Null Safety 进阶:从入门的 ? 到完整的类型系统理解
Dart 的空安全不是给变量加个问号那么简单,它是一个完整的类型系统。我见过太多的慎用者因为在空安全上理解不到位,写出了大量!感叹号,动不动就炸。这一节讲几个实战中真正需要的点。
4.1 可空类型、late 与延迟初始化
late关键字是空安全里最容易误解的成员之一。它声明的变量可以在声明时不赋值,但访问时会强制检查,如果没有赋值就直接抛错。
class Config { late final String apiKey; void load() { apiKey = '从配置读取'; } void use() { print(apiKey); // 如果 load 没被调用,这里会抛出 LateInitializationError } }late final适合那些一定会在使用前赋值的字段,但它天然具备一种"信任"的假设,一旦破坏,错误比普通空值传播得更快。这里的关键是:如果你无法确定一定会在使用前赋值,就用普通可空类型加空判断,不要用 late 逃跑。我在实现主题配置模块时,late final ThemeData就因为异步加载时机问题,偶尔会炸一下,后来改成ThemeData?并在 UI 层做了空处理,问题立刻消失。
还有一个细节:可空字段默认值是 null,但是 Dart 内部会对非空字段增加一个 sentinel 值,以便运行时检查是否赋值。这导致冷启动时未初始化非空字段的对象会比可空对象占用稍多一点空间,不过一般不会在你需要关心的量级上。
4.2 类型提升与流分析的边界
Dart 编译器会做类型提升(type promotion),它知道在哪些代码范围内一个可空变量一定是非空的。比如:
void printLength(String? text) { if (text != null) { print(text.length); // 这里编译器自动把 text 提升为 String } }前提是变量要是局部的、非捕获的、没有被闭包修改的。一旦你把这个变量放进闭包里,编译器会放弃提升。下面这个例子就编译不过:
void foo(String? text) { final list = [1, 2]; list.forEach((_) { // 编译器认为 text 可能被闭包里修改,因此不可提升 // print(text.length); // 报错:text 可能为 null }); }我在调试列表过滤时遇到过这个编译错误,一开始以为是编译器 bug,后来才知道是流分析的限制。解决方法很简单:在闭包外先把非空值赋给一个新局部变量:
void foo(String? text) { final nonNullText = text; if (nonNullText == null) return; final list = [1, 2]; list.forEach((_) { print(nonNullText.length); // 安全 }); }这个习惯也可以培养成防御性编程的标准姿势:只要看到闭包里要访问外层可空变量,就先断言非空后赋值给不可空局部变量。
4.3 模式匹配与空安全的组合
Dart 3 引入了真正的模式匹配,这跟 switch 表达式、解构、空检查组合起来,能写出比传统 if-else 更精炼的代码:
String describe(Object? value) { return switch (value) { null => '空值', int v => '整数 $v', String s => '字符串 $s', _ => '未知类型', }; }这里switch后面的 case 虽然是比对,但它不是简单的 if-else,它会结合运行时类型和值一起匹配,并且可以做对象解构。最惊艳的是,在 switch 语句里其实也会触发类型提升,所以 case 分支里可以直接使用绑定变量。
模式匹配还有一个常用场景:ListView 的 item 类型分派。我在 Flutter for OpenHarmony 的消息列表里,同时有文本消息、图片消息、系统通知三类模型,用 switch 表达式加类型模式一次性展开,替代了原先的三层 if-else,代码量和出错率都显著下降。只要你能保证每个分支返回同一种 UI 组件类型,它比继承和多态直白得多。
5. async / await 高级模式:并发限制、超时与取消
优秀的 Flutter 开发者写异步代码时,不会仅仅用 async / await 顺序执行,他们会设计并发策略。下面这些套路我在真实项目里用了很多次,简单但非常有用。
5.1 用 Future.wait 控制并发批处理
当你有多个相互独立的异步任务时,不要一个接一个地 await,应该用 Future.wait 或 Future.any:
final results = await Future.wait([ fetchUserProfile(), fetchUserMessages(), fetchUserSettings(), ]);Future.wait 会让三个请求同时发出,比顺序请求节省近三分之二的时间。但也别过度使用:如果并发数太多,比如一次性发起 50 个网络请求,会对服务端造成压力,在弱网环境下反而更慢。更稳妥的做法是做一个简单的并发限制器,比如下面的实现:
Future<List<T>> runWithConcurrency<T>( List<Future<T> Function()> tasks, int maxConcurrent, ) async { final results = <T>[]; var index = 0; Future<void> worker() async { while (index < tasks.length) { final current = tasks[index++]; final value = await current(); results.add(value); } } final workers = List.generate(maxConcurrent, (_) => worker()); await Future.wait(workers); return results; }这里 maxConcurrent 控制了同时执行的 worker 数量,每个 worker 从任务列表中取下一个任务执行,直到取完。它保证了线程模型不因为网络抖动而导致资源耗尽。在 Flutter for OpenHarmony 的某跨平台系统的图片批量上传功能里,我就用这个逻辑把并发稳定在 4,实测上传速度远高于串行,内存占用也没爆。
5.2 超时、取消与 Completer 的进阶用法
很多异步操作没有现成的取消机制,比如某个 Future 方法调用后无法手动中断。这时可以用 Completer 做包装:
Future<T> withTimeout<T>(Future<T> future, Duration timeout) { final completer = Completer<T>(); Timer(timeout, () { if (!completer.isCompleted) { completer.completeError(TimeoutException('操作超时')); } }); future.then(completer.complete, onError: completer.completeError); return completer.future; }这段代码的核心思想是:把原始 Future 的结果转发给 Completer,同时启动一个定时器,如果超时后结果还没来,就由定时器接管并返回错误。这样上层 await 一个统一的可超时 Future,不会再被一直挂着。
Completer 还有一个用途是封装回调式 API。有些第三方库还在用 callback 风格,你可以用 Completer 把它转成 Future:
Future<File> saveFileWithResult(File file) { final completer = Completer<File>(); file.writeAsBytes(...).then((value) { completer.complete(value); }).catchError((e) { completer.completeError(e); }); return completer.future; }这样业务代码就能统一用 async / await 去调用,不用再传回调传得满天飞。实际开发中,我经常把这些小工具函数放在一个 async_utils.dart 里,全局复用,几乎每个网络模块都会用到。
6. 实战综合:Flutter for OpenHarmony 中的 Dart 高级特性组合拳
很多语言特性单独看都容易懂,但它们组合在一起,才是真正提升工程质量的地方。我选一个真实场景:在 Flutter for OpenHarmony 应用中做“带缓存的消息拉取列表”,串联本文大部分知识点。
6.1 场景拆解:一个消息列表的完整流程
业务需求大概是这样的:消息列表页打开时先展示本地缓存,同时从远端拉取新消息;新消息到达后,如果用户在线,立刻增量刷新;如果拉取失败,不能影响本地缓存的展示;整个拉取过程不能阻塞 UI 线程。
拆到技术点:
- 缓存读取和网络拉取是并行任务,用 Future.wait 启动。
- 网络结果可能是一个流(多条消息分批返回),用 Stream 承载。
- 数据解析放到后台 Isolate,用 compute 执行。
- 缓存和 UI 状态用 Mixin 组合复用,比如 Cacheable 和 Listable。
- 类型安全通过泛型基类 ViewModel 保障。
- 空安全用 ? 和 late 管理缓存状态。
- 超时用 Completer 包装,防止请求挂死。
这整个流程,几乎把 Dart 高级特性全部覆盖了一遍。单独看每个点都不难,组合起来就要考虑数据流的入口、出口、异常处理、生命周期。
6.2 组合代码示例
我先写一个简化版但可运行的骨架:
mixin Cacheable<T> { T? _cache; Future<T?> loadCache() async => _cache; void saveCache(T value) => _cache = value; } mixin Listable { List<String> items = []; void addItem(String item) => items.add(item); } class MessageRepository with Cacheable<List<String>>, Listable { Future<List<String>> fetchRemote() async { final data = await Future.delayed( const Duration(seconds: 1), () => ['远程消息1', '远程消息2', '远程消息3'], ); return data; } } class MessageViewModel { MessageViewModel(this._repo); final MessageRepository _repo; Future<List<String>> load() async { final results = await Future.wait([ _repo.loadCache(), _repo.fetchRemote(), ]); final cache = results[0] ?? const []; final remote = results[1]; // 合并数据:远程优先,缓存兜底 final merged = {...remote, ...cache}.toList(); _repo.saveCache(merged); return merged; } } void main() async { final repo = MessageRepository() ..saveCache(['缓存消息1']); // 预置缓存 final viewModel = MessageViewModel(repo); final messages = await viewModel.load(); print(messages); }这段代码可见:Mixin 只用一行with Cacheable<List<String>>, Listable就把缓存和列表行为注入到 Repository,泛型约束保证了 cache 的类型是 List 。Future.wait 保证缓存读取和远端拉取并行。缓存为 null 时,空安全让合并逻辑能安全兜底。
当然真实性项目的代码会更重,比如网络请求要拼接 Token、缓存要持久化到本地文件、远端数据要多页分页、列表要做 Diff 合并。但这些核心思路是一致的:用 Dart 的高级特性去减少重复代码、减少类型错误、缩短异步时间。
6.3 维护与演进:这些特性如何撑起后期需求
工程代码不是写完就拉倒,你要考虑半年后加需求时,该怎么改。
- 加新类型列表(图片、语音):只需要新增对应 Repository,依然复用 Cacheable 和 Listable,因为泛型 T 已经隔离了类型。
- 加离线优先策略:在缓存的加载方式上增加持久化逻辑,对上层透明,不影响 UI。
- 加同步状态:用 Riverpod 或 Bloc 时,这些 Dart 层面封装好的 Repository 可以直接变成 Provider 或 Bloc 状态源,改造不用动数据获取层。
我个人的经验是,Dart 高级特性学一遍大约花两到三天,但用熟至少需要几周。像我上面写的小技巧,比如并发限制器、Completer 超时包装、泛型 Repository,写第一版用时 20 分钟,但它们的价值在后续每个网络模块里都会放大。日常开发中,我几乎每个项目都会把这些工具类复制过去,改一改就用,再也没有回到过去那种到处定义 nullable 变量、写三层 if-else、把网络回调堆成金字塔的日子。
7. 常见问题与排查技巧实录
这个板块整理的是我学习和实际开发中遇到的高频问题,附上排查思路和结论。这些问题在官方文档里不一定有明显提示,但遇到时真的很折磨。
7.1 异步执行顺序混乱
典型场景:Flutter 页面打开后,loading 状态一闪而过,数据没回来就跳走了。原因往往是你在 initState 里启动了多个异步操作,但 await 它们前 UI 已经走完 build。排查方法:在关键异步位置打印日志,明确微任务和事件队列的执行顺序;确认异步操作的依赖关系。
处理方法:
- 如果页面关闭后 Future 才返回,检查 mounted 属性。BuildContext 跨异步操作时要用 mounted 判断,否则会报使用已销毁的 context 的错误。
- 如果几个 Future 需要依次执行,用 await 链或构建单一 Future 链,不要交错。
7.2 Store 状态无法更新
如果你用了 Mixin 来封装状态,但 UI 不刷新,优先检查 mixin 里的字段是否是 final。Mixin 里定义了可变状态字段后,你把它混入多个类中,它们共享的是同一个 mixin 实现吗?答案是:不是。每个类的实例都有该 mixin 的独立副本。所以不能通过一个类修改 mixin 字段来影响另一个类的实例。设计共享状态时,要么借用 Provider、Riverpod 这类全局状态管理,要么把状态提升到父组件。
7.3 Extension 方法没有生效
三种可能:没有导入对应的库;同名 Extension 发生冲突;你调用的类型与 Extension 的 on 类型不匹配。
特别是 on 类型的细节:如果你给List<dynamic>写了 Extension,那么List<String>类型变量在调用时并不总是能自动匹配。需要使用泛型 Extension 或者在 on 后面写具体的泛型参数。我在数据转换模块里就遇到过List<Map<String, dynamic>>和List<String>不匹配,导致 Extension 没被解析的坑。
7.4 Isolate 内存泄漏
Isolate 之间通信后,如果 receivePort 没有关闭,内存会一直挂着 Extended 的对象。虽然 Isolate 随着退出会被回收,但如果你频繁创建 Isolate 且不及时杀死,旧 Isolate 仍然占用资源。处理方法是:任务完成后一定调用receivePort.close(),以及根据实际场景考虑是否调用isolate.kill()。用Isolate.run的时候,底层会帮你管理,复杂度低很多,优先考虑。
7.5 空安全错误满天飞
初期用!强制解包,出现 LateInitializationError 或 cast error 后,开始大量加?,但代码变得破破碎碎。我的建议是:不要在写代码时消灭空值,而是在设计数据流时消灭不确定的空值来源。
比如网络 JSON 解析的结果,用json['id'] as int?获取可空值,再用?? defaultValue给默认值,最后得到非空类型。如果没有默认值,就定义一个结果类型(如 sealed class),用分支处理每个可能性,比散落的 if (x != null) 优雅得多。
8. 学习路径与效率技巧
这篇笔记写到这里,已经覆盖了 Dart 高级特性的大部分内容,但看完和会写是两码事。我给你我的学习路径建议,不要直接去啃整个官方 tour,而是按项目驱动的方式推进。
第一步:用本文的样例代码,把事件循环执行顺序、Stream 的 yield、Isolate 通信三个知识点各写一遍。这三个是 Dart 异步模型的核心,其他都是它们的应用。
第二步:把自己之前写的某个业务模块,比如网络请求或者数据列表页,用 Mixin 和 Extension 重构一遍。重点不是追求 fancy,而是体会"代码量少了、粒度清了"的感觉。
第三步:读一个中大型 Flutter 开源项目的源码,只看它的 Repository 层和数据层,看别人如何组织泛型、流式、异步和异常处理。等你能看懂并且复述给别人听,你就算真正掌握了。
第四步:回到 Flutter for OpenHarmony 项目里,尝试给现有页面加上缓存、后台解析、离线优先这些能力,用 Dart 特性优化性能。遇到问题再回来看这篇笔记。
我个人在实际操作中体会最深的一件事:Dart 的高级特性不是孤立的语法技巧,它是在为状态管理、并发、架构设计铺路。Flutter 和 OpenHarmony 的适配层再复杂,你拿捏住这些底层能力,到哪里都能写出一样丝滑的代码。
最后再分享一个小技巧:写 Dart 时多开 static analysis。它默认附带的 linter 会提示你很多潜在问题,比如不必要的空安全检查、可改进的类型提升。把这些提示当成免费的老教师,一个个改过去,你的代码风格会在两周内发生明显变化。别嫌烦,这是提升最快的捷径。