Flutter Riverpod + MVI 状态管理实现的提示词优化器
本文介绍了使用Riverpod和MVI架构开发AI提示词优化器的实现方案。MVI架构通过将状态(Model)、视图(View)和用户操作(Intent)分离,使代码更加清晰和可维护。核心思路是将所有状态集中在一个不可变的OptimizationState类中,通过Notifier处理用户操作并更新状态。这种方法相比传统状态管理减少了状态变量数量,提高了类型安全性,并支持流式响应处理。文章展示了关键
在是一篇优雅的 ~ MVI状态管理文章
我之前喜欢用 Jetpack Compose 开发,个人很喜欢MVI架构,代码相当优雅~《优雅永不过时》
一个准确的提示词决定了AI回答的质量,所以我用Fluter+MVI架构开发了一个「提示词优化器」
植入一个小 advertisement 请见谅

Riverpod + MVI:架构没有最好的,只有合适的
核心思想:状态是唯一真相来源
MVI 三要素:
MVI是什么意思:
- Model:一个不可变的数据类,包含页面所有状态(开关状态、主题颜色之类的)
- View:纯展示层,只负责渲染 Model
- Intent:用户的操作(点击、输入、滑动)
- Notifier:处理 Intent,更新 Model
AI 提示词优化器实现思路
需求
一个 AI 提示词优化页面,需要:
- 切换 Tab(用户提示词 / 系统提示词)
- 选择 API 配置和模板
- 输入原始提示词
- 流式接收优化结果(像打字机一样逐字显示)
- 自动保存历史
传统做法:20+ 个状态变量,300+ 行 StatefulWidget。
Riverpod + MVI 做法:1 个 State 类 + 1 个 Notifier 类。
只展示关键代码, 方便理解MVI
第一步:定义 Model(状态)
// lib/features/optimization/domain/entities/optimization_state.dart
enum OptimizationStatus {
idle, // 空闲
loading, // 准备中
streaming, // 流式接收中
success, // 成功
error, // 错误
}
class OptimizationState {
final OptimizationStatus status;
final String originalPrompt; // 原始输入
final String optimizedPrompt; // 优化结果(累积)
final String errorMessage;
final String currentTab; // 当前 Tab
final String? selectedApiConfigId;
final String? selectedTemplateId;
const OptimizationState({
this.status = OptimizationStatus.idle,
this.originalPrompt = '',
this.optimizedPrompt = '',
this.errorMessage = '',
this.currentTab = 'userOptimize',
this.selectedApiConfigId,
this.selectedTemplateId,
});
// 计算属性:是否正在处理
bool get isProcessing =>
status == OptimizationStatus.loading ||
status == OptimizationStatus.streaming;
// 计算属性:是否有结果
bool get hasResult => optimizedPrompt.isNotEmpty;
// 不可变更新:创建新实例
OptimizationState copyWith({
OptimizationStatus? status,
String? originalPrompt,
String? optimizedPrompt,
String? errorMessage,
String? currentTab,
String? selectedApiConfigId,
String? selectedTemplateId,
}) {
return OptimizationState(
status: status ?? this.status,
originalPrompt: originalPrompt ?? this.originalPrompt,
optimizedPrompt: optimizedPrompt ?? this.optimizedPrompt,
errorMessage: errorMessage ?? this.errorMessage,
currentTab: currentTab ?? this.currentTab,
selectedApiConfigId: selectedApiConfigId ?? this.selectedApiConfigId,
selectedTemplateId: selectedTemplateId ?? this.selectedTemplateId,
);
}
}
关键点:
- ✅ 所有状态集中在一个类:不会出现状态不一致
- ✅ 不可变:每次更新都创建新实例,方便调试和时间旅行
- ✅ 计算属性:
isProcessing、hasResult自动计算,UI 不用关心逻辑 - ✅ 类型安全:
OptimizationStatus枚举,不会出现status = 'loadding'这种拼写错误
第二步:定义 Intent(用户操作)
// lib/features/optimization/presentation/providers/optimization_provider.dart
class OptimizationNotifier extends StateNotifier<OptimizationState> {
final OptimizePromptUseCase _useCase;
final SettingsRepository _settingsRepo;
StreamSubscription<String>? _streamSubscription;
OptimizationNotifier(this._useCase, this._settingsRepo)
: super(const OptimizationState());
// ─── Intent 1: 切换 Tab ───
void switchTab(String type) {
final templateId = _settingsRepo.getSelectedTemplateId(type);
state = state.copyWith(
currentTab: type,
selectedTemplateId: templateId,
);
}
// ─── Intent 2: 选择 API 配置 ───
void selectApiConfig(String id) {
state = state.copyWith(selectedApiConfigId: id);
_settingsRepo.saveSelectedApiConfigId(id);
}
// ─── Intent 3: 选择模板 ───
void selectTemplate(String id) {
state = state.copyWith(selectedTemplateId: id);
_settingsRepo.saveSelectedTemplateId(state.currentTab, id);
}
// ─── Intent 4: 执行优化 ───
Future<void> optimize(String originalPrompt) async {
// 1. 参数校验
if (originalPrompt.trim().isEmpty) {
state = state.copyWith(
status: OptimizationStatus.error,
errorMessage: 'Prompt is empty',
);
return;
}
// 2. 取消之前的流
await _streamSubscription?.cancel();
_streamSubscription = null;
// 3. 更新状态为 loading
state = state.copyWith(
status: OptimizationStatus.loading,
originalPrompt: originalPrompt,
optimizedPrompt: '',
errorMessage: '',
);
try {
// 4. 执行优化用例,获取流式响应
final stream = _useCase.execute(
originalPrompt: originalPrompt,
apiConfigId: state.selectedApiConfigId!,
templateId: state.selectedTemplateId!,
type: state.currentTab,
);
state = state.copyWith(status: OptimizationStatus.streaming);
// 5. 监听流式数据,累积拼接结果
_streamSubscription = stream.listen(
(chunk) {
// 累积流式内容
state = state.copyWith(
optimizedPrompt: state.optimizedPrompt + chunk,
);
},
onError: (Object error) {
state = state.copyWith(
status: OptimizationStatus.error,
errorMessage: error.toString(),
);
},
onDone: () async {
// 6. 自动保存历史记录
if (completedPrompt.trim().isNotEmpty) {
//插入数据库
await _useCase.saveHistory(xxx);
}
},
);
} catch (e) {
state = state.copyWith(
status: OptimizationStatus.error,
errorMessage: e.toString(),
);
}
}
}
关键点:
- ✅ Intent 即方法:每个用户操作对应一个方法
- ✅ 单向数据流:Intent → 更新 State → UI 自动刷新
- ✅ 资源管理:
dispose时自动取消订阅和定时器 - ✅ 错误处理集中:所有错误都更新到
state.errorMessage
第三步:定义 Provider(依赖注入)
// ─── Provider 定义 ───
final dioProvider = Provider<Dio>((ref) => Dio());
final openAiApiServiceProvider = Provider<OpenAiApiService>((ref) {
return OpenAiApiService(ref.watch(dioProvider));
});
final optimizePromptUseCaseProvider = Provider<OptimizePromptUseCase>((ref) {
return OptimizePromptUseCase(
apiService: ref.watch(openAiApiServiceProvider),
apiConfigRepo: ref.watch(apiConfigRepositoryProvider),
templateRepo: ref.watch(templateRepositoryProvider),
historyRepo: ref.watch(historyRepositoryProvider),
);
});
final optimizationProvider =
StateNotifierProvider<OptimizationNotifier, OptimizationState>((ref) {
return OptimizationNotifier(
ref.watch(optimizePromptUseCaseProvider),
ref.watch(settingsRepositoryProvider),
ref.watch(apiConfigRepositoryProvider),
ref.watch(templateRepositoryProvider),
);
});
关键点:
- ✅ 依赖注入:所有依赖通过 Provider 注入,方便测试
- ✅ 自动管理生命周期:Provider 自动 dispose
- ✅ 依赖声明:
dependencies参数明确依赖关系
第四步:View(UI)
// lib/features/optimization/presentation/pages/home_page.dart
class HomePage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(optimizationProvider);
final notifier = ref.read(optimizationProvider.notifier);
return GlassScaffold(
appBar: AppBar(title: const Text('Prompt Optimizer')),
body: Column(
children: [
// Tab 切换
SegmentedButton<String>(
selected: {state.currentTab},
onSelectionChanged: (Set<String> selected) {
notifier.switchTab(selected.first);
},
segments: const [
ButtonSegment(value: 'userOptimize', label: Text('用户提示词')),
ButtonSegment(value: 'systemOptimize', label: Text('系统提示词')),
],
),
// API 配置选择
ApiConfigSelector(
selectedId: state.selectedApiConfigId,
onSelected: notifier.selectApiConfig,
),
// 模板选择
TemplateSelector(
selectedId: state.selectedTemplateId,
type: state.currentTab,
onSelected: notifier.selectTemplate,
),
// 输入框
TextField(
decoration: const InputDecoration(labelText: '输入提示词'),
onSubmitted: notifier.optimize,
),
// 优化按钮
ElevatedButton(
onPressed: state.isProcessing ? null : () {
notifier.optimize(/* 从输入框获取 */);
},
child: Text(state.isProcessing ? '优化中...' : '优化'),
),
// 结果展示
if (state.status == OptimizationStatus.loading)
const CircularProgressIndicator(),
if (state.status == OptimizationStatus.streaming)
SelectableText(state.optimizedPrompt),
if (state.status == OptimizationStatus.success)
SelectableText(state.optimizedPrompt),
if (state.status == OptimizationStatus.error)
Text('错误: ${state.errorMessage}', style: TextStyle(color: Colors.red)),
],
),
);
}
}
关键点:
- ✅ 纯展示层:UI 只负责渲染 state,不包含任何业务逻辑
- ✅ 自动刷新:
ref.watch监听状态变化,自动重建 Widget - ✅ 声明式 UI:根据
state.status显示不同 UI,清晰直观 - ✅ 无
setState:完全不需要setState
核心优势:对比传统做法
1. 状态一致性
传统做法:
// ❌ 可能出现 _isLoading = true 且 _error.isNotEmpty
setState(() {
_isLoading = true;
});
// ... 异步操作
setState(() {
_error = 'xxx';
// 忘记设置 _isLoading = false
});
Riverpod + MVI:
// ✅ 状态是枚举,不可能同时 loading 和 error
state = state.copyWith(status: OptimizationStatus.loading);
// ...
state = state.copyWith(status: OptimizationStatus.error);
2. 可测试性
传统做法:
// ❌ 无法测试
testWidgets('测试提交', (tester) async {
// 怎么 mock setState?
// 怎么验证 _isLoading 的值?
});
Riverpod + MVI:
// ✅ 轻松测试
test('测试优化流程', () async {
final container = ProviderContainer(
overrides: [
optimizePromptUseCaseProvider.overrideWithValue(mockUseCase),
],
);
final notifier = container.read(optimizationProvider.notifier);
// 执行操作
await notifier.optimize('测试提示词');
// 验证状态
expect(container.read(optimizationProvider).status, OptimizationStatus.success);
expect(container.read(optimizationProvider).optimizedPrompt, isNotEmpty);
});
3. 内存安全
传统做法:
// ❌ 容易内存泄漏
void _loadData() async {
final data = await api.fetch();
if (mounted) { // ← 每次都要记得检查
setState(() => _data = data);
}
}
Riverpod + MVI:
// ✅ 自动管理生命周期
Future<void> loadData() async {
final data = await api.fetch();
state = state.copyWith(data: data); // ← 不需要检查 mounted
}
// Provider 销毁时自动调用 dispose
4. 代码复用
传统做法:
// ❌ 逻辑散落在 3 个 StatefulWidget 中,无法复用
class Page1 extends StatefulWidget { /* 重复代码 */ }
class Page2 extends StatefulWidget { /* 重复代码 */ }
class Page3 extends StatefulWidget { /* 重复代码 */ }
Riverpod + MVI:
// ✅ Notifier 可以在多个页面复用
final optimizationProvider = StateNotifierProvider<OptimizationNotifier, OptimizationState>(...);
class Page1 extends ConsumerWidget {
Widget build(context, ref) {
final state = ref.watch(optimizationProvider);
// ...
}
}
class Page2 extends ConsumerWidget {
Widget build(context, ref) {
final state = ref.watch(optimizationProvider); // ← 复用同一个 Provider
// ...
}
}
实战技巧:流式响应的正确处理
问题:如何实现 ChatGPT 那样的逐字显示?
错误做法:
// ❌ 每个 chunk 都触发 UI 重建,性能差
stream.listen((chunk) {
setState(() {
_result += chunk; // ← 每次都重建整个 Widget
});
});
正确做法:
// ✅ 使用 StreamSubscription + 累积更新
_streamSubscription = stream.listen(
(chunk) {
// 累积到 state 中
state = state.copyWith(
optimizedPrompt: state.optimizedPrompt + chunk,
);
},
onError: (error) {
state = state.copyWith(
status: OptimizationStatus.error,
errorMessage: error.toString(),
);
},
onDone: () {
state = state.copyWith(status: OptimizationStatus.success);
},
);
实战技巧:资源清理
问题:如何避免内存泄漏?
Riverpod 自动管理:
class OptimizationNotifier extends StateNotifier<OptimizationState> {
StreamSubscription<String>? _streamSubscription;
void dispose() {
// Provider 销毁时自动调用
_streamSubscription?.cancel();
super.dispose();
}
}
实战技巧:持久化选择
用户关闭应用后,如何恢复上次的选择?
class OptimizationNotifier extends StateNotifier<OptimizationState> {
final SettingsRepository _settingsRepo;
OptimizationNotifier(this._settingsRepo) : super(const OptimizationState()) {
_loadPersistedSelections();
}
void _loadPersistedSelections() {
// 从 Hive 恢复上次的选择
final apiConfigId = _settingsRepo.getSelectedApiConfigId();
final templateId = _settingsRepo.getSelectedTemplateId(state.currentTab);
state = state.copyWith(
selectedApiConfigId: apiConfigId,
selectedTemplateId: templateId,
);
}
void selectApiConfig(String id) {
state = state.copyWith(selectedApiConfigId: id);
_settingsRepo.saveSelectedApiConfigId(id); // ← 同步保存
}
}
1. 状态设计原则
✅ 单一职责:一个 State 类只管一个功能模块
// ✅ 正确
class OptimizationState { /* 只管优化流程 */ }
class SettingsState { /* 只管设置 */ }
// ❌ 错误
class AppState {
// 把整个应用的状态都塞进来
final OptimizationState optimization;
final SettingsState settings;
final HistoryState history;
// ...
}
✅ 不可变:所有字段都是 final
class OptimizationState {
final String result; // ✅ final
OptimizationState copyWith({String? result}) {
return OptimizationState(result: result ?? this.result);
}
}
✅ 计算属性:避免冗余状态
class OptimizationState {
final OptimizationStatus status;
// ✅ 计算属性
bool get isProcessing =>
status == OptimizationStatus.loading ||
status == OptimizationStatus.streaming;
// ❌ 冗余状态
// final bool isProcessing;
}
2. Notifier 设计原则
✅ Intent 即方法:每个用户操作对应一个方法
class OptimizationNotifier extends StateNotifier<OptimizationState> {
void switchTab(String type) { /* ... */ }
void selectApiConfig(String id) { /* ... */ }
void optimize(String prompt) { /* ... */ }
void cancelOptimization() { /* ... */ }
}
✅ 业务逻辑委托给 UseCase:Notifier 只负责状态更新
// ✅ 正确
class OptimizationNotifier extends StateNotifier<OptimizationState> {
final OptimizePromptUseCase _useCase;
Future<void> optimize(String prompt) async {
state = state.copyWith(status: OptimizationStatus.loading);
final stream = _useCase.execute(prompt); // ← 委托给 UseCase
stream.listen((chunk) {
state = state.copyWith(optimizedPrompt: state.optimizedPrompt + chunk);
});
}
}
// ❌ 错误:业务逻辑写在 Notifier 里
class OptimizationNotifier extends StateNotifier<OptimizationState> {
Future<void> optimize(String prompt) async {
// 构建请求
final messages = [/* ... */];
// 调用 API
final response = await dio.post(/* ... */);
// 解析响应
final result = jsonDecode(response.data);
// ...
}
}
✅ 资源清理:在 dispose 中清理资源
void dispose() {
_streamSubscription?.cancel();
_timer?.cancel();
super.dispose();
}
3. Provider 设计原则
✅ 依赖注入:通过 Provider 注入依赖
final optimizationProvider =
StateNotifierProvider<OptimizationNotifier, OptimizationState>((ref) {
return OptimizationNotifier(
ref.watch(optimizePromptUseCaseProvider), // ← 注入依赖
ref.watch(settingsRepositoryProvider),
);
});
✅ 声明依赖:使用 dependencies 参数
final optimizationProvider =
StateNotifierProvider<OptimizationNotifier, OptimizationState>((ref) {
return OptimizationNotifier(/* ... */);
}, dependencies: [
optimizePromptUseCaseProvider,
settingsRepositoryProvider,
]);
总结:
Riverpod + MVI 通过单向数据流、状态集中管理、业务与 UI 分离,让代码更易维护、测试、扩展且可读性更高。
不要为了架构而架构
Riverpod + MVI 不是银弹,它适合:
- 复杂的业务逻辑
- 多人协作的项目
- 需要长期维护的应用
如果你的应用只是一个简单的展示页面,用 StatefulWidget 就够了。
架构是为了解决问题,不是为了炫技
项目源码
- 完整实现:https://github.com/JIULANG9/PromptOptimizer
- State 定义:
lib/features/optimization/domain/entities/optimization_state.dart - Notifier 实现:
lib/features/optimization/presentation/providers/optimization_provider.dart
如果对你有帮助,可否点一颗小星星
更多推荐



所有评论(0)