Flutter 开发必备:MVI 架构的高效实现指南

本文发布于公众号:移动开发那些事Flutter 开发必备:MVI 架构的高效实现指南

1 MVI 架构

MVIModel-View-Intent,是一种基于单向数据流响应式架构 模式。它将Android 开发中的 MVCMVP模式与函数响应式编程结合,形成了一种更加可预测、易测试的架构模式。它强调单向数据流和不可变状态,使应用状态更可预测和易于调试。

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/StreamBuilderMVI协同(如数据层隔离),构建更健壮的响应式系统。

5 参考

相关推荐
用户2136610035723 小时前
Vue2组件化开发与父子通信
前端·vue.js
Momo__3 小时前
TypeScript satisfies 操作符——比 as 更安全的类型守门员
前端·typescript
用户2136610035723 小时前
Vue2事件系统与指令进阶
前端·vue.js
labixiong3 小时前
实现一个能跑的迷你版Promise(一)
前端·javascript·面试
Csvn5 小时前
`??` 和 `||` 搞混,线上用户头像全挂了
前端
kyriewen5 小时前
白宫前脚下了限制令,OpenAI 后脚就把 GPT-5.6 发了
前端·gpt·openai
用户40269244819086 小时前
CRMEB Pro 新增后台接口全链路:路由、权限、验证器、返回格式一次讲清
前端·后端
泉城老铁7 小时前
springboot+vue+ ffmpeg 实现视频的拉流播放
前端
PedroQue997 小时前
uni-router v1.8.0新增冷启动守卫补执行
前端·uni-app