本文发布于公众号:移动开发那些事Flutter 开发必备:MVI 架构的高效实现指南
1 MVI 架构
MVI 即 Model-View-Intent,是一种基于单向数据流 的响应式架构 模式。它将Android 开发中的 MVC、MVP模式与函数响应式编程结合,形成了一种更加可预测、易测试的架构模式。它强调单向数据流和不可变状态,使应用状态更可预测和易于调试。
1.1 核心组件
-
Model :表示应用的数据和业务逻辑(负责接收Intent,并把Intent转换成状态(
State)发送出去) -
View:负责 UI 展示和用户交互
-
Intent:表示用户的意图或操作(封装用户操作和事件)
-
State:应用当前的状态
1.2 架构特点
- 单向数据流:所有数据流动都是单向的,从 Intent → Model → State → View;
- 状态可预测:每个状态变化都是明确的,便于调试和追踪
- 不可变状态:每次状态变化都会生成一个新的状态,
- 易于测试:由于业务逻辑与 UI 分离,且状态变化是可预测的,MVI 架构使得单元测试变得更加容易;
1.3 适用场景
- 复杂 UI 交互场景: 当应用的 UI 交互复杂,状态管理困难时,MVI 的单向数据流和不可变状态可以大大简化状态管理。
- 需要状态恢复的场景: 如需要实现撤销 / 重做功能、离线缓存或多任务切换的场景,MVI 的状态管理机制使得状态恢复变得简单。
- 团队协作开发或多用户协同操作场景: 如在线文档编辑,
- 对测试有较高要求的场景: MVI 的架构设计使得单元测试和集成测试更加容易
- 需要高度可预测状态管理的应用:
2 简单架构示例
这里以实现计数器的例子来说明如何实现MVI的架构
scala
// 步骤1 :定义 Intent,封装意图
abstract class CounterIntent {}
// 增加的意图
class IncrementIntent extends CounterIntent {}
// 减小的意图
class DecrementIntent extends CounterIntent {}
// 步骤2: 定义 State
class CounterState {
// 注意,这里是一个final的,说明这个状态是不可变的
final int count;
CounterState(this.count);
CounterState.initial() : count = 0;
}
// 步骤3 定义 Model (这个步骤可省略,可放到ViewModel里)
class CounterModel {
Future<CounterState> increment(CounterState currentState) async {
await Future.delayed(Duration(milliseconds: 100)); // 模拟异步操作
return CounterState(currentState.count + 1);
}
Future<CounterState> decrement(CounterState currentState) async {
await Future.delayed(Duration(milliseconds: 100)); // 模拟异步操作
return CounterState(currentState.count - 1);
}
// 这里可再实现一个根据历史状态来回溯的方法和状态,这里就不展开了
}
// 步骤4 定义 ViewModel (对应架构中的 Model,接收intent,并把intent转换为对应的状态)
class CounterViewModel {
final CounterModel _model = CounterModel();
// 这里实现一个历史状态的存储,可状态的回溯(实际业务可能更复杂)
final _stateHistory = <CounterState>[];
CounterState _currentState = CounterState.initial();
final _stateController = StreamController<CounterState>();
// 通过这个stream来监听状态的变化逻辑
Stream<CounterState> get stateStream => _stateController.stream;
// 对外的方法,接收intent,并转换成对应的状态
void processIntent(CounterIntent intent) async {
if (intent is IncrementIntent) {
_stateHistory.add(_currentState);
_currentState = await _model.increment(_currentState);
} else if (intent is DecrementIntent) {
_stateHistory.add(_currentState);
_currentState = await _model.decrement(_currentState);
}
// 把状态的变化通过StreamController进行发送
_stateController.add(_currentState);
}
void dispose() {
_stateController.close();
}
}
// 步骤4 定义 View,这个就是对应的页面了
class CounterView extends StatefulWidget {
@override
_CounterViewState createState() => _CounterViewState();
}
class _CounterViewState extends State<CounterView> {
late CounterViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = CounterViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MVI Counter (Vanilla)')),
body: Center(
// 这里通过StreamBuilder 来监听stream的变化
child: StreamBuilder<CounterState>(
stream: _viewModel.stateStream,
initialData: CounterState.initial(),
builder: (context, snapshot) {
return Text(
'Count: ${snapshot.data?.count}',
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
// 按钮点击时,生成一个新的intent,并交给model处理
onPressed: () => _viewModel.processIntent(IncrementIntent()),
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
// 按钮点击时,生成一个新的intent,并交给model处理
onPressed: () => _viewModel.processIntent(DecrementIntent()),
child: Icon(Icons.remove),
),
],
),
);
}
}
这个架构工作流:
- 用户操作 → 触发Intent对象创建;
- ViewModel接收Intent → 生成新状态;
- 新状态通过Stream推送 → 触发UI更新;
- UI始终基于最新状态渲染;
3 使用GetX框架实现MVI架构
GetX是一个轻量级的Flutter状态管理框架,它天然支持响应式编程,可以简化MVI的实现。
scss
// 省略: 定义 Intent (在 GetX 中通常直接使用方法调用表示意图)
// 不需要显式定义 Intent 类
// 步骤1 : 定义 State
class CounterState {
final int count;
CounterState(this.count);
CounterState.initial() : count = 0;
}
// 步骤2: Model 保持不变
class CounterModel {
Future<CounterState> increment(CounterState currentState) async {
await Future.delayed(Duration(milliseconds: 100));
return CounterState(currentState.count + 1);
}
Future<CounterState> decrement(CounterState currentState) async {
await Future.delayed(Duration(milliseconds: 100));
return CounterState(currentState.count - 1);
}
}
// 步骤3 : 定义 ViewModel (对应架构中的 Model,接收intent,并把intent转换为对应的状态))
class CounterController extends GetxController {
final _model = CounterModel();
// 直接通过obs的变化来提供状态的监听
var _currentState = CounterState.initial().obs;
CounterState get currentState => _currentState.value;
// 这里也可以把这两个方法 封装成一个,与前面的实现类似
void increment() async {
_currentState.value = await _model.increment(_currentState.value);
}
void decrement() async {
_currentState.value = await _model.decrement(_currentState.value);
}
}
// 步骤4 : 定义 View 这个就是对应的页面了
class CounterViewGetX extends StatelessWidget {
final CounterController _controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MVI Counter (GetX)')),
body: Center(
// 这里通过Obx 来监听对应的viewmodel里的值的变化,对变化时,就会触发对应状态的监听
child: Obx(() => Text(
'Count: ${_controller.currentState.count}',
style: TextStyle(fontSize: 24),
)),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => _controller.increment(),
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
onPressed: () => _controller.decrement(),
child: Icon(Icons.remove),
),
],
),
);
}
}
4 总结
本文主要介绍了MVI 架构的特点,以及如何在在Flutter上实现该架构。这里主要介绍了两种实现方式:使用纯Dart 和 结合GetX框架。纯Dart 的 实现提供了最大的灵活性和对架构的完全控制,适合大型复杂项目。而使用GetX 框架可以显著减少样板代码,加快开发速度,适合中小型项目或需要快速迭代的项目。 但无论选择哪种方式,MVI架构的核心思想------单向数据流和明确的状态管理 ------都能帮助开发者构建更可维护、更可预测的 Flutter 应用。并且实际开发中可结合FutureBuilder/StreamBuilder与MVI协同(如数据层隔离),构建更健壮的响应式系统。