Flutter中MVVM+Repository模式极简实现方案问询
Flutter MVVM + Repository 最简实现(零额外依赖)
实现思路
- 只用Flutter原生
ChangeNotifier做状态通知,完全不需要第三方包 - 按业务主题拆分专属Repository和ViewModel,每个模块只管对应主题的逻辑
- 严格遵循你要的双向数据流:
Repository -> ViewModel -> View,反向交互则是View -> ViewModel -> Repository
1. 先定义业务模型
以你提到的「挑战」和「自我肯定」为例,先写基础实体类:
// 挑战模型 class Challenge { final String id; final String title; bool isCompleted; Challenge({ required this.id, required this.title, this.isCompleted = false, }); // 用于更新状态的拷贝方法 Challenge copyWith({bool? isCompleted}) { return Challenge( id: id, title: title, isCompleted: isCompleted ?? this.isCompleted, ); } } // 自我肯定模型 class Affirmation { final String id; final String content; Affirmation({ required this.id, required this.content, }); }
2. 实现Repository层
每个主题对应一个Repo,负责数据CRUD,并用ChangeNotifier通知ViewModel数据变化:
ChallengeRepository
import 'package:flutter/foundation.dart'; import '../models/challenge.dart'; class ChallengeRepository extends ChangeNotifier { // 模拟数据库存储,实际项目可替换为sqflite/API请求 final List<Challenge> _challenges = [ Challenge(id: '1', title: '每天步行10000步'), Challenge(id: '2', title: '连续7天早起'), ]; // 对外暴露只读数据,避免外部直接修改 List<Challenge> get challenges => List.unmodifiable(_challenges); // 模拟从数据库加载数据 Future<void> loadChallenges() async { await Future.delayed(const Duration(milliseconds: 500)); // 模拟异步延迟 notifyListeners(); // 加载完成通知ViewModel } // 更新挑战完成状态 void toggleChallengeCompletion(String challengeId) { final index = _challenges.indexWhere((c) => c.id == challengeId); if (index != -1) { _challenges[index] = _challenges[index].copyWith( isCompleted: !_challenges[index].isCompleted, ); notifyListeners(); // 更新后通知ViewModel } } // 添加新挑战 void addChallenge(String title) { _challenges.add(Challenge( id: DateTime.now().millisecondsSinceEpoch.toString(), title: title, )); notifyListeners(); } }
AffirmationRepository
import 'package:flutter/foundation.dart'; import '../models/affirmation.dart'; class AffirmationRepository extends ChangeNotifier { final List<Affirmation> _affirmations = [ Affirmation(id: '1', content: '我足够优秀'), Affirmation(id: '2', content: '我有能力克服困难'), ]; List<Affirmation> get affirmations => List.unmodifiable(_affirmations); Future<void> loadAffirmations() async { await Future.delayed(const Duration(milliseconds: 500)); notifyListeners(); } void addAffirmation(String content) { _affirmations.add(Affirmation( id: DateTime.now().millisecondsSinceEpoch.toString(), content: content, )); notifyListeners(); } }
3. 实现ViewModel层
ViewModel持有对应Repo实例,监听Repo的变化,更新自身状态,同时给View暴露交互方法:
ChallengeViewModel
import 'package:flutter/foundation.dart'; import '../repositories/challenge_repository.dart'; import '../models/challenge.dart'; class ChallengeViewModel extends ChangeNotifier { final ChallengeRepository _repo = ChallengeRepository(); List<Challenge> _challenges = []; List<Challenge> get challenges => _challenges; ChallengeViewModel() { // 监听Repo的变化,同步更新ViewModel状态 _repo.addListener(_onRepoUpdated); // 初始化加载数据 loadChallenges(); } // 响应Repo的更新 void _onRepoUpdated() { _challenges = _repo.challenges; notifyListeners(); // 通知View刷新UI } // 给View调用的方法,转发给Repo Future<void> loadChallenges() async { await _repo.loadChallenges(); } void toggleChallengeCompletion(String challengeId) { _repo.toggleChallengeCompletion(challengeId); } void addChallenge(String title) { _repo.addChallenge(title); } // 销毁时移除监听,避免内存泄漏 @override void dispose() { _repo.removeListener(_onRepoUpdated); super.dispose(); } }
AffirmationViewModel
import 'package:flutter/foundation.dart'; import '../repositories/affirmation_repository.dart'; import '../models/affirmation.dart'; class AffirmationViewModel extends ChangeNotifier { final AffirmationRepository _repo = AffirmationRepository(); List<Affirmation> _affirmations = []; List<Affirmation> get affirmations => _affirmations; AffirmationViewModel() { _repo.addListener(_onRepoUpdated); loadAffirmations(); } void _onRepoUpdated() { _affirmations = _repo.affirmations; notifyListeners(); } Future<void> loadAffirmations() async { await _repo.loadAffirmations(); } void addAffirmation(String content) { _repo.addAffirmation(content); } @override void dispose() { _repo.removeListener(_onRepoUpdated); super.dispose(); } }
4. 实现View层
用StatefulWidget直接监听ViewModel,完全不用第三方状态管理包:
ChallengeView
import 'package:flutter/material.dart'; import '../viewmodels/challenge_viewmodel.dart'; import '../models/challenge.dart'; class ChallengeView extends StatefulWidget { const ChallengeView({super.key}); @override State<ChallengeView> createState() => _ChallengeViewState(); } class _ChallengeViewState extends State<ChallengeView> { late final ChallengeViewModel _viewModel; final _textController = TextEditingController(); @override void initState() { super.initState(); _viewModel = ChallengeViewModel(); // 监听ViewModel变化,刷新UI _viewModel.addListener(() => setState(() {})); } @override void dispose() { _viewModel.dispose(); _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('我的挑战')), body: Column( children: [ // 添加挑战输入框 Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextField( controller: _textController, decoration: const InputDecoration(hintText: '输入新挑战'), ), ), ElevatedButton( onPressed: () { if (_textController.text.isNotEmpty) { _viewModel.addChallenge(_textController.text); _textController.clear(); } }, child: const Text('添加'), ), ], ), ), // 挑战列表 Expanded( child: _viewModel.challenges.isEmpty ? const Center(child: Text('加载中...')) : ListView.builder( itemCount: _viewModel.challenges.length, itemBuilder: (context, index) { final Challenge challenge = _viewModel.challenges[index]; return ListTile( title: Text( challenge.title, style: TextStyle( decoration: challenge.isCompleted ? TextDecoration.lineThrough : null, ), ), leading: Checkbox( value: challenge.isCompleted, onChanged: (value) { _viewModel.toggleChallengeCompletion(challenge.id); }, ), ); }, ), ), ], ), ); } }
数据流说明
正向(数据从Repo到View)
- Repo加载/更新数据后调用
notifyListeners() - ViewModel监听Repo变化,同步自身状态后调用
notifyListeners() - View监听ViewModel变化,执行
setState()刷新UI
反向(用户交互从View到Repo)
- 用户操作UI(比如点击复选框、输入内容)
- View调用ViewModel的对应方法
- ViewModel转发调用Repo的方法更新数据
- Repo更新后触发正向数据流,最终刷新UI
内容的提问来源于stack exchange,提问作者Big_Chair
相关产品推荐
相关产品推荐

