优化 demo
This commit is contained in:
@@ -3205,7 +3205,13 @@ flowchart LR
|
|||||||
<p><strong>两大核心逻辑</strong>:</p>
|
<p><strong>两大核心逻辑</strong>:</p>
|
||||||
<p>1. <strong>MVVM 分层职责</strong>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p>
|
<p>1. <strong>MVVM 分层职责</strong>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + 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>:View(Widget)层对业务数据严格只读。所有逻辑(导航、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 的 getter,Widget 只读取结果。例如:<code>String get buttonLabel => 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: () => ref.read(vm.notifier).doAction()</code>。</li>
|
||||||
|
<li><strong>demo/测试页面同样适用</strong>:demo 代码是示范,别人会照着模仿,写法必须与正式页面完全一致。</li>
|
||||||
|
</ul>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user