在是一篇优雅的 ~ MVI状态管理文章

我之前喜欢用 Jetpack Compose 开发,个人很喜欢MVI架构,代码相当优雅~《优雅永不过时》

一个准确的提示词决定了AI回答的质量,所以我用Fluter+MVI架构开发了一个「提示词优化器」

植入一个小 advertisement 请见谅

请添加图片描述

Riverpod + MVI:架构没有最好的,只有合适的

核心思想:状态是唯一真相来源

MVI 三要素
请添加图片描述

MVI是什么意思

  • Model:一个不可变的数据类,包含页面所有状态(开关状态、主题颜色之类的)
  • View:纯展示层,只负责渲染 Model
  • Intent:用户的操作(点击、输入、滑动)
  • Notifier:处理 Intent,更新 Model

AI 提示词优化器实现思路

需求

一个 AI 提示词优化页面,需要:

  1. 切换 Tab(用户提示词 / 系统提示词)
  2. 选择 API 配置和模板
  3. 输入原始提示词
  4. 流式接收优化结果(像打字机一样逐字显示)
  5. 自动保存历史

传统做法: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,
    );
  }
}

关键点

  1. 所有状态集中在一个类:不会出现状态不一致
  2. 不可变:每次更新都创建新实例,方便调试和时间旅行
  3. 计算属性isProcessinghasResult 自动计算,UI 不用关心逻辑
  4. 类型安全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(),
      );
    }
  }
}

关键点

  1. Intent 即方法:每个用户操作对应一个方法
  2. 单向数据流:Intent → 更新 State → UI 自动刷新
  3. 资源管理dispose 时自动取消订阅和定时器
  4. 错误处理集中:所有错误都更新到 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),
  );
});

关键点

  1. 依赖注入:所有依赖通过 Provider 注入,方便测试
  2. 自动管理生命周期:Provider 自动 dispose
  3. 依赖声明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)),
        ],
      ),
    );
  }
}

关键点

  1. 纯展示层:UI 只负责渲染 state,不包含任何业务逻辑
  2. 自动刷新ref.watch 监听状态变化,自动重建 Widget
  3. 声明式 UI:根据 state.status 显示不同 UI,清晰直观
  4. 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

如果对你有帮助,可否点一颗小星星

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