diff --git a/Doc/IM_App_架构设计.html b/Doc/IM_App_架构设计.html index 2793eac..8d9dde1 100644 --- a/Doc/IM_App_架构设计.html +++ b/Doc/IM_App_架构设计.html @@ -3205,7 +3205,13 @@ flowchart LR

两大核心逻辑

1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。

2. Riverpod 单向数据流:用户操作 → ref.read(vm.notifier).action() → ViewModel 处理逻辑 → state = newStateref.watch(vm) 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。

-

3. Widget 纯展示原则:View(Widget)层对业务数据严格只读。所有逻辑(导航、CRUD、状态变更、条件判断)必须在 ViewModel 中完成,View 只调用 ViewModel 方法并渲染返回的 State。包括 demo/测试页面也不例外。

+

3. Widget 纯展示原则build() 只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。

+
diff --git a/apps/im_app/lib/features/chat/presentation/.gitkeep b/apps/im_app/lib/features/chat/presentation/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/im_app/lib/features/chat/presentation/chat_db_test_state.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_state.dart new file mode 100644 index 0000000..c37e5ba --- /dev/null +++ b/apps/im_app/lib/features/chat/presentation/chat_db_test_state.dart @@ -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 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? testResults, + String? currentState, + }) => ChatDbTestState( + testStarted: testStarted ?? this.testStarted, + testResults: testResults ?? this.testResults, + currentState: currentState ?? this.currentState, + ); +} diff --git a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart index 1abaa52..4386f91 100644 --- a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart +++ b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart @@ -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: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'; -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 testResults; - final String currentState; - - const ChatDbTestState({ - this.testStarted = false, - this.testResults = const [], - this.currentState = '', - }); - - ChatDbTestState copyWith({ - bool? testStarted, - List? testResults, - String? currentState, - }) => ChatDbTestState( - testStarted: testStarted ?? this.testStarted, - testResults: testResults ?? this.testResults, - currentState: currentState ?? this.currentState, - ); -} - @riverpod class ChatDbTestViewModel extends _$ChatDbTestViewModel { @override diff --git a/apps/im_app/lib/features/chat/view/chat_db_test_page.dart b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart index 95c1f01..3c8c025 100644 --- a/apps/im_app/lib/features/chat/view/chat_db_test_page.dart +++ b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart @@ -30,7 +30,7 @@ class ChatDbTestPage extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppButton.inverse( - label: state.testStarted ? '结束' : '开始', + label: state.buttonLabel, onPressed: () => vm.toggleDBTest(), ), SizedBox(width: 8), diff --git a/apps/im_app/lib/features/settings/view/theme_view.dart b/apps/im_app/lib/features/settings/view/theme_view.dart index e3e96a9..3151cb9 100644 --- a/apps/im_app/lib/features/settings/view/theme_view.dart +++ b/apps/im_app/lib/features/settings/view/theme_view.dart @@ -16,32 +16,27 @@ class ThemeView extends ConsumerWidget { final current = ref.watch(themeViewModelProvider); return Scaffold( - appBar: AppBar( - title: const Text('主题'), - ), + appBar: AppBar(title: const Text('主题')), body: ListView( children: [ const SettingsSectionHeader(title: '外观'), ThemeOptionTile( label: '跟随系统', - mode: ThemeMode.system, - current: current, + isSelected: current == ThemeMode.system, onTap: () => ref .read(themeViewModelProvider.notifier) .setMode(ThemeMode.system), ), ThemeOptionTile( label: '黑色模式', - mode: ThemeMode.dark, - current: current, + isSelected: current == ThemeMode.dark, onTap: () => ref .read(themeViewModelProvider.notifier) .setMode(ThemeMode.dark), ), ThemeOptionTile( label: '白色模式', - mode: ThemeMode.light, - current: current, + isSelected: current == ThemeMode.light, onTap: () => ref .read(themeViewModelProvider.notifier) .setMode(ThemeMode.light), diff --git a/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart b/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart index d270d9f..92b1df5 100644 --- a/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart +++ b/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart @@ -5,14 +5,13 @@ import '../../../../../core/ui/base/context_theme_ext.dart'; /// 单个主题选项行 /// /// 纯展示 + 事件透传,不感知任何 Provider。 -/// 由父级传入 [current] 判断选中状态,[onTap] 处理切换。 +/// 父级传入 [isSelected] 决定是否显示勾选图标,[onTap] 处理切换。 /// /// 用法: /// ```dart /// ThemeOptionTile( /// label: '黑色模式', -/// mode: ThemeMode.dark, -/// current: current, +/// isSelected: current == ThemeMode.dark, /// onTap: () => ref.read(themeViewModelProvider.notifier).setMode(ThemeMode.dark), /// ) /// ``` @@ -20,20 +19,17 @@ class ThemeOptionTile extends StatelessWidget { const ThemeOptionTile({ super.key, required this.label, - required this.mode, - required this.current, + required this.isSelected, required this.onTap, }); final String label; - final ThemeMode mode; - final ThemeMode current; + final bool isSelected; final VoidCallback onTap; @override Widget build(BuildContext context) { final s = context.styles; - final isSelected = current == mode; return ListTile( title: Text(label),