Initial project
This commit is contained in:
21
apps/im_app/lib/features/settings/di/settings_providers.dart
Normal file
21
apps/im_app/lib/features/settings/di/settings_providers.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../usecases/set_theme_usecase.dart';
|
||||
|
||||
/// Settings feature DI 装配
|
||||
///
|
||||
/// 手动装配 UseCase Provider,ViewModel 通过此处获取依赖。
|
||||
///
|
||||
/// ```
|
||||
/// ThemeViewModel
|
||||
/// → ref.read(setThemeUseCaseProvider) ← 此处装配
|
||||
/// → SetThemeUseCase(幂等校验)
|
||||
/// → onApply → ThemeModeNotifier.setMode()(内存 + 持久化 TODO)
|
||||
/// ```
|
||||
|
||||
// ── UseCase ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 设置主题用例 Provider
|
||||
final setThemeUseCaseProvider = Provider<SetThemeUseCase>(
|
||||
(_) => const SetThemeUseCase(),
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../../app/router/app_route_name.dart';
|
||||
|
||||
part 'settings_view_model.g.dart';
|
||||
|
||||
/// 设置页 ViewModel
|
||||
///
|
||||
/// ## 数据流位置
|
||||
///
|
||||
/// ```
|
||||
/// SettingsPage
|
||||
/// → ref.read(settingsViewModelProvider.notifier).navigateToTheme(context)
|
||||
/// → ★ SettingsViewModel.navigateToTheme() ★ ← 你在这里
|
||||
/// → context.push(AppRouteName.settingsTheme.path)
|
||||
/// ```
|
||||
///
|
||||
/// 导航意图由 ViewModel 统一管理,View 不直接调用路由。
|
||||
@riverpod
|
||||
class SettingsViewModel extends _$SettingsViewModel {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
/// 跳转到主题设置页。
|
||||
void navigateToTheme(BuildContext context) {
|
||||
context.push(AppRouteName.settingsTheme.path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../../app/di/app_providers.dart';
|
||||
import '../di/settings_providers.dart';
|
||||
|
||||
part 'theme_view_model.g.dart';
|
||||
|
||||
/// 主题 ViewModel
|
||||
///
|
||||
/// View 层只感知此 ViewModel,不直接依赖 app 级 Provider。
|
||||
///
|
||||
/// ## 数据流
|
||||
///
|
||||
/// ```
|
||||
/// ThemeView
|
||||
/// → ref.watch(themeViewModelProvider) ← 当前 ThemeMode
|
||||
/// → ref.read(themeViewModelProvider.notifier).setMode(mode)
|
||||
/// → ★ ThemeViewModel.setMode() ★ ← 你在这里
|
||||
/// → SetThemeUseCase.execute()
|
||||
/// → 幂等校验(相同模式直接返回)
|
||||
/// → onApply → ThemeModeNotifier.setMode() ← 更新内存状态
|
||||
/// → TODO: 持久化(storage_sdk)
|
||||
/// ```
|
||||
@riverpod
|
||||
class ThemeViewModel extends _$ThemeViewModel {
|
||||
@override
|
||||
ThemeMode build() => ref.watch(themeModeProvider);
|
||||
|
||||
void setMode(ThemeMode mode) {
|
||||
ref.read(setThemeUseCaseProvider).execute(
|
||||
current: state,
|
||||
requested: mode,
|
||||
onApply: (m) => ref.read(themeModeProvider.notifier).setMode(m),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 设置主题用例
|
||||
///
|
||||
/// 职责:幂等校验——当前模式与目标模式相同时直接返回,不触发任何变更。
|
||||
///
|
||||
/// 持久化由 [ThemeModeNotifier.setMode] 负责(在 `onApply` 被调用后执行),
|
||||
/// UseCase 不感知存储细节。
|
||||
///
|
||||
/// ## 数据流
|
||||
///
|
||||
/// ```
|
||||
/// ThemeViewModel.setMode(mode)
|
||||
/// → ★ SetThemeUseCase.execute() ★ ← 你在这里
|
||||
/// → 幂等校验(相同模式 → 直接返回)
|
||||
/// → onApply(mode)
|
||||
/// → ThemeModeNotifier.setMode() ← 更新内存 + 写入持久化(TODO)
|
||||
/// ```
|
||||
class SetThemeUseCase {
|
||||
const SetThemeUseCase();
|
||||
|
||||
/// 执行主题切换
|
||||
///
|
||||
/// [current] 当前生效的主题模式
|
||||
/// [requested] 用户选择的目标模式
|
||||
/// [onApply] 校验通过后回调,由 ViewModel 负责调用 ThemeModeNotifier
|
||||
void execute({
|
||||
required ThemeMode current,
|
||||
required ThemeMode requested,
|
||||
required void Function(ThemeMode mode) onApply,
|
||||
}) {
|
||||
if (current == requested) return;
|
||||
onApply(requested);
|
||||
}
|
||||
}
|
||||
31
apps/im_app/lib/features/settings/view/settings_page.dart
Normal file
31
apps/im_app/lib/features/settings/view/settings_page.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../presentation/settings_view_model.dart';
|
||||
|
||||
/// 设置页
|
||||
///
|
||||
/// 所有用户操作通过 [SettingsViewModel] 处理,View 不直接调用路由。
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('设置'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('主题'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => ref
|
||||
.read(settingsViewModelProvider.notifier)
|
||||
.navigateToTheme(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
apps/im_app/lib/features/settings/view/theme_view.dart
Normal file
53
apps/im_app/lib/features/settings/view/theme_view.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../presentation/theme_view_model.dart';
|
||||
import 'widgets/settings_section_header.dart';
|
||||
import 'widgets/theme_option_tile.dart';
|
||||
|
||||
/// 主题选择页
|
||||
///
|
||||
/// 通过 [ThemeViewModel] 读写主题状态,不直接感知 app 级 Provider。
|
||||
class ThemeView extends ConsumerWidget {
|
||||
const ThemeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final current = ref.watch(themeViewModelProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('主题'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
const SettingsSectionHeader(title: '外观'),
|
||||
ThemeOptionTile(
|
||||
label: '跟随系统',
|
||||
mode: ThemeMode.system,
|
||||
current: current,
|
||||
onTap: () => ref
|
||||
.read(themeViewModelProvider.notifier)
|
||||
.setMode(ThemeMode.system),
|
||||
),
|
||||
ThemeOptionTile(
|
||||
label: '黑色模式',
|
||||
mode: ThemeMode.dark,
|
||||
current: current,
|
||||
onTap: () => ref
|
||||
.read(themeViewModelProvider.notifier)
|
||||
.setMode(ThemeMode.dark),
|
||||
),
|
||||
ThemeOptionTile(
|
||||
label: '白色模式',
|
||||
mode: ThemeMode.light,
|
||||
current: current,
|
||||
onTap: () => ref
|
||||
.read(themeViewModelProvider.notifier)
|
||||
.setMode(ThemeMode.light),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../core/ui/base/context_theme_ext.dart';
|
||||
|
||||
/// 设置页分组标题
|
||||
///
|
||||
/// 用于在列表中区分配置分组,如「外观」、「通知」。
|
||||
/// 文字颜色跟随 [ColorScheme.primary],自带上下留白。
|
||||
///
|
||||
/// 用法:
|
||||
/// ```dart
|
||||
/// const SettingsSectionHeader(title: '外观')
|
||||
/// ```
|
||||
class SettingsSectionHeader extends StatelessWidget {
|
||||
const SettingsSectionHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = context.styles;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 4),
|
||||
child: Text(title, style: s.sectionLabel),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../core/ui/base/context_theme_ext.dart';
|
||||
|
||||
/// 单个主题选项行
|
||||
///
|
||||
/// 纯展示 + 事件透传,不感知任何 Provider。
|
||||
/// 由父级传入 [current] 判断选中状态,[onTap] 处理切换。
|
||||
///
|
||||
/// 用法:
|
||||
/// ```dart
|
||||
/// ThemeOptionTile(
|
||||
/// label: '黑色模式',
|
||||
/// mode: ThemeMode.dark,
|
||||
/// current: current,
|
||||
/// onTap: () => ref.read(themeViewModelProvider.notifier).setMode(ThemeMode.dark),
|
||||
/// )
|
||||
/// ```
|
||||
class ThemeOptionTile extends StatelessWidget {
|
||||
const ThemeOptionTile({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.mode,
|
||||
required this.current,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final ThemeMode mode;
|
||||
final ThemeMode current;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = context.styles;
|
||||
final isSelected = current == mode;
|
||||
|
||||
return ListTile(
|
||||
title: Text(label),
|
||||
trailing: isSelected ? Icon(Icons.check, color: s.primary) : null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user