优化 demo

This commit is contained in:
Cody
2026-03-08 21:13:48 +08:00
parent c310ded32a
commit 9610c455ec
7 changed files with 59 additions and 53 deletions

View File

@@ -3205,7 +3205,13 @@ flowchart LR
<p><strong>两大核心逻辑</strong></p> <p><strong>两大核心逻辑</strong></p>
<p>1. <strong>MVVM 分层职责</strong>Viewview/只负责渲染和用户交互ViewModelpresentation/持有状态并处理业务逻辑Modelmodel/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p> <p>1. <strong>MVVM 分层职责</strong>Viewview/只负责渲染和用户交互ViewModelpresentation/持有状态并处理业务逻辑Modelmodel/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p>
<p>2. <strong>Riverpod 单向数据流</strong>:用户操作 → <code>ref.read(vm.notifier).action()</code> → ViewModel 处理逻辑 → <code>state = newState</code><code>ref.watch(vm)</code> 检测变化 → View 自动 rebuild。数据永远单向流动UI 永远是状态的函数。</p> <p>2. <strong>Riverpod 单向数据流</strong>:用户操作 → <code>ref.read(vm.notifier).action()</code> → ViewModel 处理逻辑 → <code>state = newState</code><code>ref.watch(vm)</code> 检测变化 → View 自动 rebuild。数据永远单向流动UI 永远是状态的函数。</p>
<p>3. <strong>Widget 纯展示原则</strong>ViewWidget层对业务数据严格只读。所有逻辑导航、CRUD、状态变更、条件判断必须在 ViewModel 中完成View 只调用 ViewModel 方法并渲染返回的 State。包括 demo/测试页面也不例外</p> <p>3. <strong>Widget 纯展示原则</strong><code>build()</code> 只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑</p>
<ul>
<li><strong>派生显示值必须是 State getter</strong>:凡是需要从 State 字段推导出另一个值(文本、颜色、样式等),一律写成 State 的 getterWidget 只读取结果。例如:<code>String get buttonLabel =&gt; testStarted ? '结束' : '开始';</code>Widget 写 <code>state.buttonLabel</code>,不在 <code>build()</code> 里写三元。</li>
<li><strong>禁止 <code>build()</code> 内定义局部计算变量</strong><code>final isSelected = current == mode</code> 这类从构造参数/state 派生值的局部变量,不属于 <code>build()</code>。组件应接受已算好的 <code>bool isSelected</code> 参数,由父级或 State getter 负责计算。</li>
<li><strong>导航、CRUD、状态变更全部在 ViewModel 中完成</strong>Widget 只转发事件:<code>onTap: () =&gt; ref.read(vm.notifier).doAction()</code></li>
<li><strong>demo/测试页面同样适用</strong>demo 代码是示范,别人会照着模仿,写法必须与正式页面完全一致。</li>
</ul>
</blockquote> </blockquote>
<hr> <hr>

View File

@@ -0,0 +1,39 @@
// 数据库测试页状态Demo正式开发后随页面一并删除
/// 单条测试结果记录
class TestResult {
final String title;
final String subtitle;
final String duration;
TestResult({
required this.title,
required this.subtitle,
required this.duration,
});
}
class ChatDbTestState {
final bool testStarted;
final List<TestResult> testResults;
final String currentState;
const ChatDbTestState({
this.testStarted = false,
this.testResults = const [],
this.currentState = '',
});
/// 按钮文案Widget 直接读,不在 View 层做判断)
String get buttonLabel => testStarted ? '结束' : '开始';
ChatDbTestState copyWith({
bool? testStarted,
List<TestResult>? testResults,
String? currentState,
}) => ChatDbTestState(
testStarted: testStarted ?? this.testStarted,
testResults: testResults ?? this.testResults,
currentState: currentState ?? this.currentState,
);
}

View File

