Skip to content

Commit 3646797

Browse files
shadowfish07claude
andauthored
fix: 修复 FAB 滚动隐藏功能在页面切换后失效的问题 (#86)
* fix: 修复 FAB 滚动隐藏功能在页面切换后失效的问题 修复了 FloatingActionButton 在从未读页切换到每日阅读页后, 下滑隐藏功能失效的问题。 主要变更: - 重构 FAB 管理架构,从全局 Provider 模式改为页面级管理 - 修复 ScrollController 绑定时机问题,确保滚动监听器正常工作 - 为所有书签列表页面(未读、阅读中、已归档、收藏)添加 FAB 支持 - 优化 ScrollController 生命周期管理 影响页面: - 每日阅读页面 - 未读书签页面 - 阅读中书签页面 - 已归档书签页面 - 收藏书签页面 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(书签列表): 修复滚动控制器内存泄漏问题 修复书签列表页面和FAB组件中滚动控制器的监听管理问题 确保在组件更新和销毁时正确解绑监听器 添加所有权标志来正确管理控制器生命周期 * test(测试): 添加MainAppViewModel和AboutViewModel的mock支持 在daily_read_screen_test和reading_screen_test测试文件中添加了对MainAppViewModel和AboutViewModel的mock支持,以支持更全面的测试场景 * test(app_update): 使用测试日志助手简化测试日志配置 将测试中的日志配置改为使用测试专用的SimpleTestPrinter和MultiOutput,避免测试时产生不必要的输出。同时添加setupTestGroupLogging()来统一管理测试组的日志设置。 * fix(daily_read): 修复滚动控制器和错误订阅的内存泄漏问题 确保在组件销毁时正确释放滚动控制器和所有错误订阅 添加对组件挂载状态的检查以避免在未挂载时更新UI --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 89fb145 commit 3646797

File tree

14 files changed

+843
-278
lines changed

14 files changed

+843
-278
lines changed

lib/routing/router.dart

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,6 @@ GoRouter router(SettingsRepository settingsRepository) => GoRouter(
7070
final title =
7171
_getTitleForRoute(state.fullPath ?? state.matchedLocation);
7272

73-
// 检查是否为书签列表页面,需要显示FAB
74-
final isBookmarkListRoute = [
75-
Routes.dailyRead,
76-
Routes.unarchived,
77-
Routes.reading,
78-
Routes.archived,
79-
Routes.marked,
80-
].contains(state.fullPath ?? state.matchedLocation);
81-
8273
// 从设置页返回,跳转首页
8374
if ((state.fullPath ?? state.matchedLocation) ==
8475
Routes.settings) {
@@ -95,9 +86,22 @@ GoRouter router(SettingsRepository settingsRepository) => GoRouter(
9586
);
9687
}
9788

89+
// 书签列表页面不被 MainLayout 包装,它们自己管理 UI
90+
final selfManagedRoutes = [
91+
Routes.dailyRead,
92+
Routes.unarchived,
93+
Routes.reading,
94+
Routes.archived,
95+
Routes.marked,
96+
];
97+
98+
if (selfManagedRoutes
99+
.contains(state.fullPath ?? state.matchedLocation)) {
100+
return child;
101+
}
102+
98103
return MainLayout(
99104
title: title,
100-
showFab: isBookmarkListRoute,
101105
child: child,
102106
);
103107
},

lib/ui/bookmarks/widget/archived_screen.dart

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
11
import 'package:flutter/material.dart';
22
import 'package:readeck_app/ui/bookmarks/view_models/bookmarks_viewmodel.dart';
33
import 'package:readeck_app/ui/bookmarks/widget/bookmark_list_screen.dart';
4+
import 'package:readeck_app/ui/core/main_layout.dart';
45

5-
class ArchivedScreen extends StatelessWidget {
6+
class ArchivedScreen extends StatefulWidget {
67
const ArchivedScreen({super.key, required this.viewModel});
78

89
final ArchivedViewmodel viewModel;
910

11+
@override
12+
State<ArchivedScreen> createState() => _ArchivedScreenState();
13+
}
14+
15+
class _ArchivedScreenState extends State<ArchivedScreen> {
16+
late final ScrollController _scrollController;
17+
18+
@override
19+
void initState() {
20+
super.initState();
21+
_scrollController = ScrollController();
22+
}
23+
24+
@override
25+
void dispose() {
26+
_scrollController.dispose();
27+
super.dispose();
28+
}
29+
1030
@override
1131
Widget build(BuildContext context) {
12-
return BookmarkListScreen(
13-
viewModel: viewModel,
14-
texts: const BookmarkListTexts(
15-
loadingText: '正在加载已归档书签',
16-
errorMessage: '已归档书签加载失败',
17-
emptyIcon: Icons.archive_outlined,
18-
emptyTitle: '暂无已归档书签',
19-
emptySubtitle: '归档的书签将在这里显示',
32+
return MainLayout(
33+
title: '已归档',
34+
showFab: true,
35+
scrollController: _scrollController,
36+
child: BookmarkListScreen(
37+
viewModel: widget.viewModel,
38+
scrollController: _scrollController,
39+
texts: const BookmarkListTexts(
40+
loadingText: '正在加载已归档书签',
41+
errorMessage: '已归档书签加载失败',
42+
emptyIcon: Icons.archive_outlined,
43+
emptyTitle: '暂无已归档书签',
44+
emptySubtitle: '归档的书签将在这里显示',
45+
),
2046
),
2147
);
2248
}

lib/ui/bookmarks/widget/bookmark_list_screen.dart

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import 'package:readeck_app/ui/core/ui/loading.dart';
1010
import 'package:readeck_app/ui/bookmarks/view_models/bookmarks_viewmodel.dart';
1111
import 'package:readeck_app/utils/network_error_exception.dart';
1212
import 'package:readeck_app/ui/core/ui/snack_bar_helper.dart';
13-
import 'package:readeck_app/ui/core/main_layout.dart';
1413

1514
/// 书签列表页面的文案配置
1615
class BookmarkListTexts {
@@ -45,10 +44,12 @@ class BookmarkListScreen<T extends BaseBookmarksViewmodel>
4544
super.key,
4645
required this.viewModel,
4746
required this.texts,
47+
this.scrollController,
4848
});
4949

5050
final T viewModel;
5151
final BookmarkListTexts texts;
52+
final ScrollController? scrollController;
5253

5354
@override
5455
State<BookmarkListScreen<T>> createState() => _BookmarkListScreenState<T>();
@@ -57,6 +58,7 @@ class BookmarkListScreen<T extends BaseBookmarksViewmodel>
5758
class _BookmarkListScreenState<T extends BaseBookmarksViewmodel>
5859
extends State<BookmarkListScreen<T>> {
5960
ScrollController? _scrollController;
61+
bool _ownsController = false;
6062
late final ListenableSubscription _deleteSuccessSubscription;
6163

6264
@override
@@ -92,37 +94,37 @@ class _BookmarkListScreenState<T extends BaseBookmarksViewmodel>
9294
void didChangeDependencies() {
9395
super.didChangeDependencies();
9496

95-
// 只有在没有外部提供ScrollController时才创建自己的
97+
// 使用外部提供的 ScrollController 或创建自己的(仅首次绑定)
9698
if (_scrollController == null) {
97-
_scrollController = ScrollController();
98-
_scrollController?.addListener(_onScroll);
99-
100-
// 延迟到下一帧更新Provider,避免在build过程中触发rebuild
101-
WidgetsBinding.instance.addPostFrameCallback((_) {
102-
if (mounted) {
103-
try {
104-
final provider = context.read<ScrollControllerProvider?>();
105-
provider?.setScrollController(_scrollController);
106-
} catch (e) {
107-
// 在测试环境中可能会失败,忽略
108-
}
109-
}
110-
});
99+
_scrollController = widget.scrollController ?? ScrollController();
100+
_scrollController!.addListener(_onScroll);
101+
_ownsController = widget.scrollController == null;
111102
}
112103
}
113104

114105
@override
115-
void dispose() {
116-
// 清除Provider中的ScrollController引用
117-
try {
118-
final provider = context.read<ScrollControllerProvider?>();
119-
provider?.setScrollController(null);
120-
} catch (e) {
121-
// 在测试或context已失效时忽略错误
106+
void didUpdateWidget(covariant BookmarkListScreen<T> oldWidget) {
107+
super.didUpdateWidget(oldWidget);
108+
if (oldWidget.scrollController != widget.scrollController) {
109+
// 解绑旧
110+
_scrollController?.removeListener(_onScroll);
111+
if (_ownsController) {
112+
_scrollController?.dispose();
113+
}
114+
// 绑定新
115+
_scrollController = widget.scrollController ?? ScrollController();
116+
_ownsController = widget.scrollController == null;
117+
_scrollController!.addListener(_onScroll);
122118
}
119+
}
123120

121+
@override
122+
void dispose() {
124123
_scrollController?.removeListener(_onScroll);
125-
_scrollController?.dispose();
124+
// 只有自己创建的 ScrollController 才需要 dispose
125+
if (_ownsController) {
126+
_scrollController?.dispose();
127+
}
126128
_deleteSuccessSubscription.cancel();
127129
super.dispose();
128130
}

lib/ui/bookmarks/widget/marked_screen.dart

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
11
import 'package:flutter/material.dart';
22
import 'package:readeck_app/ui/bookmarks/view_models/bookmarks_viewmodel.dart';
33
import 'package:readeck_app/ui/bookmarks/widget/bookmark_list_screen.dart';
4+
import 'package:readeck_app/ui/core/main_layout.dart';
45

5-
class MarkedScreen extends StatelessWidget {
6+
class MarkedScreen extends StatefulWidget {
67
const MarkedScreen({super.key, required this.viewModel});
78

89
final MarkedViewmodel viewModel;
910

11+
@override
12+
State<MarkedScreen> createState() => _MarkedScreenState();
13+
}
14+
15+
class _MarkedScreenState extends State<MarkedScreen> {
16+
late final ScrollController _scrollController;
17+
18+
@override
19+
void initState() {
20+
super.initState();
21+
_scrollController = ScrollController();
22+
}
23+
24+
@override
25+
void dispose() {
26+
_scrollController.dispose();
27+
super.dispose();
28+
}
29+
1030
@override
1131
Widget build(BuildContext context) {
12-
return BookmarkListScreen(
13-
viewModel: viewModel,
14-
texts: const BookmarkListTexts(
15-
loadingText: '正在加载喜爱书签',
16-
errorMessage: '喜爱书签加载失败',
17-
emptyIcon: Icons.favorite_outline,
18-
emptyTitle: '暂无喜爱书签',
19-
emptySubtitle: '标记为喜爱的书签将在这里显示',
32+
return MainLayout(
33+
title: '标记喜爱',
34+
showFab: true,
35+
scrollController: _scrollController,
36+
child: BookmarkListScreen(
37+
viewModel: widget.viewModel,
38+
scrollController: _scrollController,
39+
texts: const BookmarkListTexts(
40+
loadingText: '正在加载喜爱书签',
41+
errorMessage: '喜爱书签加载失败',
42+
emptyIcon: Icons.favorite_outline,
43+
emptyTitle: '暂无喜爱书签',
44+
emptySubtitle: '标记为喜爱的书签将在这里显示',
45+
),
2046
),
2147
);
2248
}

lib/ui/bookmarks/widget/reading_screen.dart

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
11
import 'package:flutter/material.dart';
22
import 'package:readeck_app/ui/bookmarks/view_models/bookmarks_viewmodel.dart';
33
import 'package:readeck_app/ui/bookmarks/widget/bookmark_list_screen.dart';
4+
import 'package:readeck_app/ui/core/main_layout.dart';
45

5-
class ReadingScreen extends StatelessWidget {
6+
class ReadingScreen extends StatefulWidget {
67
const ReadingScreen({super.key, required this.viewModel});
78

89
final ReadingViewmodel viewModel;
910

11+
@override
12+
State<ReadingScreen> createState() => _ReadingScreenState();
13+
}
14+
15+
class _ReadingScreenState extends State<ReadingScreen> {
16+
late final ScrollController _scrollController;
17+
18+
@override
19+
void initState() {
20+
super.initState();
21+
_scrollController = ScrollController();
22+
}
23+
24+
@override
25+
void dispose() {
26+
_scrollController.dispose();
27+
super.dispose();
28+
}
29+
1030
@override
1131
Widget build(BuildContext context) {
12-
return BookmarkListScreen(
13-
viewModel: viewModel,
14-
texts: const BookmarkListTexts(
15-
loadingText: '正在加载阅读中书签',
16-
errorMessage: '阅读中书签加载失败',
17-
emptyIcon: Icons.auto_stories_outlined,
18-
emptyTitle: '暂无阅读中书签',
19-
emptySubtitle: '下拉刷新或去Readeck开始阅读书签',
32+
return MainLayout(
33+
title: '阅读中',
34+
showFab: true,
35+
scrollController: _scrollController,
36+
child: BookmarkListScreen(
37+
viewModel: widget.viewModel,
38+
scrollController: _scrollController,
39+
texts: const BookmarkListTexts(
40+
loadingText: '正在加载阅读中书签',
41+
errorMessage: '阅读中书签加载失败',
42+
emptyIcon: Icons.auto_stories_outlined,
43+
emptyTitle: '暂无阅读中书签',
44+
emptySubtitle: '下拉刷新或去Readeck开始阅读书签',
45+
),
2046
),
2147
);
2248
}

lib/ui/bookmarks/widget/unarchived_screen.dart

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
11
import 'package:flutter/material.dart';
22
import 'package:readeck_app/ui/bookmarks/view_models/bookmarks_viewmodel.dart';
33
import 'package:readeck_app/ui/bookmarks/widget/bookmark_list_screen.dart';
4+
import 'package:readeck_app/ui/core/main_layout.dart';
45

5-
class UnarchivedScreen extends StatelessWidget {
6+
class UnarchivedScreen extends StatefulWidget {
67
const UnarchivedScreen({super.key, required this.viewModel});
78

89
final UnarchivedViewmodel viewModel;
910

11+
@override
12+
State<UnarchivedScreen> createState() => _UnarchivedScreenState();
13+
}
14+
15+
class _UnarchivedScreenState extends State<UnarchivedScreen> {
16+
late final ScrollController _scrollController;
17+
18+
@override
19+
void initState() {
20+
super.initState();
21+
_scrollController = ScrollController();
22+
}
23+
24+
@override
25+
void dispose() {
26+
_scrollController.dispose();
27+
super.dispose();
28+
}
29+
1030
@override
1131
Widget build(BuildContext context) {
12-
return BookmarkListScreen(
13-
viewModel: viewModel,
14-
texts: const BookmarkListTexts(
15-
loadingText: '正在加载未归档书签',
16-
errorMessage: '未归档书签加载失败',
17-
emptyIcon: Icons.inbox_outlined,
18-
emptyTitle: '暂无未归档书签',
19-
emptySubtitle: '下拉刷新或去Readeck添加新的书签',
32+
return MainLayout(
33+
title: '未读',
34+
showFab: true,
35+
scrollController: _scrollController,
36+
child: BookmarkListScreen(
37+
viewModel: widget.viewModel,
38+
scrollController: _scrollController,
39+
texts: const BookmarkListTexts(
40+
loadingText: '正在加载未归档书签',
41+
errorMessage: '未归档书签加载失败',
42+
emptyIcon: Icons.inbox_outlined,
43+
emptyTitle: '暂无未归档书签',
44+
emptySubtitle: '下拉刷新或去Readeck添加新的书签',
45+
),
2046
),
2147
);
2248
}

0 commit comments

Comments
 (0)