☰
Flutter动态GetX obs变量:从Map管理到元素级响应式刷新
2026/10/18 18:17:08 网站建设 项目流程

直接进入正题。

先抛一个很多 Flutter 开发者都会遇到的真实场景:你在维护一个筛选页面,面板上有价格区间、品牌、是否包邮、库存状态、排序方式……这些字段一开始只有五六个,后来产品每周加一个条件,你的代码就从手写几个final price = 0.0.obs变成了十几个。再后来字段变成“动态配置”,后台下发什么,前端就渲染什么,这时候你还对着 .obs 一个个声明,就完全写不动了。

这就是标题里“动态 obs 变量”真正要解的问题:不是怎么用var a = 0.obs,而是当变量的数量、名称、类型在运行时才能确定时,怎样依然保持 getx 的响应式能力。

这篇东西主要写给两类读者:一是已经用过 getx,但业务里碰到过“字段数量不固定”的开发者,想找一个更灵活但依旧规范的写法;二是正在从 setState 转向 getx,听说 obs 很好用,但对“动态”完全没有概念,想系统了解一下边界在哪里。两种情况我都会照顾到,代码都以 Dart 为主,环境默认是 Flutter 3.x + getx 4.x。

1. 为什么需要“动态”的 obs 变量

1.1 静态声明模式先天的短板

我先带你看看最常规的写法。假设页面里需要展示五个指标,你可能会写:

class MetricsController extends GetxController { final onlineCount = 0.obs; final todayOrders = 0.obs; final todayRevenue = 0.0.obs; final averagePrice = 0.0.obs; final conversionRate = 0.0.obs; }

这种写法在字段个数固定时是完全没有问题的,简单、直白、类型安全,编辑器还能自动补全。可一旦出现下面三种情况,它就开始别扭了:

第一,字段列表来自接口。比如一个“数据面板配置项”,后端返回[{key: "onlineCount", label: "在线人数", type: "number"}, {key: "wordCount", label: "文章数", type: "number"} ...],你不可能为每一种可能都预声明一个字段,因为后台随时会新增指标。

第二,存在“先有配置、后有值”的联动。你根据用户勾选的维度动态生成字段,比如用户勾选了“城市”,就生成city_1、city_2这种字段,同时还要实时监听变化。这玩意儿没法在编译期写死。

第三,同一个组件要做通用化复用。你写了一个通用的表格或表单组件,它接收的字段配置是传进来的,而不是写死在控制器里的。这时候静态声明等于把组件的灵活性废掉了。

1.2 动态需求的典型长相

把需求归纳一下,动态 obs 最常见的形态就是两种:一种是“动态数量的字段”,比如上面说的高频指标、筛选条件、动态表单项;另一种是“动态元素的可观察集合”,比如聊天消息列表、日志列表、接口返回的动态列表项。

很多新手一开始想到的方案是:既然字段是动态的,那就全部用Map<String, dynamic>存起来,然后map.obs不就完事了吗?这个思路方向是对的,但不完整。Rx<Map<String, dynamic>>只能让你在“整个 Map 被替换”的时候刷新 UI,如果你只修改其中一个 key,其他依赖这个 Map 的组件依然会全部重建。要命的地方在于,getx 的刷新机制是依赖级而不是字段级的,你的 UI 如果同时监听了一百个指标,改其中一个,所有指标组件都会 getBuilder 一遍。

所以动态 obs 的核心技术点,不是“把变量塞进 Map 里”,而是“让每一个动态字段都拥有独立的响应式能力”,这样以后改一个字段,只有监听这个字段的组件感知到变化,其他组件完全不受影响。

在进入具体方案之前,请记住一句话:我们想要的不是一个能动态存取的容器,而是一组能动态创建、独立感知的响应式对象。

2. 最朴素的动态方案:用 Map 管理一群 obs

2.1 Map<String, Rx > 与 RxMap 的区别

如果你把动态 obs 拆成“动态的 key + 独立的响应式对象”,第一个冒出来的方案就是Map<String, Rx<dynamic>>。

不要把它跟 getx 自带的RxMap混淆。RxMap本身是一个响应式的 Map 容器,它的监听粒度是整个 Map 被增删改的动作,并不是每个 key 都对应一个独立的Rx。你可以把RxMap理解成一个“会通知的字典”,而我们要的Map<String, Rx<T>>是一个“里面每个元素都是会通知的个体”的字典。