@@ -6,42 +6,12 @@ import 'package:im_app/app/di/db_provider.dart';
import 'package:im_app/data/local/drift/app_database.dart'; import 'package:im_app/data/local/drift/app_database.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'chat_db_test_state.dart';
export 'chat_db_test_state.dart';
part 'chat_db_test_view_model.g.dart'; part 'chat_db_test_view_model.g.dart';
class TestResult {
final String title;
final String subtitle;
final String duration;
TestResult({
required this.title,
required this.subtitle,
required this.duration,
});
}
class ChatDbTestState {
final bool testStarted;
final List<TestResult> testResults;
final String currentState;
const ChatDbTestState({
this.testStarted = false,
this.testResults = const [],
this.currentState = '',
});
ChatDbTestState copyWith({
bool? testStarted,
List<TestResult>? testResults,
String? currentState,
}) => ChatDbTestState(
testStarted: testStarted ?? this.testStarted,
testResults: testResults ?? this.testResults,
currentState: currentState ?? this.currentState,
);
}
@riverpod @riverpod
class ChatDbTestViewModel extends _$ChatDbTestViewModel { class ChatDbTestViewModel extends _$ChatDbTestViewModel {
@override @override

View File

@@ -30,7 +30,7 @@ class ChatDbTestPage extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
AppButton.inverse( AppButton.inverse(
label: state.testStarted ? '结束' : '开始', label: state.buttonLabel,
onPressed: () => vm.toggleDBTest(), onPressed: () => vm.toggleDBTest(),
), ),
SizedBox(width: 8), SizedBox(width: 8),

View File

@@ -16,32 +16,27 @@ class ThemeView extends ConsumerWidget {
final current = ref.watch(themeViewModelProvider); final current = ref.watch(themeViewModelProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: const Text('主题')),
title: const Text('主题'),
),
body: ListView( body: ListView(
children: [ children: [
const SettingsSectionHeader(title: '外观'), const SettingsSectionHeader(title: '外观'),
ThemeOptionTile( ThemeOptionTile(
label: '跟随系统', label: '跟随系统',
mode: ThemeMode.system, isSelected: current == ThemeMode.system,
current: current,
onTap: () => ref onTap: () => ref
.read(themeViewModelProvider.notifier) .read(themeViewModelProvider.notifier)
.setMode(ThemeMode.system), .setMode(ThemeMode.system),
), ),
ThemeOptionTile( ThemeOptionTile(
label: '黑色模式', label: '黑色模式',
mode: ThemeMode.dark, isSelected: current == ThemeMode.dark,
current: current,
onTap: () => ref onTap: () => ref
.read(themeViewModelProvider.notifier) .read(themeViewModelProvider.notifier)
.setMode(ThemeMode.dark), .setMode(ThemeMode.dark),
), ),
ThemeOptionTile( ThemeOptionTile(
label: '白色模式', label: '白色模式',
mode: ThemeMode.light, isSelected: current == ThemeMode.light,
current: current,
onTap: () => ref onTap: () => ref
.read(themeViewModelProvider.notifier) .read(themeViewModelProvider.notifier)
.setMode(ThemeMode.light), .setMode(ThemeMode.light),

View File

@@ -5,14 +5,13 @@ import '../../../../../core/ui/base/context_theme_ext.dart';
/// 单个主题选项行 /// 单个主题选项行
/// ///
/// 纯展示 + 事件透传,不感知任何 Provider。 /// 纯展示 + 事件透传,不感知任何 Provider。
/// 父级传入 [current] 判断选中状态[onTap] 处理切换。 /// 父级传入 [isSelected] 决定是否显示勾选图标[onTap] 处理切换。
/// ///
/// 用法: /// 用法:
/// ```dart /// ```dart
/// ThemeOptionTile( /// ThemeOptionTile(
/// label: '黑色模式', /// label: '黑色模式',
/// mode: ThemeMode.dark, /// isSelected: current == ThemeMode.dark,
/// current: current,
/// onTap: () => ref.read(themeViewModelProvider.notifier).setMode(ThemeMode.dark), /// onTap: () => ref.read(themeViewModelProvider.notifier).setMode(ThemeMode.dark),
/// ) /// )
/// ``` /// ```
@@ -20,20 +19,17 @@ class ThemeOptionTile extends StatelessWidget {
const ThemeOptionTile({ const ThemeOptionTile({
super.key, super.key,
required this.label, required this.label,
required this.mode, required this.isSelected,
required this.current,
required this.onTap, required this.onTap,
}); });
final String label; final String label;
final ThemeMode mode; final bool isSelected;
final ThemeMode current;
final VoidCallback onTap; final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = context.styles; final s = context.styles;
final isSelected = current == mode;
return ListTile( return ListTile(
title: Text(label), title: Text(label),