MoLc
MoLc 是一个轻量 Flutter 状态管理与页面架构组件。它的核心目标不是替代某个 状态管理库的 API,而是把 Flutter 页面里的 UI 状态 Model 和 业务逻辑 Logic 拆开,并补齐真实工程中常见的跨组件通信、全局状态局部刷新、细粒度刷新等能力。
当前版本基于 Flutter 原生的 InheritedWidget / InheritedNotifier 实现,
不依赖 provider。
设计目标
MoLc 关注以下工程问题:
- Model / Logic 分离:Widget 只负责声明 UI,状态放在 Model,业务动作放在 Logic。
- 上下文树访问补全:支持子 context 访问父级对象,也支持通过
top<T>()和find<T>()做跨页面、父访问子、兄弟组件访问。 - 逻辑复用:Logic 可以脱离 Widget 组织业务行为,也可以按需暴露给其他组件调用。
- 精准刷新:Model 可以通过
SelectorMixin决定是否刷新。 - 全局状态局部刷新:TopModel 可以按事件刷新指定消费者,避免全局 Model 更新导致整棵 App rebuild。
- 颗粒刷新:
Mutable<T>可以实现接近 GetX / mutableStateOf 的局部响应式刷新。
安装
dependencies:
molc: ^0.2.1
import 'package:molc/molc.dart';
快速开始
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return MoLcWidget<_CounterModel, _CounterLogic>(
modelCreate: (_) => _CounterModel(),
logicCreate: (_) => _CounterLogic(),
init: (_, model, logic) => logic.init(model),
builder: (context, model, logic, child) {
return Column(
children: [
Text('count: ${model.count}'),
TextButton(
onPressed: logic.increase,
child: const Text('+1'),
),
],
);
},
);
}
}
class _CounterModel extends Model {
int count = 0;
}
class _CounterLogic extends MoLogic<_CounterModel> {
void init(_CounterModel model) {}
void increase() {
refresh(() {
model.count++;
});
}
}
refresh() 会触发 Model 的 notifyListeners(),刷新依赖当前 Model 的 UI。
核心概念
Model
Model 用来保存 Widget 状态。
class PageModel extends Model {
int selectedIndex = 0;
bool loading = false;
}
更新状态后调用:
model.refresh(() {
model.selectedIndex = 1;
});
如果 Model 需要访问当前 widget 的 BuildContext,使用 WidgetModel:
class PageModel extends WidgetModel {
String get title => Localizations.localeOf(context).toString();
}
WidgetModel.context 只在 Model 挂载到 ModelWidget / MoLcWidget 时有效。
当 widget 从树上移除时,MoLc 会 detach 这个 context。
Logic
Logic 用来放请求、跳转、事件处理、业务协调等行为。
class RequestLogic extends Logic {
Future<void> load(PageModel model) async {
// request data
model.refresh();
}
}
如果 Logic 需要 context,使用 WidgetLogic:
class NavigatorLogic extends WidgetLogic {
void close() {
Navigator.of(context).pop();
}
}
如果 Logic 与某个 Model 强绑定,使用 MoLogic<T>:
class BoundPageLogic extends MoLogic<PageModel> {
void init(PageModel model) {}
void reload() {
refresh(() {
model.loading = true;
});
}
}
MoLogic<T> 适合页面级或组件级强关联逻辑。可复用 Logic 建议通过方法参数传入 Model,
避免业务逻辑过度依赖某个具体 UI 状态结构。
组件
ModelWidget
只有 Model 时使用:
ModelWidget<PageModel>(
create: (_) => PageModel(),
builder: (context, model, child) {
return Text('${model.selectedIndex}');
},
);
外部持有 Model 时使用 .value:
final model = PageModel();
ModelWidget<PageModel>.value(
value: model,
builder: (context, model, child) => Text('${model.selectedIndex}'),
);
生命周期规则:
ModelWidget(create:)创建并负责 dispose Model。ModelWidget.value不负责 dispose 外部 Model。- 即使是
.value,MoLc 也会在 widget 卸载时 detachWidgetModel.context, 并移除ExposedMixin的活跃注册。
LogicWidget
只有 Logic 时使用:
class SubmitLogic extends Logic {
void init() {}
void submit() {}
}
LogicWidget<SubmitLogic>(
create: (_) => SubmitLogic(),
init: (context, logic) => logic.init(),
builder: (context, logic) {
return TextButton(
onPressed: logic.submit,
child: const Text('submit'),
);
},
);
LogicWidget 会创建 Logic,并在 widget 卸载时调用 logic.dispose()。
MoLcWidget
同时需要 Model 和 Logic 时使用,这是最常用的组件。
MoLcWidget<PageModel, BoundPageLogic>(
modelCreate: (_) => PageModel(),
logicCreate: (_) => BoundPageLogic(),
init: (_, model, logic) => logic.init(model),
builder: (context, model, logic, child) {
return Text('${model.selectedIndex}');
},
);
原生 Provider 层
MoLc 提供了一层极薄的原生注入能力:
MoNotifierProvider<T extends ChangeNotifier>MoProvider<T>MoMultiProvidercontext.read<T>()context.watch<T>()
read<T>() 只读取对象,不订阅刷新。
watch<T>() 会建立 inherited 依赖。当对应 MoNotifierProvider 的 notifier
触发通知时,使用 watch<T>() 的 widget 会 rebuild。
MoNotifierProvider<PageModel>(
create: (_) => PageModel(),
child: Builder(
builder: (context) {
final model = context.watch<PageModel>();
return Text('${model.selectedIndex}');
},
),
);
普通对象使用 MoProvider:
MoProvider<ApiClient>(
create: (_) => ApiClient(),
dispose: (client) => client.close(),
child: const App(),
);
MoMultiProvider 中 provider 的顺序是从外到内:
MoMultiProvider(
providers: [
moProvider<ApiClient>((_) => ApiClient()),
moProvider<UserRepository>((context) {
return UserRepository(context.read<ApiClient>());
}),
],
child: const App(),
);
靠前的 provider 在树的外层,靠后的 provider 可以在 create 中读取靠前的对象。
不要在 dispose 阶段通过 context.read<T>() 查找祖先。Flutter 会认为 deactivated
context 的祖先查找是不安全的。需要清理依赖时,应在对象创建、初始化或 attach 阶段先缓存。
TopProvider 与 TopModel
Flutter 的 inherited 机制天然适合父节点向子节点传值。App 级状态通常需要放在
MaterialApp 之上,MoLc 用 TopProvider 统一承载这些顶级对象,并维护内部容器。
void main() {
runApp(
TopProvider(
providers: [
moNotifierProvider<AppModel>((_) => AppModel()),
],
child: const MaterialApp(
home: HomePage(),
),
),
);
}
class AppModel extends TopModel {}
读取 TopModel:
final appModel = context.read<AppModel>();
final sameModel = top<AppModel>();
规则:
- 一个 App 同时只能挂载一个
TopProvider。 top<T>()只能在TopProvider已经挂载后使用。- 测试中需要先
pumpWidget(const SizedBox.shrink())卸载旧树,再挂新TopProvider。
全局 Model 局部刷新
全局 Model 更新时,如果直接刷新整个 TopModel,容易导致大范围 rebuild。
MoLc 用 EventModel<T> 和 EventConsumerMixin 做事件级局部刷新。
enum AppEvent { userChanged, themeChanged }
class AppModel extends TopModel with EventModel<AppEvent> {}
class ProfileModel extends Model with EventConsumerMixin {
void init() {
listenTopModelEvent(AppEvent.userChanged);
}
}
触发事件:
top<AppModel>().refreshEvent(AppEvent.userChanged);
只监听 AppEvent.userChanged 的 Model 会刷新。
也可以提供自定义刷新函数和判断条件:
listenTopModelEvent(
AppEvent.userChanged,
test: () => shouldUpdate,
refresh: () {
reloadLocalData();
refresh();
},
);
事件 key 使用事件对象本身的 == / hashCode,不是 toString()。
推荐使用 enum 或稳定的 value object。
ExposedMixin
InheritedWidget 只能自然支持子 context 访问父 context。业务中有时需要父组件访问子组件、
兄弟组件互相访问,或者在非 Widget 层调用当前活跃的 Logic。ExposedMixin 用来把当前
活跃对象注册到 MoLc 容器中。
class DetailModel extends Model with ExposedMixin {
void reload() {}
}
查找当前活跃实例:
find<DetailModel>()?.reload();
Logic 也可以暴露:
class DetailLogic extends Logic with ExposedMixin {
void scrollToTop() {}
}
find<DetailLogic>()?.scrollToTop();
规则:
find<T>()返回当前活跃的最后一个T实例。- 对象从
ModelWidget/LogicWidget卸载时会自动移除注册。 .value外部对象不会被 dispose,但也会在 widget 卸载时从活跃注册中移除。findFuzzy(String)仍可用于字符串查找,但新代码推荐使用find<T>()。
SelectorMixin
SelectorMixin<T> 用于 Model 内部的精细化刷新判断。
class PageModel extends Model with SelectorMixin<(int, String)> {
int count = 0;
String keyword = '';
bool loading = false;
@override
(int, String) selectWith() {
return (count, keyword);
}
}
refresh() 时,MoLc 会比较 selectWith() 的返回值。如果返回值没有变化,
Model 不会通知 UI 刷新。
注意:selectWith() 返回值需要有可靠的 == 语义。可以使用 record、tuple、
不可变 value object 等。
Mutable
Mutable<T> 适合非常局部的响应式状态,例如列表项、计数器、局部开关。
final count = 0.mt;
MutableWidget(
(context) {
return Row(
children: [
Text('${count.value}'),
TextButton(
onPressed: () {
count.value += 1;
},
child: const Text('+1'),
),
],
);
},
);
读取 count.value 时,当前 MutableWidget 会被注册为依赖者。
写入 count.value 时,所有依赖它的 MutableWidget 会刷新。
设计备注:Mutable 内部的静态 build-phase delegate 是刻意设计,不是残留的全局状态。
它利用 Flutter widget build 在单 isolate 上同步执行的模型,在读取 value 时自动捕获当前
MutableWidget 并建立订阅,从而实现接近 GetX 的自动依赖追踪。除非要改变这套自动追踪语义,
否则不应把它改成显式订阅 API。
规则:
Mutable.value应该在MutableWidget的 builder 同步读取。- 可以在事件回调、异步回调中写入
Mutable.value。 - 不要在没有
MutableWidget上下文的地方读取Mutable.value或隐式调用toString(),否则没有可绑定的刷新目标。 MutableWidget支持嵌套,也支持多个 widget 监听同一个Mutable值。
简单值组件
如果只是需要一个临时 Model,可以使用 NoMoWidget:
NoMoWidget<int>(
value: 0,
builder: (context, model, child) {
return TextButton(
onPressed: () {
model.refresh(() {
model.value++;
});
},
child: Text('${model.value}'),
);
},
);
也提供 NoMo2Widget 和 NoMo3Widget,分别对应 Value2Model 和 Value3Model。
推荐组织方式
页面级代码建议按这个思路拆分:
page/
user_page.dart // Widget 和 MoLcWidget 装配
user_model.dart // UI 状态
user_logic.dart // 请求、跳转、业务动作
建议:
- 页面内部状态优先放在页面 Model。
- 请求、跳转、复杂业务流程放在 Logic。
- 跨页面共享状态放在 TopModel。
- 父访问子、兄弟访问、临时跨层调用才使用 ExposedMixin。
- 高频局部小状态可以使用 Mutable。
- 不要把所有状态都放进 TopModel,也不要把
find<T>()当作默认数据流。
从 provider 迁移
MoLc 0.2.0 起不再依赖 provider,也不再从 molc.dart 导出 provider API。
旧写法:
ChangeNotifierProvider(
create: (_) => AppModel(),
child: const App(),
);
新写法:
MoNotifierProvider<AppModel>(
create: (_) => AppModel(),
child: const App(),
);
旧的 TopProvider.providers 如果使用 provider 的 SingleChildWidget:
TopProvider(
providers: [
ChangeNotifierProvider(create: (_) => AppModel()),
],
child: const App(),
);
迁移为:
TopProvider(
providers: [
moNotifierProvider<AppModel>((_) => AppModel()),
],
child: const App(),
);
API 一览
核心组件:
ModelWidget<T extends Model>LogicWidget<T extends Logic>MoLcWidget<T extends Model, R extends Logic>NoMoWidget<T>NoMo2Widget<A, B>NoMo3Widget<A, B, C>
核心类型:
ModelWidgetModelLogicWidgetLogicMoLogic<T extends Model>TopModel
跨层能力:
TopProvidertop<T extends TopModel>()ExposedMixinfind<T extends ExposedMixin>()findFuzzy(String)
刷新能力:
Model.refresh()SelectorMixin<T>EventModel<T>EventConsumerMixinMutable<T>MutableWidget
原生注入层:
MoProvider<T>MoNotifierProvider<T extends ChangeNotifier>MoMultiProvidermoProvider<T>()moProviderValue<T>()moNotifierProvider<T>()moNotifierProviderValue<T>()context.read<T>()context.watch<T>()
适用场景
MoLc 适合:
- 中小型到中大型 Flutter App;
- 页面状态较多、组件拆分较多的业务项目;
- 希望保留 Flutter 原生 Widget 思维,但减少状态和逻辑混杂的项目;
- 不想引入重型状态框架,但需要跨页面共享、局部刷新、逻辑复用的项目。
不建议把 MoLc 用成完全全局化的数据流。top<T>()、find<T>()、Mutable<T> 都是为
特定工程问题准备的工具,应按场景使用。