Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Bolt Journal ⚡

## 2026-09-16 - Granular Riverpod `.select()` watching in Sub-Setting Pages **经验心得:** 在 Settings 界面各个子页面中,如果直接使用 `ref.watch(appSettingsProvider)` 监听整个 `AppSettings` 对象,任何设置的改变(例如修改 AI 接口地址或切换显示模式)都会触发所有已打开 Settings 子页面的重新构建(rebuild)。而通过使用 `ref.watch(appSettingsProvider.select(...))` 只监听子页面实际依赖的具体字段,能够在设置字段更新时仅重绘关心该字段的页面,避免无谓的全页重新构建。 **后续行动:** 在后续新增 Riverpod ConsumerWidget 或 Setting 页面时,优先使用 `.select()` 提取所需属性,减少全量 Model 监听引起的性能损耗。
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:card_settings_ui/card_settings_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:janus/models/app_settings.dart';
import 'package:janus/providers/settings_provider.dart';
import 'package:janus/shared/custom_app_settings_tile.dart';
import 'package:janus/shared/custom_appbar.dart';
Expand All @@ -18,8 +17,21 @@ class AiSettingPage extends ConsumerStatefulWidget {
class _AiSettingPageState extends ConsumerState<AiSettingPage> {
@override
Widget build(BuildContext context) {
final settings =
ref.watch(appSettingsProvider).value ?? const AppSettings();
// Performance optimization: watch specific fields via granular .select()
// instead of watching the entire appSettingsProvider. This prevents
// unnecessary full-page rebuilds when unrelated settings change.
final aiDailySummary = ref.watch(
appSettingsProvider.select((s) => s.value?.aiDailySummary ?? false),
);
final aiAnalyseReport = ref.watch(
appSettingsProvider.select((s) => s.value?.aiAnalyseReport ?? false),
);
final aiTextToTask = ref.watch(
appSettingsProvider.select((s) => s.value?.aiTextToTask ?? false),
);
final aiPicToTask = ref.watch(
appSettingsProvider.select((s) => s.value?.aiPicToTask ?? false),
);
final tt = Theme.of(context).textTheme;

return GlassScaffold(
Expand Down Expand Up @@ -103,7 +115,7 @@ class _AiSettingPageState extends ConsumerState<AiSettingPage> {
SettingsTile(
title: Text('每日日报总结'),
trailing: GlassSwitch(
value: settings.aiDailySummary,
value: aiDailySummary,
onChanged: (val) {
ref
.read(appSettingsProvider.notifier)
Expand All @@ -114,7 +126,7 @@ class _AiSettingPageState extends ConsumerState<AiSettingPage> {
SettingsTile(
title: Text('更完善的分析报告'),
trailing: GlassSwitch(
value: settings.aiAnalyseReport,
value: aiAnalyseReport,
onChanged: (val) {
ref
.read(appSettingsProvider.notifier)
Expand All @@ -126,7 +138,7 @@ class _AiSettingPageState extends ConsumerState<AiSettingPage> {
title: Text('智慧文本日程提取'),
description: Text('指拥有更强的上下文理解能力和联想推理能力'),
trailing: GlassSwitch(
value: settings.aiTextToTask,
value: aiTextToTask,
onChanged: (val) {
ref
.read(appSettingsProvider.notifier)
Expand All @@ -138,7 +150,7 @@ class _AiSettingPageState extends ConsumerState<AiSettingPage> {
title: Text('图片日程提取'),
description: Text('需要对应模型支持图片输入和图像理解'),
trailing: GlassSwitch(
value: settings.aiPicToTask,
value: aiPicToTask,
onChanged: (val) {
ref
.read(appSettingsProvider.notifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,30 @@ class FocusSettingPage extends ConsumerStatefulWidget {
class _FocusSettingPageState extends ConsumerState<FocusSettingPage> {
@override
Widget build(BuildContext context) {
final settings =
ref.watch(appSettingsProvider).value ?? const AppSettings();
// Performance optimization: watch specific fields via granular .select()
// instead of watching the entire appSettingsProvider. This prevents
// unnecessary full-page rebuilds when unrelated settings change.
final tempLeaveDuration = ref.watch(
appSettingsProvider.select(
(s) => s.value?.tempLeaveDuration ?? TempLeaveDuration.tenM,
),
);
final tempLeaveTimes = ref.watch(
appSettingsProvider.select(
(s) => s.value?.tempLeaveTimes ?? TempLeaveTimes.twice,
),
);
final focusSceneRenderMode = ref.watch(
appSettingsProvider.select(
(s) => s.value?.focusSceneRenderMode ?? FocusSceneRenderMode.rive,
),
);
final focusSceneRenderQuality = ref.watch(
appSettingsProvider.select(
(s) =>
s.value?.focusSceneRenderQuality ?? FocusSceneRenderQuality.medium,
),
);
final tt = Theme.of(context).textTheme;

return GlassScaffold(
Expand Down Expand Up @@ -73,11 +95,11 @@ class _FocusSettingPageState extends ConsumerState<FocusSettingPage> {
SettingsTile(
title: Text('单次暂离时长'),
trailing: GlassPullDownButton(
label: settings.tempLeaveDuration.label,
label: tempLeaveDuration.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: TempLeaveDuration.values.map((duration) {
final isSelected = settings.tempLeaveDuration == duration;
final isSelected = tempLeaveDuration == duration;
return GlassMenuItem(
title: duration.label,
icon: Icon(
Expand All @@ -97,11 +119,11 @@ class _FocusSettingPageState extends ConsumerState<FocusSettingPage> {
SettingsTile(
title: Text('单次专注最大暂离次数'),
trailing: GlassPullDownButton(
label: settings.tempLeaveTimes.label,
label: tempLeaveTimes.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: TempLeaveTimes.values.map((times) {
final isSelected = settings.tempLeaveTimes == times;
final isSelected = tempLeaveTimes == times;
return GlassMenuItem(
title: times.label,
icon: Icon(
Expand All @@ -128,11 +150,11 @@ class _FocusSettingPageState extends ConsumerState<FocusSettingPage> {
trailing: GlassPullDownButton(
//TODO: 添加渲染引擎描述和建议
// TODO: 可以适当添加商标icon
label: settings.focusSceneRenderMode.label,
label: focusSceneRenderMode.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: FocusSceneRenderMode.values.map((mode) {
final isSelected = settings.focusSceneRenderMode == mode;
final isSelected = focusSceneRenderMode == mode;
return GlassMenuItem(
title: mode.label,
icon: Icon(
Expand All @@ -152,12 +174,11 @@ class _FocusSettingPageState extends ConsumerState<FocusSettingPage> {
SettingsTile(
title: Text('场景渲染质量'),
trailing: GlassPullDownButton(
label: settings.focusSceneRenderQuality.label,
label: focusSceneRenderQuality.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: FocusSceneRenderQuality.values.map((quality) {
final isSelected =
settings.focusSceneRenderQuality == quality;
final isSelected = focusSceneRenderQuality == quality;
return GlassMenuItem(
title: quality.label,
icon: Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,28 @@ class _NotificationSettingPageState
extends ConsumerState<NotificationSettingPage> {
@override
Widget build(BuildContext context) {
final settings =
ref.watch(appSettingsProvider).value ?? const AppSettings();
// Performance optimization: watch specific fields via granular .select()
// instead of watching the entire appSettingsProvider. This prevents
// unnecessary full-page rebuilds when unrelated settings change.
final isNotificationEnabled = ref.watch(
appSettingsProvider.select(
(s) => s.value?.isNotificationEnabled ?? false,
),
);
final urgentNotificationStyle = ref.watch(
appSettingsProvider.select(
(s) =>
s.value?.urgentNotificationStyle ??
UrgentNotificationStyle.notifier,
),
);
final approachingNotificationStyle = ref.watch(
appSettingsProvider.select(
(s) =>
s.value?.approachingNotificationStyle ??
ApproachingNotificationStyle.notifier,
),
);
final tt = Theme.of(context).textTheme;

return GlassScaffold(
Expand All @@ -36,7 +56,7 @@ class _NotificationSettingPageState
title: Text('是否通知'),
leading: Icon(Icons.notifications_rounded),
trailing: GlassSwitch(
value: settings.isNotificationEnabled,
value: isNotificationEnabled,
onChanged: (status) {
ref
.read(appSettingsProvider.notifier)
Expand All @@ -52,13 +72,12 @@ class _NotificationSettingPageState
SettingsTile(
title: Text('紧迫通知方式'),
trailing: GlassPullDownButton(
icon: Icon(settings.urgentNotificationStyle.icon),
label: settings.urgentNotificationStyle.label,
icon: Icon(urgentNotificationStyle.icon),
label: urgentNotificationStyle.label,
buttonWidth: 160,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: UrgentNotificationStyle.values.map((mode) {
final isSelected =
settings.urgentNotificationStyle == mode;
final isSelected = urgentNotificationStyle == mode;
return GlassMenuItem(
title: mode.label,
icon: Icon(
Expand All @@ -78,13 +97,12 @@ class _NotificationSettingPageState
SettingsTile(
title: Text('临近通知方式'),
trailing: GlassPullDownButton(
icon: Icon(settings.approachingNotificationStyle.icon),
label: settings.approachingNotificationStyle.label,
icon: Icon(approachingNotificationStyle.icon),
label: approachingNotificationStyle.label,
buttonWidth: 160,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: ApproachingNotificationStyle.values.map((mode) {
final isSelected =
settings.approachingNotificationStyle == mode;
final isSelected = approachingNotificationStyle == mode;
return GlassMenuItem(
title: mode.label,
icon: Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,24 @@ class PlanningSettingPage extends ConsumerStatefulWidget {
class _PlanningSettingPageState extends ConsumerState<PlanningSettingPage> {
@override
Widget build(BuildContext context) {
final settings =
ref.watch(appSettingsProvider).value ?? const AppSettings();
// Performance optimization: watch specific fields via granular .select()
// instead of watching the entire appSettingsProvider. This prevents
// unnecessary full-page rebuilds when unrelated settings change.
final workingDayTaskDensity = ref.watch(
appSettingsProvider.select(
(s) => s.value?.workingDayTaskDensity ?? WorkingDayTaskDensity.medium,
),
);
final restDayTaskDensity = ref.watch(
appSettingsProvider.select(
(s) => s.value?.restDayTaskDensity ?? RestDayTaskDensity.loose,
),
);
final planningHorizon = ref.watch(
appSettingsProvider.select(
(s) => s.value?.planningHorizon ?? PlanningHorizon.weeks,
),
);
final tt = Theme.of(context).textTheme;
return GlassScaffold(
topEdgeFade: false,
Expand Down Expand Up @@ -81,12 +97,11 @@ class _PlanningSettingPageState extends ConsumerState<PlanningSettingPage> {
SettingsTile(
title: Text('工作日'),
trailing: GlassPullDownButton(
label: settings.workingDayTaskDensity.label,
label: workingDayTaskDensity.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: WorkingDayTaskDensity.values.map((density) {
final isSelected =
settings.workingDayTaskDensity == density;
final isSelected = workingDayTaskDensity == density;
return GlassMenuItem(
title: density.label,
icon: Icon(
Expand All @@ -106,11 +121,11 @@ class _PlanningSettingPageState extends ConsumerState<PlanningSettingPage> {
SettingsTile(
title: Text('休息日'),
trailing: GlassPullDownButton(
label: settings.restDayTaskDensity.label,
label: restDayTaskDensity.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: RestDayTaskDensity.values.map((density) {
final isSelected = settings.restDayTaskDensity == density;
final isSelected = restDayTaskDensity == density;
return GlassMenuItem(
title: density.label,
icon: Icon(
Expand All @@ -137,11 +152,11 @@ class _PlanningSettingPageState extends ConsumerState<PlanningSettingPage> {
title: Text('跨度'),
description: Text('规划跨度越长,安排越详细,但用户需要更提前输入足够多的日程以供计算。'),
trailing: GlassPullDownButton(
label: settings.planningHorizon.label,
label: planningHorizon.label,
buttonWidth: 120,
buttonShape: const LiquidRoundedRectangle(borderRadius: 64),
items: PlanningHorizon.values.map((horizon) {
final isSelected = settings.planningHorizon == horizon;
final isSelected = planningHorizon == horizon;
return GlassMenuItem(
title: horizon.label,
icon: Icon(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'package:card_settings_ui/card_settings_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:janus/models/app_settings.dart';
import 'package:janus/providers/settings_provider.dart';
import 'package:janus/theme/theme.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.dart';
import 'package:janus/shared/custom_appbar.dart';
import 'package:janus/shared/custom_app_settings_tile.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
Expand All @@ -21,8 +19,12 @@ class StorageSettingPage extends ConsumerStatefulWidget {
class _StorageSettingPageState extends ConsumerState<StorageSettingPage> {
@override
Widget build(BuildContext context) {
final settings =
ref.watch(appSettingsProvider).value ?? const AppSettings();
// Performance optimization: watch specific fields via granular .select()
// instead of watching the entire appSettingsProvider. This prevents
// unnecessary full-page rebuilds when unrelated settings change.
final useLogToTrain = ref.watch(
appSettingsProvider.select((s) => s.value?.useLogToTrain ?? false),
);
final tt = Theme.of(context).textTheme;
return GlassScaffold(
topEdgeFade: false,
Expand Down Expand Up @@ -89,7 +91,7 @@ class _StorageSettingPageState extends ConsumerState<StorageSettingPage> {
title: Text('本地模型训练'),
description: Text('是否允许用于本地小型神经网络训练?'),
trailing: GlassSwitch(
value: settings.useLogToTrain,
value: useLogToTrain,
onChanged: (status) {
ref
.read(appSettingsProvider.notifier)
Expand Down
Loading
Loading