用生活里的话来说:RxMap是装了一袋子灯泡,这袋子被碰一下,所有灯泡都闪一下;Map<String, Rx<dynamic>>是每个灯泡都有独立开关,你只打开某一盏,其他灯泡毫无反应。

2.2 用 Map<String, Rx > 实现动态变量

直接看代码,这个方案的核心实现也不复杂,就是把“创建 obs”的过程从编译期搬到运行期:

class DynamicFieldsController extends GetxController { // 每个 key 对应一个独立的响应式对象 late final Map<String, Rx<dynamic>> _fields = {}; // 动态注册一个字段,并提供初始值 Rx<T> registerField<T>(String key, T initialValue) { if (_fields.containsKey(key)) { throw ArgumentError('Field $key already exists'); } final rx = Rx<T>(initialValue); _fields[key] = rx; return rx; } // 动态获取字段的响应式对象 Rx<T> field<T>(String key) { if (!_fields.containsKey(key)) { throw ArgumentError('Field $key not registered'); } return _fields[key]! as Rx<T>; } // 更新字段值,推荐统一走这个方法 void setField<T>(String key, T value) { final rx = field<T>(key); rx.value = value; } bool contains(String key) => _fields.containsKey(key); }

用的时候大概是这样的:

class MetricsPanel extends GetView<DynamicFieldsController> { const MetricsPanel({super.key}); @override Widget build(BuildContext context) { return Column( children: [ // 注册一个动态字段 Obx(() { final value = controller.field<int>('onlineCount').value; return Text('在线人数: $value'); }), ElevatedButton( onPressed: () => controller.setField<int>('onlineCount', 10086), child: const Text('更新'), ), ], ); } }

注意我在registerField里用了泛型<T>来保留变量的类型,这样虽然存储在Map<String, Rx<dynamic>>里,但使用方可以通过field<int>('onlineCount')取得强类型引用,编辑器也能正确推断。

2.3 为什么 RxMap 不能完全替代这个方案

你可能会问,直接用RxMap<String, dynamic>不也能动态读写吗?能,但刷新粒度完全不一样。看这个例子:

class SimpleController extends GetxController { final metrics = <String, dynamic>{}.obs; void updateMetric(String key, dynamic value) { metrics[key] = value; // 或者 metrics.update(key, (_) => value); } }

表面上看,metrics['onlineCount'] = 1和metrics['onlineCount'] = 2都会触发依赖metrics的Obx重建。但是有多少组件在监听metrics?只要你写了一个Obx(() => Text('${metrics['onlineCount']}')),那个 Obx 内部几乎会订阅整个metrics对象的事件流。结果就是,哪怕你只想刷新“在线人数”这一个文本,所有依赖这个 Map 的指标组件都会重建。

如果指标少还好,一旦页面有几十个指标卡,或者指标卡片内部还包含复杂的图表控件,这种“一刀切”的刷新策略就会带来肉眼可见的卡顿。更微妙的是,如果你在某个Obx里访问了多个 key,getx 没有办法跟踪“究竟哪一次变更触发的重建”,它只会简单地把你的闭包重新执行一遍。

所以我的建议很明确:当动态字段之间存在较强的独立性时,优先选Map<String, Rx<dynamic>>,而不是RxMap。只有在“整个 Map 的增删动作才需要联动 UI”的极简场景下,RxMap才更合适。

3. 进阶方案:自定义动态 Store 类

3.1 设计一个更规范的 DynamicStore

Map<String, Rx<dynamic>>虽然解决了核心问题,但它太“裸”了。真实项目里,你不仅要存取字段,还要处理类型非法、默认值、字段的增删通知、甚至字段之间的联动校验。直接暴露 Map 给业务层,项目一大会非常容易失控。

所以我在实际项目中会再包一层,做成一个专门用于动态响应的 Store 类。这层的价值不是炫技,而是把“动态”这个口子收窄,让业务代码不至于到处散落as Rx<T>和Stringkey。

class DynamicStore { // 维护 key -> Rx<dynamic> final Map<String, Rx<dynamic>> _rxMap = {}; /// 注册字段,返回该字段的可观察对象 Rx<T> createField<T>(String key, T initialValue) { if (_rxMap.containsKey(key)) { return _rxMap[key]! as Rx<T>; } final rx = Rx<T>(initialValue); _rxMap[key] = rx; return rx; } /// 读取字段值(带安全兜底) T? get<T>(String key) { final rx = _rxMap[key]; if (rx == null) return null; return rx.value as T; } /// 更新字段值(带类型检查和变更通知) bool set<T>(String key, T value) { final rx = _rxMap[key]; if (rx == null) return false; rx.value = value; return true; } /// 移除字段,并销毁对应的观测者 bool remove(String key) { final rx = _rxMap.remove(key); rx?.close(); return rx != null; } bool has(String key) => _rxMap.containsKey(key); /// 清空全部字段 void clear() { for (final rx in _rxMap.values) { rx.close(); } _rxMap.clear(); } /// 获取当前所有注册的 key,方便 UI 遍历 List<String> get keys => _rxMap.keys.toList(); }

看到没,这个 Store 本身不继承GetxController,它只是一个纯逻辑的容器。这样设计的理由有两点:一是它可以被多个 Controller 共享,二是不需要在 Controller 层把一堆动态字段和业务方法混在一起。比如你可以在这个 Store 之上,封装一个专门的SettingsController extends GetxController,内部持有DynamicStore。

3.2 动态表单的校验与联动更新

一旦 Store 成形,动态表单就好做了。我先说一个实际体会:表单类需求是动态 obs 最常用的落地场景,因为表单项几乎都是“后台配置什么、前端渲染什么”,而且每个字段都有自己的校验规则。

class DynamicFormController extends GetxController { final store = DynamicStore(); // 根据配置的字段初始化 void initFromConfig(List<FieldConfig> configs) { for (final config in configs) { if (!store.has(config.key)) { store.createField(config.key, config.defaultValue); } } } // 表单提交时统一读取 Map<String, dynamic> submit() { return { for (final key in store.keys) key: store.get(key), }; } // 联动更新:比如勾选了“包邮”,自动把“运费”字段置零 void enableFreeShipping(bool enable) { if (enable) { store.set('shippingFee', 0); } } }

这个方案的好处是,字段的创建、读取、更新、销毁都收敛到一处。以后需要加“自定义校验器”,只需要在 Store 里再加一个Map<String, FormFieldValidator>记录器;需要做“脏检查”,只要维护一个“是否修改过”的标记集合。这些扩展都不会动到外层 UI 代码。

3.3 这个方案帮我们避开了哪些坑

先说类型问题。Map<String, Rx<dynamic>>最容易被吐槽的就是dynamic。如果你写store.get('age')拿到一个dynamic,直接参与计算很容易在运行时炸出type 'String' is not a subtype of type 'int'。Store 方案用泛型把类型责任推给调用方,至少在 Controller 层你能写出store.set<int>('age', 18),编译器能帮你挡掉一部分错误。

其次是“字段不存在”的静默失败。裸 Map 版本里,_fields['notExist']?.value = 1会静默通过,然后 UI 永远不刷新,排查半天。Store 的set方法会返回bool,你可以强制要求业务方对返回值做出断言。

最后是销毁。动态字段如果不再需要,应该主动close()掉对应的Rx,否则它的监听者会一直挂在内存里。我用 Store 统一管理之后,只需要在整个控制器onClose()里调用store.clear(),所有注册字段的订阅关系一次性断开,不用再一个个盯着。

4. 列表场景下的“元素级”动态 obs

4.1 RxList 的坑:整条数据变成可观察

如果说前两节讲的是“字段的动态”,这一节要讲的是“元素的动态”。很多人在列表场景里犯的第一个错,是习惯性地把整条 List 变成RxList:

class ChatController extends GetxController { final messages = <Message>[].obs; }

用起来确实方便,messages.add(item)会自动通知 UI,整个列表会 rebuild 一遍。但列表越长,这个写法越浪费,因为每添加一条消息,所有依赖messages的Obx都会重新执行。

更麻烦的是子项内部的状态。比如每条消息有个“已读/未读”标记,点击某一条要单独翻转状态。用RxList<Message>的话,你得先把Message变成可观察对象,或者整体替换。稍微复杂一点,比如 ListView 使用了itemBuilder按需构建,列表外部根本没有组件在监听messages,你改messages[3].read = true之后 UI 毫无变化,此时只能被迫messages.refresh(),又回到全量刷新。

4.2 用 List<Rx > 实现元素级刷新

不推荐把整个列表做成一个大号响应式集合,更合理的方式是“列表本身可以不是响应式的,但每个元素是响应式的”。

比如实现一个聊天消息列表,你只需要保证每一条消息里的关键字段是Rx:

class Message { final String id; final String content; final RxBool isRead; Message({ required this.id, required this.content, bool isRead = false, }) : isRead = RxBool(isRead); }

在控制器里,用一个普通List<Message>保存消息:

class ChatController extends GetxController { final messages = <Message>[]; void loadMockData() { messages.addAll([ Message(id: '1', content: '你好'), Message(id: '2', content: 'getx 动态 obs 好用吗'), ]); } void toggleRead(String messageId) { final index = messages.indexWhere((m) => m.id == messageId); if (index != -1) { messages[index].isRead.toggle(); } } }

UI 里,itemBuilder中单独监听每个元素的isRead:

ListView.builder( itemCount: controller.messages.length, itemBuilder: (context, index) { final msg = controller.messages[index]; return Obx(() { return ListTile( title: Text(msg.content), trailing: msg.isRead.value ? const Icon(Icons.done) : null, ); }); }, )

猜猜效果是什么?点击某一条消息的“已读”按钮,只有那一条 ListTile 会重建,其他条目纹丝不动。

你可能已经发现,这个场景里 Obs 变量不是靠一个 Map 管理的,而是“天然地长在每个元素上”。这就是动态 obs 的第二种形态:集合里的每一个元素都自己携带响应式字段,集合本身不负责通知。

4.3 什么时候该选哪种列表方案

下面这个对比,基本涵盖了我日常开发里的决策路径:

场景推荐方案理由
列表整体增删且项内数据简单RxList<Model>代码最少,性能可接受
列表量大,项内字段频繁局部变化List<Model>且 Model 内部字段用 Rx最小化刷新范围
列表项本身是动态配置字段List<DynamicItem>,每个 item 持有 DynamicStore字段数量不确定
列表需要极频繁地整体打乱、排序RxList+ 不可变 Model依赖排序的批量操作,局部 Rx 收益不大

这里补充一条我踩过的坑:不要把一个元素的整体变成Rx<Message>。一旦你这么做了,每次message.value = newMessage都会触发元素级的整体重建。如果 Message 里有多个子组件,这种重建可能和RxList全量刷新一样糟糕。正确做法是继续往下拆,把变化最频繁的最小单位变成Rx,比如RxBool、RxString、RxInt。

5. 动态控制器与动态 obs 的组合

5.1 getx 控制器也可以按需创建

聊完字段和元素,还有一个维度值得考虑:如果你动态创建了一组 obs,很多时候应该配套动态创建一个控制器实例,否则几十个字段全塞在一个全局 Controller 里,命名冲突、生命周期都不好维护。

getx 本身支持通过 Tag 创建同类型的多个实例:

class DetailController extends GetxController { final DynamicStore store = DynamicStore(); static DetailController of(String id) { final tag = 'detail_$id'; if (!Get.isRegistered<DetailController>(tag: tag)) { Get.put(DetailController(), tag: tag); } return Get.find<DetailController>(tag: tag); } @override void onClose() { store.clear(); super.onClose(); } }

坐实一下这个写法的用处。你在一个动态列表里每个 item 都对应一个详情页,列表长度是后台下发的,你不可能预先写好DetailControllerA、DetailControllerB。用 Tag 之后,每点击一个 item 就创建并绑定一个独立控制器,对应的字段也各自独立。

5.2 动态 Tab 页的场景示例

这个组合最常见的形态是“动态 Tab + 每个 Tab 独立状态”。比如一个报表页面,Tab 数量来自配置:

class ReportPage extends StatelessWidget { final List<String> tabKeys = ['sales', 'traffic', 'conversion']; @override Widget build(BuildContext context) { return DefaultTabController( length: tabKeys.length, child: Scaffold( appBar: AppBar( bottom: TabBar( tabs: tabKeys.map((key) => Tab(text: key)).toList(), ), ), body: TabBarView( children: tabKeys.map((key) { final controller = ReportController.of(key); return ReportView(controller: controller); }).toList(), ), ), ); } }

每个 ReportController 里再创建自己的 DynamicStore,store 里的字段互不干扰。这样“动态 obs”从变量层面扩展到了整个控制器生命周期,逻辑上非常干净。

5.3 别忘掉 dispose 与生命周期管理

动态创建的Rx和Controller如果忘记清理,轻则内存报警,重则出现“页面关闭后还在后台刷新”之类的奇怪现象。我用三条铁律管理清理:

第一,能绑定生命周期的绑定。Controller 里创建的 obs,统一交给 Controller 的onClose()释放,不要散落在组件里手动close()。用上面 DynamicStore 的clear()就能把一批字段全部断开。

第二,Tag 控制实例生命周期。凡是Get.put(Controller(), tag: tag)创建的实例,要么在父级销毁时手动Get.delete<Controller>(tag: tag),要么利用 getx 的绑定机制让它在路由销毁时自动回收。不要在普通 Widget 里直接Get.put却不考虑回收。

第三,Rx对象本身支持close()。如果你确实在某个临时工具类里创建了Rx,不需要挂到 Controller 上,用完立刻close(),不要等 GC。这地方新手最容易犯的错是不知道Rx内部持有StreamController相关的资源,不及时释放确实会泄漏。

6. 实战中的高频坑位与排查经验

6.1 变量造出来了 UI 却不刷新

动态 obs 场景下,这个问题排名第一。典型代码是这样的:

final map = <String, Rx<dynamic>>{}; map['count'] = 1.obs; // 错!1.obs 创建过程中,并没有和 map 建立关系 map['count']?.value = 2; // 这行只改了 Rx 的值,Obx 在监听 map 吗?

其实关键不在 map,而在你 UI 里的写法。如果你写的是Obx(() => Text('${map['count']!.value}')),那么 getx 会根据运行期间的依赖收集,自动把map['count']这个 Rx 对象的监听关系建立起来,刷新是正常的。

但如果你的代码长这样,就不刷新了:

final rawValue = map['count']!.value; // 提前取值 Obx(() => Text('$rawValue'));

看明白没?rawValue不是响应式的,Obx 里执行闭包时读的是缓存好的一个普通值,getx 根本找不到依赖。这种“提前取出”的写法和动态变量无关,但动态场景多了以后特别容易触发,因为大家习惯用 Map 取值然后到处传。

还有一种更隐蔽的情况:你在set的时候给同一字段重新赋了同一个内存地址的对象,比如:

final item = Message('x'); store.set('msg', item); store.set('msg', item); // 第二次 value 相同,Rx 不会通知

Rx 的判断逻辑是“新旧值是否相等”,对于同一个对象引用,它认为是“没变化”,不会触发通知。如果业务上确实需要强制刷新,可以使用rx.refresh()或者赋一个深拷贝的新对象。

6.2 类型强制转换的坑

动态 + 泛型最容易踩cast的坑。看我之前写的 DynamicStore:

final rx = _rxMap[key]! as Rx<T>;

这里有个不易察觉的问题,Rx<int>和Rx<num>并不等价,甚至Rx<int?>和Rx<int>也不一样。如果你先创建了Rx<int>,然后用store.set<num>('age', 3.14)去更新,as Rx<num>可能在强转时就崩了。

所以我后来在 DynamicStore 里放弃在as上做文章,改为完全存Rx<dynamic>,取值时用Rx<T>泛型只做“读取层的约束”,不在“写入层做强转”:

bool set(String key, dynamic value) { final rx = _rxMap[key]; if (rx == null) return false; rx.value = value; // dynamic 层面赋值,编译器不挡,但运行时类型不对会赋值期报错 return true; } T get<T>(String key) { final rx = _rxMap[key]; if (rx == null) throw Exception('Field not found: $key'); return rx.value as T; // 只在读取时做类型断言 }

如果你有强类型洁癖,也可以升级方案:在创建字段的时候同时记录一个Type type,set前先做runtimeType校验。但我的经验是,绝大多数项目根本到不了那个复杂度,把类型错误暴露在“读取时的明显位置”比在“写入时的隐蔽角落”更好排查。

6.3 一次性把 GetBuilder 和 Obx 混用的陷阱

另一个很常见的坑是把GetBuilder和Obx混在一个页面上。

getx 的GetBuilder走的是GetxController.update()机制,它和Rx的响应式流是两套体系。如果你在同一个页面上既用Obx监听某个Rx,又用GetBuilder包裹同一块 UI,很容易出现“我以为刷新了,其实没刷新;我以为没刷新,其实所有组件都重建了”。

结合动态 obs 的场景,我给你的建议是:有多字段独立刷新的需求,就统一用Obx/GetX,不要再退回GetBuilder。因为GetBuilder注定是 Controller 级别的全量刷新,你费尽心思做的“字段级动态 obs”会被它一锅端。

6.4 一个完整的小案例:动态指标卡片面板

最后用一个综合案例把前面所有内容串起来。

需求是这样的:一个大盘页面,顶部有几张指标卡片,卡片数量、指标名称、数值类型都由后台配置下发,每张卡片单独刷新,且不需要在编译期写死任何指标。

Controller 层:

class DashboardController extends GetxController { final store = DynamicStore(); List<MetricConfig> configs = []; void loadConfigFromServer() { // 模拟拉取配置 configs = [ MetricConfig(key: 'online_users', label: '在线用户', type: 'int', initial: 0), MetricConfig(key: 'today_sales', label: '今日销量', type: 'int', initial: 0), MetricConfig(key: 'avg_price', label: '平均客单价', type: 'double', initial: 0.0), ]; for (final config in configs) { if (!store.has(config.key)) { final dynamic value = config.type == 'int' ? config.initial as int : config.initial as double; store.createField<dynamic>(config.key, value); } } } void mockPushData() { store.set('online_users', 2333); store.set('today_sales', 188); store.set('avg_price', 99.9); } @override void onClose() { store.clear(); super.onClose(); } }

UI 层,每张卡片独立 Obx 监听:

class MetricCard extends StatelessWidget { final MetricConfig config; const MetricCard({super.key, required this.config}); @override Widget build(BuildContext context) { final controller = Get.find<DashboardController>(); return Card( child: Padding( padding: const EdgeInsets.all(16), child: Obx(() { final value = controller.store.get<dynamic>(config.key); final displayValue = value is double ? value.toStringAsFixed(2) : value.toString(); return Column( children: [ Text(config.label), Text(displayValue, style: const TextStyle(fontSize: 24)), ], ); }), ), ); } }

你发现问题了吗?上面 UI 里有个很重要的细节:Obx闭包里通过controller.store.get<dynamic>(config.key)获取值时,get 方法内部访问了rx.value,这个访问动作会被 getx 的依赖收集器记录。所以虽然 store 是被封装过的,UI 依然能够准确监听到该字段的变化。

动手试一下,你会观察到:mockPushData()连续更新三个字段时,三张卡片各自刷新自己的文字,彼此之间完全无感。这就是动态 obs 该有的体验。

6.5 性能取舍和高频更新的注意点

动态 obs 不是银弹,它也有自己的性能边界。

第一,不要在Obx闭包里做复杂的计算或列表过滤。因为每次字段变化,闭包都会重新执行。如果你在闭包里盲目遍历几千个配置项,单次更新的成本就会很高。

第二,如果单个字段的更新频率特别高(比如每帧更新一个进度条),还是要考虑节流。getx 没有内置 throttle 机制,我会在 Store 层简单加一个新值队尾标记,或在更新处用Timer控制频率,不要一帧写个几十次rx.value。

第三,动态字段数量超过一定量级(我个人觉得单控制器超过一两百个)时,建议拆分 Store,或者改成“分页注册”的思路。否则每个Rx对象本身也有创建和持有成本,UI 的依赖关系也会变得非常难调试。

最后说几句我自己的经验

把动态 obs 玩明白之后,我对 getx 状态管理的整体理解也上了一个台阶:响应式的本质是“被依赖的数据发生变化时精确通知”,而不是“所有变量都塞进一个大容器里被动通知”。单纯依赖RxMap或者把所有东西都声明成全局 Controller 的静态字段,都没法在真正动态的业务场景里走远。

最常用的组合其实就是 DynamicStore + 按 Tag 动态创建的 Controller,前者管字段,后者管生命周期。如果你项目里已经有类似“配置驱动 UI”的需求,建议直接从动态 Store 这层起步,不要等字段写死之后再重构,那个成本要大得多。

后续如果你在实践里遇到什么新鲜坑,比如 Flutter Web 上动态 obs 的刷新时机,或者 Widget 复用时的订阅残留,都值得好好记录一下。这种从真实业务里打磨出来的经验,往往比 API 文档有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询