Skip to content

Conversation

@shadowfish07
Copy link
Owner

@shadowfish07 shadowfish07 commented Aug 25, 2025

概述

修复了 FloatingActionButton 在页面切换后滚动隐藏功能失效的问题。

Closes #85

问题背景

FAB 的下滑隐藏功能在从未读页切换到每日阅读页后会失效,影响用户体验的一致性。

解决方案

🔧 架构重构

从全局 Provider 模式改为页面级管理

  • ❌ 旧架构:使用 ScrollControllerProvider 全局共享 ScrollController
  • ✅ 新架构:每个页面独立管理自己的 ScrollController 和 FAB

🎯 关键修改

  1. 移除全局 Provider 依赖

    • 删除 ScrollControllerProvider
    • MainLayout 直接接受 scrollController 参数
  2. 优化 ScrollController 绑定

    • FAB 组件在 build 方法中直接绑定监听器
    • 避免异步时机问题,确保监听器正常工作
  3. 统一页面架构

    • 所有书签列表页面采用相同的 FAB 管理模式
    • 确保功能一致性和可维护性

技术细节

修改的文件

核心组件

  • lib/ui/core/ui/bookmark_list_fab.dart - FAB 组件重构
  • lib/ui/core/main_layout.dart - 移除 Provider,支持直接传参

页面层

  • lib/ui/daily_read/widgets/daily_read_screen.dart - 自管理 FAB
  • lib/ui/bookmarks/widget/unarchived_screen.dart - 添加 FAB 支持
  • lib/ui/bookmarks/widget/reading_screen.dart - 添加 FAB 支持
  • lib/ui/bookmarks/widget/archived_screen.dart - 添加 FAB 支持
  • lib/ui/bookmarks/widget/marked_screen.dart - 添加 FAB 支持

架构支持

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart - 支持外部 ScrollController
  • lib/routing/router.dart - 更新路由配置,避免双重包装

关键代码实现

FAB 组件监听器绑定

@override
Widget build(BuildContext context) {
  // 简单直接地绑定监听器
  if (widget.scrollController != null) {
    widget.scrollController!.removeListener(_onScroll);
    widget.scrollController!.addListener(_onScroll);
  }
  
  return ScaleTransition(
    scale: _animation,
    child: FloatingActionButton(
      onPressed: _onFabPressed,
      tooltip: '添加书签',
      child: const Icon(Icons.add),
    ),
  );
}

页面级管理模式

class _UnarchivedScreenState extends State<UnarchivedScreen> {
  late final ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
  }

  @override
  Widget build(BuildContext context) {
    return MainLayout(
      title: '未读',
      showFab: true,
      scrollController: _scrollController,
      child: BookmarkListScreen(
        viewModel: widget.viewModel,
        scrollController: _scrollController,
        // ...
      ),
    );
  }
}

测试验证

✅ 功能验证

  • 每日阅读页面 FAB 滚动功能正常
  • 未读页面 FAB 滚动功能正常
  • 阅读中页面 FAB 滚动功能正常
  • 已归档页面 FAB 滚动功能正常
  • 收藏页面 FAB 滚动功能正常
  • 页面切换后功能保持正常

✅ 代码质量

  • Flutter analyze 通过(无警告和错误)
  • 代码格式化符合项目规范
  • 遵循项目架构模式

影响评估

🎉 正面影响

  • 修复了影响用户体验的滚动功能问题
  • 统一了所有书签列表页面的 FAB 行为
  • 简化了架构,提高了可维护性
  • 消除了时机竞态条件,提升了稳定性

⚠️ 潜在风险

  • 低风险:从全局 Provider 改为页面级管理,改动较大但架构更清晰
  • 缓解措施:充分测试所有相关页面,确保功能正常

检查清单

Summary by CodeRabbit

  • 新功能
    • 部分书签页与“每日阅读”页采用主布局呈现页面标题并支持与列表滚动联动的悬浮操作按钮(FAB)。
  • 修复
    • 优化 FAB 可见性逻辑:更稳健的上/下滚动与靠近顶部判定,减少误触发与抖动。
    • 某些书签类路由现在由自管理路线处理,不再通过主布局包裹,FAB 展示行为随之变化。
  • 重构
    • 简化滚动控制与依赖注入,移除共享提供者,提升稳定性与可维护性。
  • 测试
    • 更新并扩展测试与模拟以适配新的页面结构与依赖变更。

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 25, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

将全局 Provider 管理的 ScrollController 重构为页面自管理:书签列表页各自创建并共享 ScrollController 给 MainLayout 与 BookmarkListScreen,MainLayout 直接传给 BookmarkListFab;FAB 的监听在 build/didUpdateWidget 中绑定/重绑定;路由新增 selfManagedRoutes,部分路由绕过统一 MainLayout 包裹。

Changes

Cohort / File(s) Summary
路由自管理调整
lib/routing/router.dart
为每日阅读/未读/阅读中/已归档/收藏配置 selfManagedRoutes,绕过统一 MainLayout 包裹,移除 isBookmarkListRouteMainLayout.showFab 的路由判断。
书签列表页改为状态组件并共享滚动控制器
lib/ui/bookmarks/widget/archived_screen.dart, lib/ui/bookmarks/widget/marked_screen.dart, lib/ui/bookmarks/widget/reading_screen.dart, lib/ui/bookmarks/widget/unarchived_screen.dart
各 Screen 从 Stateless → Stateful,新增 ScrollController 生命周期(initState/dispose);build 改为以 MainLayout 包裹并将同一 ScrollController 传入 MainLayout 与 BookmarkListScreen。
列表组件支持外部 ScrollController
lib/ui/bookmarks/widget/bookmark_list_screen.dart
BookmarkListScreen 增加可选公有构造参数 scrollController;优先使用外部控制器,内部按需创建并仅在拥有时负责 dispose;统一监听绑定/移除;移除对 Provider 的依赖。
主布局去 Provider 化并直连 FAB
lib/ui/core/main_layout.dart
移除 ScrollControllerProvider 与 Consumer 用法;MainLayout 新增可选 scrollController 字段并接收构造参数;若 showFab 且提供控制器则直接构建 BookmarkListFab(scrollController: ...)
FAB 监听与显示策略重写
lib/ui/core/ui/bookmark_list_fab.dart
移除原有 attach/detach helper,改为直接在 initState/didUpdateWidget/dispose 中绑定/解绑控制器监听;阈值由 3→5;调整顶端强制显示与上下滑动显示/隐藏逻辑;didUpdateWidget 重置状态并触发动画。
每日阅读页重构与测试更新
lib/ui/daily_read/widgets/daily_read_screen.dart, test/ui/daily_read/widgets/daily_read_screen_test.dart
DailyReadScreen 改为自管理 ScrollController,包裹 MainLayout 并传入控制器与 showFab;相应测试移除 ScrollControllerProvider,增加并注入 MainAppViewModelAboutViewModel mocks。
测试 mocks 增补与重复
test/ui/bookmarks/widget/reading_screen_test.mocks.dart, test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart
为测试增加 MockMainAppViewModelMockAboutViewModel;部分 mocks 文件包含重复生成的类定义(重复块)。
测试日志调整
test/unit/domain/use_cases/app_update_use_case_test.dart
引入测试日志 helper 并将测试日志配置设为静默输出。

Sequence Diagram(s)

sequenceDiagram
  participant Router
  participant Screen as BookmarkScreen(Stateful)
  participant MainLayout
  participant BookmarkListScreen
  participant BookmarkListFab

  Router->>Screen: 构建 self-managed 书签页面
  activate Screen
  Screen->>Screen: initState 创建 ScrollController
  Screen->>MainLayout: build(title, scrollController, child)
  Screen->>BookmarkListScreen: build(viewModel, scrollController)
  activate MainLayout
  MainLayout->>BookmarkListFab: 构建并传入 scrollController
  Note over BookmarkListFab: 在 initState/didUpdateWidget 中绑定/重绑定监听\n阈值调整为 5;顶部强制显示
  deactivate MainLayout

  BookmarkListScreen-->>BookmarkListFab: 用户滚动事件(同一 controller)
  BookmarkListFab->>BookmarkListFab: _onScroll: 顶部显示 / 下滑隐藏 / 上滑显示

  Router->>Screen: 页面切换
  Screen->>BookmarkListFab: didUpdateWidget → 重新绑定 & 重置状态
Loading

Assessment against linked issues

Objective Addressed Explanation
FAB 在所有书签列表页随滚动隐藏/显示(#85
页面顶部始终显示 FAB(#85
页面切换后 FAB 隐藏功能保持正常(#85
修复 ScrollController 绑定时机/竞态问题(移除 Provider、确保监听绑定与更新)(#85

Out-of-scope changes

Code Change Explanation
添加并重复生成测试 mocks(test/ui/bookmarks/widget/reading_screen_test.mocks.dart mocks 文件包含重复类定义块,属于测试生成器/脚手架问题,与 #85 的 FAB/ScrollController 修复目标无直接关系。
在多个测试中增加对 AboutViewModel 与 MainAppViewModel 的 mock 注入(test/ui/daily_read/widgets/daily_read_screen_test.dart, test/ui/bookmarks/widget/reading_screen_test.dart 这些测试 wiring 的扩展是测试覆盖/依赖调整,与解决 FAB 绑定竞态问题的核心修正无直接关系。

Possibly related issues

Poem

小兔举灯跃纸屏,
滚轮轻拨影无形。
上滑显, 下滑藏,
路由自理任风行。
FAB 随行如萤星 ✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/fab-scroll-functionality

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @shadowfish07, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在修复 FloatingActionButton (FAB) 在页面切换后滚动隐藏功能失效的问题。通过将 FAB 的滚动控制器管理从全局 Provider 模式重构为页面级独立管理,解决了因控制器共享导致的失效问题,提升了用户体验的一致性和系统的稳定性。

Highlights

  • 架构重构: 将 FloatingActionButton (FAB) 的 ScrollController 管理从全局 ScrollControllerProvider 模式改为每个页面独立管理,从而解决了页面切换后 FAB 滚动隐藏功能失效的问题。
  • 移除全局依赖: 删除了旧的全局 ScrollControllerProvider 类,MainLayout 组件现在直接接收 ScrollController 参数,简化了依赖管理。
  • 优化监听器绑定: BookmarkListFab 组件的滚动监听器现在直接在 build 方法中绑定,确保了监听器的及时和正确工作,避免了异步时机问题。
  • 统一页面行为: 所有书签列表页面(如每日阅读、未读、阅读中、已归档、收藏)都已更新,以独立管理其 FAB 及其滚动行为,确保了功能的一致性和可维护性。
  • 路由配置更新: 路由配置已调整,使书签列表页面不再被 MainLayout 包装,而是自行管理其 UI,避免了双重包装和潜在的冲突。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 旨在修复页面切换后 FAB 滚动隐藏功能失效的问题,通过将 ScrollController 的管理从全局 Provider 模式重构为页面级管理,解决方案思路清晰且正确。代码重构范围较大,但提升了页面的封装性和可维护性。

我发现了一些可以改进的地方:

  1. BookmarkListFab 中,ScrollController 监听器的管理方式可以优化。当前在 build 方法中操作监听器是 Flutter 的反模式,可能引起性能问题。建议使用 initState, didUpdateWidgetdispose 来管理监听器生命周期。
  2. BookmarkListScreen 中,对外部传入的 scrollController 的处理存在缺陷。当 widget.scrollController 发生变化时,组件内部状态不会随之更新,可能导致 bug。这也需要通过 didUpdateWidget 来正确处理。

除了以上两点,其余的修改都很好地完成了重构目标。修复这些问题将使代码更加健壮和高效。

Comment on lines 96 to 98
// 使用外部提供的 ScrollController 或创建自己的
_scrollController ??= widget.scrollController ?? ScrollController();
_scrollController?.addListener(_onScroll);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前在 didChangeDependencies 中初始化 _scrollController 的方式存在一个潜在的 bug。如果父组件重建并传入一个新的 scrollController 实例,_scrollController 不会更新,因为它只在 null 的时候被赋值一次。这会导致 BookmarkListScreen 仍然使用旧的 controller,与父组件(如 MainLayoutBookmarkListFab)使用的 controller 不一致。

为了正确处理 scrollController 的更新,建议采用标准的 StatefulWidget 生命周期来管理 controller

  1. initState 中初始化 _scrollController
  2. 实现 didUpdateWidget,在 widget.scrollController 改变时,更新 _scrollController 并正确处理监听器的移除和添加。
  3. 相应地更新 dispose 逻辑。

例如:

class _BookmarkListScreenState<T extends BaseBookmarksViewmodel>
    extends State<BookmarkListScreen<T>> {
  ScrollController? _scrollController;
  bool _isInternalController = false;

  @override
  void initState() {
    super.initState();
    _setupController();
    // ... other initState logic
  }

  @override
  void didUpdateWidget(BookmarkListScreen<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.scrollController != oldWidget.scrollController) {
      _cleanupController();
      _setupController();
    }
  }

  void _setupController() {
    if (widget.scrollController == null) {
      _scrollController = ScrollController();
      _isInternalController = true;
    } else {
      _scrollController = widget.scrollController;
      _isInternalController = false;
    }
    _scrollController!.addListener(_onScroll);
  }

  void _cleanupController() {
    _scrollController?.removeListener(_onScroll);
    if (_isInternalController) {
      _scrollController?.dispose();
    }
  }

  @override
  void dispose() {
    _cleanupController();
    // ... other dispose logic
    super.dispose();
  }
  // ...
}

这样可以确保 BookmarkListScreen 始终使用正确的 ScrollController 实例。

Comment on lines 123 to 127
// 简单直接地绑定监听器
if (widget.scrollController != null) {
widget.scrollController!.removeListener(_onScroll);
widget.scrollController!.addListener(_onScroll);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

build 方法中添加和移除监听器是一种反模式(anti-pattern),可能会导致性能问题,因为 build 方法会被频繁调用。

推荐将监听器的管理逻辑移到 State 的生命周期方法中:

  1. initState 中,为初始的 scrollController 添加监听器。
  2. didUpdateWidget 中,当 widget.scrollController 发生变化时,先从旧的 controller 移除监听器,再为新的 controller 添加监听器。
  3. dispose 中,移除监听器以防止内存泄漏。

例如:

@override
void initState() {
  super.initState();
  // ... other init logic
  widget.scrollController?.addListener(_onScroll);
}

@override
void didUpdateWidget(BookmarkListFab oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.scrollController != widget.scrollController) {
    oldWidget.scrollController?.removeListener(_onScroll);
    widget.scrollController?.addListener(_onScroll);
    // ... reset state logic from your existing didUpdateWidget
  }
}

@override
void dispose() {
  widget.scrollController?.removeListener(_onScroll);
  _animationController.dispose();
  super.dispose();
}

这个改动可以提高性能和代码的可维护性。

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/ui/daily_read/widgets/daily_read_screen.dart (1)

65-105: 请在 initState 中订阅 Command 错误并在 dispose 中取消监听

当前在 lib/ui/daily_read/widgets/daily_read_screen.dartdidChangeDependencies(约第 66 行)里,对 widget.viewModel.loadwidget.viewModel.toggleBookmarkArchivedwidget.viewModel.toggleBookmarkMarked.errors.where(...).listen(...) 进行了多次注册,但在 dispose(第 123–133 行)中并未调用对应的 cancel(),会导致内存泄漏并在页面销毁后仍触发 SnackBar。

  • 问题位置

    • didChangeDependencies 注册监听:第 66–75 行
    • dispose 中仅取消了 _deleteSuccessSubscription,未对上述错误监听取消:第 123–133 行
  • 建议改动

    1. .errors.where(...).listen(...) 迁移到 initState(),并将返回的 ListenableSubscription 保存为字段。
    2. dispose() 中对所有这些订阅调用 cancel()
    3. 在回调内首行加上 if (!mounted) return; 防止页面已卸载时操作 context。
 class _DailyReadScreenState extends State<DailyReadScreen> {
   // 新增订阅字段
+  late final ListenableSubscription _loadErrorSub;
+  late final ListenableSubscription _toggleArchiveErrorSub;
+  late final ListenableSubscription _toggleMarkedErrorSub;
 
   @override
   void initState() {
     super.initState();
+    _loadErrorSub = widget.viewModel.load.errors
+      .where((x) => x != null)
+      .listen((error, _) {
+        appLogger.e('加载书签失败', error: error);
+        if (!mounted) return;
+        SnackBarHelper.showError(context, '加载书签失败');
+      });
+    _toggleArchiveErrorSub = widget.viewModel.toggleBookmarkArchived.errors
+      .where((x) => x != null)
+      .listen((error, _) {
+        appLogger.e('切换书签归档状态失败', error: error);
+        if (!mounted) return;
+        SnackBarHelper.showError(context, '切换书签归档状态失败');
+      });
+    _toggleMarkedErrorSub = widget.viewModel.toggleBookmarkMarked.errors
+      .where((x) => x != null)
+      .listen((error, _) {
+        appLogger.e('切换书签标记状态失败', error: error);
+        if (!mounted) return;
+        SnackBarHelper.showError(context, '切换书签标记状态失败');
+      });
   }
 
-  @override
-  void didChangeDependencies() {
-    super.didChangeDependencies();
-
-    // 创建ScrollController
-    _scrollController ??= ScrollController();
-    widget.viewModel.load.errors.where((x) => x != null).listen((error, _) {
-      appLogger.e(
-        '加载书签失败',
-        error: error,
-      );
-      SnackBarHelper.showError(
-        context,
-        '加载书签失败',
-      );
-    });
-    widget.viewModel.toggleBookmarkArchived.errors
-        .where((x) => x != null)
-        .listen((error, _) {
-      appLogger.e(
-        '切换书签归档状态失败',
-        error: error,
-      );
-      SnackBarHelper.showError(
-        context,
-        '切换书签归档状态失败',
-      );
-    });
-    widget.viewModel.toggleBookmarkMarked.errors
-        .where((x) => x != null)
-        .listen((error, _) {
-      appLogger.e(
-        '切换书签标记状态失败',
-        error: error,
-      );
-      SnackBarHelper.showError(
-        context,
-        '切换书签标记状态失败',
-      );
-    });
-  }
 
   @override
   void dispose() {
+    // 取消命令错误监听订阅
+    _loadErrorSub.cancel();
+    _toggleArchiveErrorSub.cancel();
+    _toggleMarkedErrorSub.cancel();
     // 释放动画控制器
     _confettiController.dispose();
     // 释放滚动控制器
     _scrollController?.dispose();
     // 取消删除成功监听
     _deleteSuccessSubscription.cancel();
     // 清除回调
     widget.viewModel.setOnBookmarkArchivedCallback(null);
     widget.viewModel.setNavigateToDetailCallback((_) {});
     super.dispose();
   }
lib/ui/core/main_layout.dart (1)

80-100: AboutViewModel 的 addListener 未在 dispose 中移除,存在泄漏与重复提示风险

main 布局在整个 App 中出现频繁,未移除监听会造成多次弹出“发现新版本”SnackBar。建议保存回调并在 dispose 中移除。

 class _MainLayoutState extends State<MainLayout> {
   StreamSubscription<String>? _shareTextSubscription;
   bool _isProcessingShare = false;
   bool _hasUpdate = false;
+  VoidCallback? _aboutUpdateListener;
+  AboutViewModel? _aboutViewModel;

   ...

   void _setupUpdateListener() {
-    final aboutViewModel = context.read<AboutViewModel>();
-    aboutViewModel.addListener(() {
+    _aboutViewModel = context.read<AboutViewModel>();
+    _aboutUpdateListener = () {
       if (mounted && aboutViewModel.updateInfo != null && !_hasUpdate) {
         setState(() {
           _hasUpdate = true;
         });
         ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(
             content: Text('发现新版本: ${aboutViewModel.updateInfo!.version}'),
             action: SnackBarAction(
               label: '前往更新',
               onPressed: () {
                 context.go(Routes.about);
               },
             ),
           ),
         );
       }
-    });
+    };
+    _aboutViewModel!.addListener(_aboutUpdateListener!);
   }

   @override
   void dispose() {
     _shareTextSubscription?.cancel();
+    if (_aboutUpdateListener != null) {
+      _aboutViewModel?.removeListener(_aboutUpdateListener!);
+    }
     super.dispose();
   }

Also applies to: 115-118

🧹 Nitpick comments (9)
lib/ui/bookmarks/widget/bookmark_list_screen.dart (2)

115-121: 滚动触底触发翻页缺少“正在加载/无更多”保护,易重复触发 loadNextPage

当滚动位置停留在阈值区间内时,会持续触发 loadNextPage。建议在触发前判断 hasMoreDataloadMore.isExecuting,降低无谓调用。

应用如下修改:

     // 处理分页加载
     if (currentScrollPosition >=
         _scrollController!.position.maxScrollExtent - 200) {
+      // 避免在已无更多或正在加载时重复触发
+      if (!widget.viewModel.hasMoreData ||
+          widget.viewModel.loadMore.isExecuting.value) {
+        return;
+      }
       widget.viewModel.loadNextPage();
     }

151-201: 空态样式已使用主题色,建议将尺寸常量抽取为设计系统/主题扩展(可选)

当前使用了若干硬编码尺寸(如 64、16、8)。从可维护性与一致性考虑,可将常用间距/尺寸抽取为统一的 spacing 常量或 ThemeExtension。

如项目已有 spacing 约定,可替换为对应常量;否则可后续统一抽取,不必阻塞本 PR。

lib/ui/core/ui/bookmark_list_fab.dart (2)

11-12: 文档注释未同步:仍称“通过Provider获取ScrollController”

实现已改为通过构造参数传入 controller,注释需更新以避免误导。

应用如下修改:

-/// - 通过Provider获取ScrollController
+/// - 通过构造参数传入 ScrollController(与列表共享同一控制器)

78-83: 滚动阈值从 3.0 提升到 5.0 的改动请与列表页保持一致(可选)

为保证多页体验一致,建议将阈值抽为常量(例如在 theme/const 中统一定义),FAB 与列表页若有类似阈值逻辑请复用同一处定义。

如暂无统一定义,可先在本文件顶部定义 const kScrollThreshold = 5.0; 并在后续 PR 统一。

lib/routing/router.dart (1)

89-102: 使用 selfManagedRoutes 绕过 MainLayout 包装的策略正确;建议改为常量 Set 并抽到外层以避免每次构建重复分配

当前在 builder 内部每次构建都会创建一个 List;改用顶层/静态常量 Set,可读性与性能更优,且包含判断语义更贴近。

在文件顶部(或 builder 外)新增:

const Set<String> kSelfManagedRoutes = {
  Routes.dailyRead,
  Routes.unarchived,
  Routes.reading,
  Routes.archived,
  Routes.marked,
};

并替换本段为:

-              // 书签列表页面不被 MainLayout 包装,它们自己管理 UI
-              final selfManagedRoutes = [
-                Routes.dailyRead,
-                Routes.unarchived,
-                Routes.reading,
-                Routes.archived,
-                Routes.marked,
-              ];
-
-              if (selfManagedRoutes
-                  .contains(state.fullPath ?? state.matchedLocation)) {
+              // 书签列表页面不被 MainLayout 包装,它们自己管理 UI
+              if (kSelfManagedRoutes
+                  .contains(state.fullPath ?? state.matchedLocation)) {
                 return child;
               }
test/ui/daily_read/widgets/daily_read_screen_test.dart (1)

58-122: 覆盖了空数据/有数据/加载中态,建议新增“滚动隐藏 FAB”回归测试(可选)

本 PR 的核心是修复 FAB 在页面切换后的滚动隐藏失效。建议新增一个 widget 测试:构建含 MainLayout+DailyReadScreen 的树,模拟滚动手势(向下再向上),断言 FAB 的可见性(例如通过查找 FloatingActionButton 或动画值/opacity)。

需要的话我可以起草一个测试用例,包含:

  • pump 初始布局,断言 FAB 可见
  • 执行拖拽 tester.drag(find.byType(ListView), const Offset(0, -300)),pump 动画,断言 FAB 隐藏
  • 反向拖拽,断言 FAB 显示
    并再构造一次页面切换场景(push 到另一书签页后 pop/切换分支),验证回到原页后逻辑仍然生效。
lib/ui/daily_read/widgets/daily_read_screen.dart (1)

28-36: 统一 ScrollController 生命周期至 initState,避免在 didChangeDependencies 中初始化造成不一致

其他书签列表页面(未读/阅读中/已归档等)均在 initState 中创建并在 dispose 中释放 ScrollController;本文件却在 didChangeDependencies 中惰性创建(且将字段声明为可空),模式不一致且增加理解成本。建议与其它页面对齐:在 initState 中创建,字段改为 late final,并移除 didChangeDependencies 中的初始化代码。这样也能更早可用,避免极端场景的空值判断。

可按如下 diff 调整:

-class _DailyReadScreenState extends State<DailyReadScreen> {
-  late ConfettiController _confettiController;
-  ScrollController? _scrollController;
+class _DailyReadScreenState extends State<DailyReadScreen> {
+  late ConfettiController _confettiController;
+  late final ScrollController _scrollController;

   late final ListenableSubscription _deleteSuccessSubscription;
   ...
   @override
   void initState() {
     super.initState();
     // 初始化礼花控制器
     _confettiController = ConfettiController(
       duration: const Duration(seconds: 3),
     );
+    // 创建 ScrollController
+    _scrollController = ScrollController();
     ...
   }
 
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
-
-    // 创建ScrollController
-    _scrollController ??= ScrollController();
     ...
   }
 
   @override
   void dispose() {
     // 释放动画控制器
     _confettiController.dispose();
     // 释放滚动控制器
-    _scrollController?.dispose();
+    _scrollController.dispose();
     ...
   }

Also applies to: 69-70, 122-128

lib/ui/core/main_layout.dart (2)

18-32: 对外暴露 scrollController + 保留 showFab:建议添加构造断言防止误用

当前当 showFab == true 且未提供 scrollController 时会回退到 floatingActionButton,这可能掩盖接线遗漏,导致 FAB 缺失。建议在构造函数添加断言,强制要求 showFab 时必须提供 scrollController。

-  const MainLayout({
-    super.key,
-    required this.child,
-    this.appBar,
-    this.title,
-    this.actions,
-    this.leading,
-    this.automaticallyImplyLeading = true,
-    this.floatingActionButton,
-    this.scrollController,
-    this.showFab = false,
-  });
+  const MainLayout({
+    super.key,
+    required this.child,
+    this.appBar,
+    this.title,
+    this.actions,
+    this.leading,
+    this.automaticallyImplyLeading = true,
+    this.floatingActionButton,
+    this.scrollController,
+    this.showFab = false,
+  }) : assert(!showFab || scrollController != null,
+        'When showFab is true, a non-null scrollController must be provided.');

为确保所有调用方满足该约束,可运行我在下方评论提供的脚本进行静态检查。


122-179: 可选:Drawer 中的导航建议去抖以避免极快多次点击造成重复路由入栈

目前通过关闭 Drawer 再延后一帧跳转已降低风险,但仍可考虑简单去抖。若有需要我可以补一个通用的 navigateDebounced 工具。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1013ece and ea05b6b.

📒 Files selected for processing (10)
  • lib/routing/router.dart (1 hunks)
  • lib/ui/bookmarks/widget/archived_screen.dart (1 hunks)
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart (2 hunks)
  • lib/ui/bookmarks/widget/marked_screen.dart (1 hunks)
  • lib/ui/bookmarks/widget/reading_screen.dart (1 hunks)
  • lib/ui/bookmarks/widget/unarchived_screen.dart (1 hunks)
  • lib/ui/core/main_layout.dart (4 hunks)
  • lib/ui/core/ui/bookmark_list_fab.dart (3 hunks)
  • lib/ui/daily_read/widgets/daily_read_screen.dart (2 hunks)
  • test/ui/daily_read/widgets/daily_read_screen_test.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
lib/ui/**/!(theme).dart

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

UI 样式必须使用主题,不允许硬编码颜色、字号等

Files:

  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/bookmarks/widget/reading_screen.dart
  • lib/ui/bookmarks/widget/archived_screen.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/bookmarks/widget/marked_screen.dart
  • lib/ui/core/main_layout.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/bookmarks/widget/unarchived_screen.dart
lib/ui/**/*.dart

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

lib/ui/**/*.dart: View 只处理展示与交互,构造函数仅接受 key 与 viewModel,不包含业务逻辑;用户交互通过 Command 执行
Command 监听器:在 initState 中订阅 results/errors,dispose 中取消订阅,并使用 mounted 检查
错误状态统一使用 ErrorPage 组件,优先使用工厂方法(如 ErrorPage.fromException),并遵循主题样式与可用性要求

Files:

  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/bookmarks/widget/reading_screen.dart
  • lib/ui/bookmarks/widget/archived_screen.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/bookmarks/widget/marked_screen.dart
  • lib/ui/core/main_layout.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/bookmarks/widget/unarchived_screen.dart
lib/routing/{router.dart,routes.dart}

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

使用 go_router 管理路由配置与路由定义

Files:

  • lib/routing/router.dart
🧠 Learnings (6)
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to test/**/*.{dart} : 应编写测试覆盖:Repository 层与 ViewModel 层 100% 覆盖,关键 Widget/UI、Command 流程与 Result 逻辑具备测试

Applied to files:

  • test/ui/daily_read/widgets/daily_read_screen_test.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/config/dependencies.dart : 使用 provider 进行依赖注入管理(构造函数注入 Repository 等)

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/core/main_layout.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : View 只处理展示与交互,构造函数仅接受 key 与 viewModel,不包含业务逻辑;用户交互通过 Command 执行

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/view_models/**/*.dart : ViewModel 必须继承 ChangeNotifier,使用 flutter_command 的 Command 暴露异步操作,并在构造函数初始化 Commands

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/bookmarks/widget/marked_screen.dart
  • lib/ui/core/main_layout.dart
  • lib/ui/bookmarks/widget/unarchived_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/core/ui/error_page.dart : 提供统一错误页面组件 ErrorPage 并位于 lib/ui/core/ui/error_page.dart

Applied to files:

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : 错误状态统一使用 ErrorPage 组件,优先使用工厂方法(如 ErrorPage.fromException),并遵循主题样式与可用性要求

Applied to files:

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (16)
lib/ui/bookmarks/widget/bookmark_list_screen.dart (1)

47-48: 新增 scrollController 参数的 API 设计合理,但需注意生命周期与一致性

引入可选的外部 ScrollController 能与 MainLayout/FAB 共享滚动源,方向正确。后续请确保当父组件替换 controller 时,本组件能正确切换与解绑监听(见后续针对 didChangeDependencies 的评论)。

是否存在父组件在运行期切换 scrollController 的场景?若有,请参见下方建议在 didUpdateWidget 中处理切换逻辑。

Also applies to: 52-53

lib/routing/router.dart (1)

61-87: 对设置页的特殊回退逻辑保持了与主导航一致性,LGTM

PopScope + 强制跳回首页的处理明确,避免在设置页留下空栈。

test/ui/daily_read/widgets/daily_read_screen_test.dart (1)

51-54: 测试装配简化为单一 Provider,方向正确

使用 ChangeNotifierProvider.value 注入 mock ViewModel 与通过构造注入 viewModel 的新接口匹配良好。

lib/ui/bookmarks/widget/marked_screen.dart (2)

16-22: 局部自管 ScrollController 的生命周期处理妥当

initState 创建、dispose 释放,避免了历史 Provider 方案中的竞态。与 BookmarkListScreen 共享 controller 以统一 FAB 行为,方向正确。


32-46: MainLayout 参数校验通过,无需移除 showFab

MainLayout 构造函数依然定义了

  • final bool showFab(可选且有默认值)
  • final ScrollController? scrollController

你在各书签页面均已显式传入:

  • showFab: true
  • 相同的 _scrollController

并在 BookmarkListScreen 中同步使用,行为和编译均正常。

lib/ui/daily_read/widgets/daily_read_screen.dart (1)

253-284: MainLayout 包裹与 CommandBuilder 状态切换逻辑清晰,符合 PR 目标

  • MainLayout 接入本地 ScrollController,showFab 打开,符合 FAB 滚动联动方案。
  • whileExecuting/onError/onData 分流合理,ErrorPage 使用规范。

LGTM。

lib/ui/bookmarks/widget/archived_screen.dart (3)

6-13: 从 StatelessWidget 切换为 StatefulWidget 的封装合理

该改动为页面内管理 ScrollController 创造条件,符合“页面级管理”的架构方向。


15-22: ScrollController 生命周期管理规范

initState 创建、dispose 释放,简洁到位,与其它页面保持一致。


32-46: MainLayout 与 BookmarkListScreen 共享同一个 ScrollController,FAB 行为可预期

  • title/showFab/scrollController 的传递完整;
  • BookmarkListScreen 复用同一 controller,避免 FAB 与列表不同步的边缘态。
lib/ui/bookmarks/widget/reading_screen.dart (3)

6-13: 形态切换为 StatefulWidget 用于注入 ScrollController:合理且必要

与 PR 其它书签列表页面一致,便于维护。


15-22: ScrollController 按规范创建/释放

实现无多余逻辑,清晰。


32-46: 主布局和列表共享 ScrollController 的接线正确

FAB 滚动联动在该页面应可稳定复现,文案与主题使用也符合规范。

lib/ui/bookmarks/widget/unarchived_screen.dart (3)

6-13: 切换为 StatefulWidget 以承载页面级 ScrollController:一致性良好

与 archived/reading 等页面同一模式,便于后续统一维护。


15-22: ScrollController 生命周期处理得当

无资源泄漏风险。


32-46: MainLayout/BookmarkListScreen 共享控制器,FAB 与列表联动可靠

命名与文案规范,未出现硬编码色值。

lib/ui/core/main_layout.dart (1)

122-179: Scaffold 与 Drawer/导航逻辑清晰,FAB 装配方式符合新架构

  • AppBar/Drawer 的分支判断可读性较好;
  • FAB 条件渲染依赖 showFab 与 scrollController,一致性良好。

LGTM。

修复了 FloatingActionButton 在从未读页切换到每日阅读页后,
下滑隐藏功能失效的问题。

主要变更:
- 重构 FAB 管理架构,从全局 Provider 模式改为页面级管理
- 修复 ScrollController 绑定时机问题,确保滚动监听器正常工作
- 为所有书签列表页面(未读、阅读中、已归档、收藏)添加 FAB 支持
- 优化 ScrollController 生命周期管理

影响页面:
- 每日阅读页面
- 未读书签页面
- 阅读中书签页面
- 已归档书签页面
- 收藏书签页面

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@shadowfish07 shadowfish07 force-pushed the fix/fab-scroll-functionality branch from ea05b6b to e15e4fb Compare August 27, 2025 15:14
修复书签列表页面和FAB组件中滚动控制器的监听管理问题
确保在组件更新和销毁时正确解绑监听器
添加所有权标志来正确管理控制器生命周期
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
lib/ui/core/ui/bookmark_list_fab.dart (3)

53-58: didUpdateWidget 未解绑旧 controller,存在监听泄漏与错误回调风险

当父组件替换 ScrollController 时,需要先从旧实例移除监听,再给新实例添加。否则可能触发已卸载 FAB 的 setState。建议就地修复:

   @override
   void didUpdateWidget(BookmarkListFab oldWidget) {
     super.didUpdateWidget(oldWidget);
     if (oldWidget.scrollController != widget.scrollController) {
-      // ScrollController 变化时重置状态
-      _lastScrollPosition = 0;
-      setState(() {
-        _isVisible = true;
-      });
-      _animationController.forward();
+      // 先解绑旧,再绑定新
+      oldWidget.scrollController?.removeListener(_onScroll);
+      widget.scrollController?.addListener(_onScroll);
+      // 重置滚动状态与动画
+      setState(() {
+        _lastScrollPosition = 0;
+        _isVisible = true;
+      });
+      _animationController.forward();
     }
   }

62-66: dispose 未移除滚动监听,可能导致 “setState() called after dispose()”

FAB 销毁后若外部 ScrollController 仍存活,将继续回调已销毁状态。需显式移除监听。

   @override
   void dispose() {
+    // 解绑监听,防止内存泄漏与已销毁状态的 setState
+    widget.scrollController?.removeListener(_onScroll);
     _animationController.dispose();
     super.dispose();
   }

123-127: 在 build 中反复 remove/add 监听是反模式,应改用生命周期管理

build 可能高频触发,反复解绑/绑定影响性能且时序难控。将首次绑定放在 initState,变更时在 didUpdateWidget 处理,销毁时在 dispose 解绑。就地移除本段代码:

-    // 简单直接地绑定监听器
-    if (widget.scrollController != null) {
-      widget.scrollController!.removeListener(_onScroll);
-      widget.scrollController!.addListener(_onScroll);
-    }

并在 initState 首次绑定(非选区,示意):

@override
void initState() {
  super.initState();
  _animationController = AnimationController(duration: const Duration(milliseconds: 200), vsync: this);
  _animation = CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
  _animationController.forward();
  widget.scrollController?.addListener(_onScroll);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ea05b6b and e15e4fb.

📒 Files selected for processing (10)
  • lib/routing/router.dart (1 hunks)
  • lib/ui/bookmarks/widget/archived_screen.dart (1 hunks)
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart (2 hunks)
  • lib/ui/bookmarks/widget/marked_screen.dart (1 hunks)
  • lib/ui/bookmarks/widget/reading_screen.dart (1 hunks)
  • lib/ui/bookmarks/widget/unarchived_screen.dart (1 hunks)
  • lib/ui/core/main_layout.dart (4 hunks)
  • lib/ui/core/ui/bookmark_list_fab.dart (3 hunks)
  • lib/ui/daily_read/widgets/daily_read_screen.dart (2 hunks)
  • test/ui/daily_read/widgets/daily_read_screen_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • test/ui/daily_read/widgets/daily_read_screen_test.dart
  • lib/ui/bookmarks/widget/marked_screen.dart
  • lib/ui/bookmarks/widget/archived_screen.dart
  • lib/ui/bookmarks/widget/unarchived_screen.dart
  • lib/ui/core/main_layout.dart
  • lib/routing/router.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
🧰 Additional context used
📓 Path-based instructions (2)
lib/ui/**/!(theme).dart

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

UI 样式必须使用主题,不允许硬编码颜色、字号等

Files:

  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/bookmarks/widget/reading_screen.dart
lib/ui/**/*.dart

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

lib/ui/**/*.dart: View 只处理展示与交互,构造函数仅接受 key 与 viewModel,不包含业务逻辑;用户交互通过 Command 执行
Command 监听器:在 initState 中订阅 results/errors,dispose 中取消订阅,并使用 mounted 检查
错误状态统一使用 ErrorPage 组件,优先使用工厂方法(如 ErrorPage.fromException),并遵循主题样式与可用性要求

Files:

  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/bookmarks/widget/reading_screen.dart
🧠 Learnings (4)
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : Command 监听器:在 initState 中订阅 results/errors,dispose 中取消订阅,并使用 mounted 检查

Applied to files:

  • lib/ui/core/ui/bookmark_list_fab.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/view_models/**/*.dart : ViewModel 必须继承 ChangeNotifier,使用 flutter_command 的 Command 暴露异步操作,并在构造函数初始化 Commands

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/config/dependencies.dart : 使用 provider 进行依赖注入管理(构造函数注入 Repository 等)

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : View 只处理展示与交互,构造函数仅接受 key 与 viewModel,不包含业务逻辑;用户交互通过 Command 执行

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (4)
lib/ui/daily_read/widgets/daily_read_screen.dart (1)

253-284: MainLayout 内联包裹 + 统一传入 scrollController 的做法很稳

标题、showFab 与同一 controller 透传,契合此次从全局 Provider 下沉到页面级管理的目标,有助于 FAB 在页面切换后的稳定性与一致性。

lib/ui/bookmarks/widget/reading_screen.dart (2)

15-28: 本地持有 ScrollController 的 Stateful 化改造正确,生命周期处理到位

initState 创建、dispose 释放,避免外部依赖与全局状态耦合,符合此次架构收敛方向。


32-47: 同一 ScrollController 传递给 MainLayout 与 BookmarkListScreen,FAB 行为可预测

该 wiring 保障 FAB 监听到真实滚动来源,切页后隐藏/显示逻辑可持续工作。

lib/ui/core/ui/bookmark_list_fab.dart (1)

78-83: 细节赞:微小滚动阈值与最后位置更新避免抖动

提高到 5.0 并在 early-return 前更新 _lastScrollPosition,能有效减少动画抖动。

在daily_read_screen_test和reading_screen_test测试文件中添加了对MainAppViewModel和AboutViewModel的mock支持,以支持更全面的测试场景
@codecov
Copy link

codecov bot commented Aug 27, 2025

Codecov Report

❌ Patch coverage is 41.05960% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.38%. Comparing base (89fb145) to head (3137fc6).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/ui/daily_read/widgets/daily_read_screen.dart 56.41% 17 Missing ⚠️
lib/ui/core/main_layout.dart 55.55% 16 Missing ⚠️
lib/ui/bookmarks/widget/archived_screen.dart 0.00% 13 Missing ⚠️
lib/ui/bookmarks/widget/marked_screen.dart 0.00% 13 Missing ⚠️
lib/ui/bookmarks/widget/unarchived_screen.dart 0.00% 13 Missing ⚠️
lib/ui/bookmarks/widget/bookmark_list_screen.dart 35.71% 9 Missing ⚠️
lib/ui/core/ui/bookmark_list_fab.dart 25.00% 6 Missing ⚠️
lib/routing/router.dart 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #86      +/-   ##
==========================================
+ Coverage   42.70%   43.38%   +0.68%     
==========================================
  Files          87       87              
  Lines        5943     5981      +38     
==========================================
+ Hits         2538     2595      +57     
+ Misses       3405     3386      -19     
Flag Coverage Δ
unittests 43.38% <41.05%> (+0.68%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

将测试中的日志配置改为使用测试专用的SimpleTestPrinter和MultiOutput,避免测试时产生不必要的输出。同时添加setupTestGroupLogging()来统一管理测试组的日志设置。
确保在组件销毁时正确释放滚动控制器和所有错误订阅
添加对组件挂载状态的检查以避免在未挂载时更新UI
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (9)
test/unit/domain/use_cases/app_update_use_case_test.dart (1)

309-319: 重复的“静默 Logger”构造可抽到 helper,减少样板

建议在 test/helpers/test_logger_helper.dart 中新增 createSilentTestLogger(),此处直接调用,统一配置,避免各处手写。

可参考:

// test/helpers/test_logger_helper.dart
Logger createSilentTestLogger() => Logger(
  printer: SimpleTestPrinter(),
  output: MultiOutput([]),
  level: Level.off,
);
lib/ui/core/ui/bookmark_list_fab.dart (1)

5-12: 文档注释与实现不一致(仍提 Provider)

实现已改为通过外部传入的 ScrollController,建议同步更新注释,避免误导。

-/// - 通过Provider获取ScrollController
+/// - 通过外部传入的 ScrollController 驱动滚动隐藏/显示
lib/ui/bookmarks/widget/bookmark_list_screen.dart (3)

47-53: 构造函数新增 scrollController 参数,需与项目规范对齐

规范要求 “View 构造函数仅接受 key 与 viewModel”。该新增参数为纯 UI 依赖,且与本 PR 架构目标一致,但建议在项目规则中明确此类例外(如:可接受的 UI 级依赖:ScrollController、FocusNode 等),避免后续 PR 产生分歧。

需要我提交一段项目规则补充说明到 .trae/rules/project_rules.md 吗?


137-142: 分页触底缺少节流/重入保护,可能重复触发 loadNextPage

快速滚动到尾部时会高频进入该分支。建议在执行中或无更多数据时直接返回。

-    if (currentScrollPosition >=
-        _scrollController!.position.maxScrollExtent - 200) {
-      widget.viewModel.loadNextPage();
-    }
+    if (currentScrollPosition >=
+        _scrollController!.position.maxScrollExtent - 200) {
+      // 防重入:执行中或无更多数据则跳过
+      final isLoadingMore = widget.viewModel.loadMore.isExecuting.value;
+      if (isLoadingMore || !widget.viewModel.hasMoreData) return;
+      widget.viewModel.loadNextPage();
+    }

68-79: 清理导航回调以避免持有已卸载 context

initState 中设置了 navigateToDetail 回调,但 dispose 未清理,若 ViewModel 复用可能残留无效闭包。

   void dispose() {
     _scrollController?.removeListener(_onScroll);
     // 只有自己创建的 ScrollController 才需要 dispose
     if (_ownsController) {
       _scrollController?.dispose();
     }
+    // 清理回调,避免持有已卸载的 context
+    widget.viewModel.setNavigateToDetailCallback((_) {});
     _deleteSuccessSubscription.cancel();
     super.dispose();
   }

Also applies to: 121-129

lib/ui/daily_read/widgets/daily_read_screen.dart (1)

70-117: 错误订阅完备且带 mounted 检查:LGTM;可考虑消除重复样板

三处 errors 监听结构一致,可抽一个私有辅助方法以减少重复(可选)。

示例:

void _listenError<T>(Command<T, dynamic> cmd, String logMsg, String snackMsg) {
  cmd.errors.where((e) => e != null).listen((error, _) {
    if (!mounted) return;
    appLogger.e(logMsg, error: error);
    SnackBarHelper.showError(context, snackMsg);
  });
}
test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart (1)

332-410: 不同测试套件的 mock 策略不一致(是否抛出缺失桩)

此处的 NiceMock 未启用缺失桩抛错,而 reading_screen_test.mocks.dart 中的同类 mock 使用了 throwOnMissingStub。建议在注解处统一策略,避免测试行为不一致导致的隐性问题。

test/ui/daily_read/widgets/daily_read_screen_test.dart (2)

17-21: 统一 mock 生成策略以提升用例健壮性(可选)

建议在 @GenerateNiceMocks 中为所有 MockSpec 指定 onMissingStub: OnMissingStub.throw,与另一个测试套件保持一致,避免漏 stub 时悄然返回默认值。

可参考:

 @GenerateNiceMocks([
-  MockSpec<DailyReadViewModel>(),
-  MockSpec<MainAppViewModel>(),
-  MockSpec<AboutViewModel>(),
+  MockSpec<DailyReadViewModel>(onMissingStub: OnMissingStub.throw),
+  MockSpec<MainAppViewModel>(onMissingStub: OnMissingStub.throw),
+  MockSpec<AboutViewModel>(onMissingStub: OnMissingStub.throw),
 ])

12-14: 补充与 FAB 滚动隐藏/页面切换相关的用例(可选)

为更贴合本 PR 修复目标,建议增加一条 Widget 测试:在两个包含各自 ScrollController 与 FAB 的页面之间切换,模拟滚动并断言 FAB 的显隐状态在返回前页后仍能正确响应。

可在本文件新增用例(示意):

testWidgets('FAB 在页面切换后仍能随滚动隐藏/显示', (tester) async {
  // 构建包含两个路由的 MaterialApp,并为每页各自提供 ScrollController 与 FAB
  // push -> scroll down (expect: FAB 不可见)
  // pop  -> 再次 scroll up (expect: FAB 可见)
  // 断言使用 find.byType(FloatingActionButton) 与 offstage/opacity/visibility 判定
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e15e4fb and 3137fc6.

📒 Files selected for processing (8)
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart (3 hunks)
  • lib/ui/core/ui/bookmark_list_fab.dart (2 hunks)
  • lib/ui/daily_read/widgets/daily_read_screen.dart (5 hunks)
  • test/ui/bookmarks/widget/reading_screen_test.dart (3 hunks)
  • test/ui/bookmarks/widget/reading_screen_test.mocks.dart (2 hunks)
  • test/ui/daily_read/widgets/daily_read_screen_test.dart (3 hunks)
  • test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart (2 hunks)
  • test/unit/domain/use_cases/app_update_use_case_test.dart (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
lib/ui/**/!(theme).dart

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

UI 样式必须使用主题,不允许硬编码颜色、字号等

Files:

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
lib/ui/**/*.dart

📄 CodeRabbit inference engine (.trae/rules/project_rules.md)

lib/ui/**/*.dart: View 只处理展示与交互,构造函数仅接受 key 与 viewModel,不包含业务逻辑;用户交互通过 Command 执行
Command 监听器:在 initState 中订阅 results/errors,dispose 中取消订阅,并使用 mounted 检查
错误状态统一使用 ErrorPage 组件,优先使用工厂方法(如 ErrorPage.fromException),并遵循主题样式与可用性要求

Files:

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
🧠 Learnings (6)
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to test/**/*.{dart} : 应编写测试覆盖:Repository 层与 ViewModel 层 100% 覆盖,关键 Widget/UI、Command 流程与 Result 逻辑具备测试

Applied to files:

  • test/ui/bookmarks/widget/reading_screen_test.dart
  • test/ui/bookmarks/widget/reading_screen_test.mocks.dart
  • test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart
  • test/ui/daily_read/widgets/daily_read_screen_test.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/view_models/**/*.dart : ViewModel 必须继承 ChangeNotifier,使用 flutter_command 的 Command 暴露异步操作,并在构造函数初始化 Commands

Applied to files:

  • test/ui/bookmarks/widget/reading_screen_test.dart
  • test/ui/bookmarks/widget/reading_screen_test.mocks.dart
  • test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/core/ui/error_page.dart : 提供统一错误页面组件 ErrorPage 并位于 lib/ui/core/ui/error_page.dart

Applied to files:

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : 错误状态统一使用 ErrorPage 组件,优先使用工厂方法(如 ErrorPage.fromException),并遵循主题样式与可用性要求

Applied to files:

  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : Command 监听器:在 initState 中订阅 results/errors,dispose 中取消订阅,并使用 mounted 检查

Applied to files:

  • test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart
  • lib/ui/core/ui/bookmark_list_fab.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-25T09:44:33.606Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-25T09:44:33.606Z
Learning: Applies to lib/ui/**/*.dart : View 只处理展示与交互,构造函数仅接受 key 与 viewModel,不包含业务逻辑;用户交互通过 Command 执行

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (19)
test/unit/domain/use_cases/app_update_use_case_test.dart (2)

11-11: 测试日志初始化:LGTM

引入 test_logger_helper 并在分组前初始化日志,符合单测静默化诉求。

Also applies to: 29-30


36-41: 确认失败场景下仍能看到必要日志

当前 Logger 使用 Level.off 且 MultiOutput([]) 会完全禁用输出。请确认 setupTestGroupLogging() 的失败回放机制不依赖 Logger 本身;否则失败时也可能拿不到日志。

是否需要改为 Level.warning(或结合自定义 LogFilter)以仅在较高等级输出?

lib/ui/core/ui/bookmark_list_fab.dart (4)

49-50: 在 initState 绑定滚动监听:LGTM

从 build 期迁移到生命周期方法,避免重复绑定与性能抖动。


56-63: didUpdateWidget 解绑旧 controller 并重置状态:LGTM

正确处理 controller 切换与可见性复位,规避监听泄漏。


68-70: dispose 中解绑监听:LGTM

避免 “setState after dispose” 与潜在泄漏。


84-89: 滚动阈值与微滚动去抖:LGTM

阈值提升到 5.0 并在微小滚动仅更新游标,可减少动画抖动。

lib/ui/bookmarks/widget/bookmark_list_screen.dart (3)

97-103: 首次绑定 controller 并标记所有权:LGTM

只在 _scrollController 为空时创建/绑定,避免重复 addListener。


106-119: didUpdateWidget 中的解绑/重新绑定与自有控制权处理:LGTM

覆盖 controller 切换的关键路径,避免悬挂监听与双重回调。


121-129: 按自有控制权释放 controller:LGTM

避免从外部传入的 controller 被误 dispose。

lib/ui/daily_read/widgets/daily_read_screen.dart (2)

30-35: 在 initState 创建 late final ScrollController 并统一释放:LGTM

符合“页面自管理 controller”的重构目标,配合 MainLayout/FAB 共享一份滚动源。

Also applies to: 43-45


269-301: MainLayout 直连 scrollController 并开启 FAB:LGTM

与本 PR 的“页面级管理”策略一致。

test/ui/bookmarks/widget/reading_screen_test.dart (4)

11-13: 新增 MainAppViewModel/AboutViewModel mocks:LGTM

与 MainLayout 的新依赖保持一致,避免构建时缺依赖。

Also applies to: 16-16


61-67: 基础桩配置完整:LGTM

shareTextStream 与 updateInfo 的默认桩能满足构建路径。


71-83: MultiProvider 包装被测 Widget:LGTM

与生产代码依赖图一致,提升用例稳定性。


157-159: 先设置 bookmarks 后构建:LGTM

避免竞态导致的初次渲染不一致。

test/ui/daily_read/widgets/daily_read_screen_test.dart (1)

73-78: Provider 注入改造与 PR 目标一致,LGTM

从移除 ScrollControllerProvider 到按页面注入 MainAppViewModel / AboutViewModel 的改动合理,未见问题。

test/ui/bookmarks/widget/reading_screen_test.mocks.dart (3)

9-21: 导入与类型依赖齐全,符合生成规范

引入 ThemeMode、UpdateInfo 与内部 dummies 用于默认返回值,满足测试场景需要。


318-397: MockMainAppViewModel 定义完善,严格模式更易暴露漏桩问题

启用 throwOnMissingStub 有助于发现未 stub 的调用,保持现状即可。


399-522: MockAboutViewModel 定义完整,LGTM

涵盖下载/安装命令及状态字段,满足 UI 测试依赖。

Comment on lines +62 to 64
// Mock AboutViewModel
when(mockAboutViewModel.updateInfo).thenReturn(null);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修复对 AboutViewModel 不存在成员的 stub

mockAboutViewModel.updateInfo 在生成的 mock 中不存在。请改为 stub 已有成员或移除该行。

应用如下 diff:

-    // Mock AboutViewModel
-    when(mockAboutViewModel.updateInfo).thenReturn(null);
+    // Mock AboutViewModel(示例:根据需要 stub 已存在的成员)
+    when(mockAboutViewModel.version).thenReturn('test-version');
+    when(mockAboutViewModel.load).thenReturn(
+      Command.createSyncNoParam<dynamic>(() => null),
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Mock AboutViewModel
when(mockAboutViewModel.updateInfo).thenReturn(null);
});
// Mock AboutViewModel(示例:根据需要 stub 已存在的成员)
when(mockAboutViewModel.version).thenReturn('test-version');
when(mockAboutViewModel.load).thenReturn(
Command.createSyncNoParam<dynamic>(() => null),
);
});
🤖 Prompt for AI Agents
In test/ui/daily_read/widgets/daily_read_screen_test.dart around lines 62-64,
the test stubs mockAboutViewModel.updateInfo which does not exist on the
generated mock; remove that stub line or replace it with a stub of an actual
member that exists on the generated AboutViewModel mock (inspect the generated
mock to find the correct method/property name) and update the
when(...).thenReturn(...) call accordingly.

Comment on lines +412 to +555
/// A class which mocks [AboutViewModel].
///
/// See the documentation for Mockito's code generation for more information.
class MockAboutViewModel extends _i1.Mock implements _i11.AboutViewModel {
@override
_i2.Command<dynamic, dynamic> get load => (super.noSuchMethod(
Invocation.getter(#load),
returnValue: _FakeCommand_0<dynamic, dynamic>(
this,
Invocation.getter(#load),
),
returnValueForMissingStub: _FakeCommand_0<dynamic, dynamic>(
this,
Invocation.getter(#load),
),
) as _i2.Command<dynamic, dynamic>);

@override
String get version => (super.noSuchMethod(
Invocation.getter(#version),
returnValue: _i12.dummyValue<String>(
this,
Invocation.getter(#version),
),
returnValueForMissingStub: _i12.dummyValue<String>(
this,
Invocation.getter(#version),
),
) as String);

@override
double get downloadProgress => (super.noSuchMethod(
Invocation.getter(#downloadProgress),
returnValue: 0.0,
returnValueForMissingStub: 0.0,
) as double);

@override
bool get isDownloading => (super.noSuchMethod(
Invocation.getter(#isDownloading),
returnValue: false,
returnValueForMissingStub: false,
) as bool);

@override
bool get isInstalling => (super.noSuchMethod(
Invocation.getter(#isInstalling),
returnValue: false,
returnValueForMissingStub: false,
) as bool);

@override
_i2.Command<_i13.UpdateInfo, void> get downloadAndInstallUpdateCommand =>
(super.noSuchMethod(
Invocation.getter(#downloadAndInstallUpdateCommand),
returnValue: _FakeCommand_0<_i13.UpdateInfo, void>(
this,
Invocation.getter(#downloadAndInstallUpdateCommand),
),
returnValueForMissingStub: _FakeCommand_0<_i13.UpdateInfo, void>(
this,
Invocation.getter(#downloadAndInstallUpdateCommand),
),
) as _i2.Command<_i13.UpdateInfo, void>);

@override
_i2.Command<_i13.UpdateInfo, String> get downloadUpdateCommand =>
(super.noSuchMethod(
Invocation.getter(#downloadUpdateCommand),
returnValue: _FakeCommand_0<_i13.UpdateInfo, String>(
this,
Invocation.getter(#downloadUpdateCommand),
),
returnValueForMissingStub: _FakeCommand_0<_i13.UpdateInfo, String>(
this,
Invocation.getter(#downloadUpdateCommand),
),
) as _i2.Command<_i13.UpdateInfo, String>);

@override
_i2.Command<String, void> get installUpdateCommand => (super.noSuchMethod(
Invocation.getter(#installUpdateCommand),
returnValue: _FakeCommand_0<String, void>(
this,
Invocation.getter(#installUpdateCommand),
),
returnValueForMissingStub: _FakeCommand_0<String, void>(
this,
Invocation.getter(#installUpdateCommand),
),
) as _i2.Command<String, void>);

@override
set load(_i2.Command<dynamic, dynamic>? _load) => super.noSuchMethod(
Invocation.setter(
#load,
_load,
),
returnValueForMissingStub: null,
);

@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,
returnValueForMissingStub: false,
) as bool);

@override
void addListener(_i6.VoidCallback? listener) => super.noSuchMethod(
Invocation.method(
#addListener,
[listener],
),
returnValueForMissingStub: null,
);

@override
void removeListener(_i6.VoidCallback? listener) => super.noSuchMethod(
Invocation.method(
#removeListener,
[listener],
),
returnValueForMissingStub: null,
);

@override
void dispose() => super.noSuchMethod(
Invocation.method(
#dispose,
[],
),
returnValueForMissingStub: null,
);

@override
void notifyListeners() => super.noSuchMethod(
Invocation.method(
#notifyListeners,
[],
),
returnValueForMissingStub: null,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

MockAboutViewModel 未包含 updateInfo,和测试桩冲突

生成的 MockAboutViewModel 没有 updateInfo getter;但测试中对 mockAboutViewModel.updateInfo 进行了 stub,会导致编译/运行期错误。请在测试中移除该 stub,或改为 stub 已存在的成员(如 versionload),或在真实 AboutViewModel 中新增 updateInfo 并重新生成 mocks。

建议在仓库根目录执行下列脚本确认接口中是否存在 updateInfo


🏁 Script executed:

#!/bin/bash
# 查看 AboutViewModel 声明及是否含有 updateInfo
rg -nP '(^|\s)(abstract\s+)?class\s+AboutViewModel\b' -C2
rg -n '\bupdateInfo\b' -C2

Length of output: 24654


🏁 Script executed:

#!/bin/bash
# 确认 Daily Read widget 的 MockAboutViewModel 中是否包含 updateInfo getter
rg -n 'updateInfo' -C2 test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart

Length of output: 83


修复 MockAboutViewModel 缺失 updateInfo 导致测试编译失败

经确认,真实的 AboutViewModellib/ui/settings/view_models/about_viewmodel.dart 中确实声明了

UpdateInfo? _updateInfo;
UpdateInfo? get updateInfo => _updateInfo;

但生成在 test/ui/daily_read/widgets/daily_read_screen_test.mocks.dartMockAboutViewModel 并未包含 updateInfo 的重写,导致如下测试代码无法编译:

when(mockAboutViewModel.updateInfo).thenReturn(null);

请在以下两种方案中择一进行修复:

  • 方案一:在项目根目录重新生成 mocks,确保 AboutViewModel 的所有成员(含 updateInfo)都被正确生成
  • 方案二:临时移除或改用已有成员(如 versionload)来替换测试中的 updateInfo stub,待 mocks 配置修正后再恢复

定位:

  • lib/ui/settings/view_models/about_viewmodel.dart(定义 updateInfo
  • test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart(缺失该 getter)
  • test/ui/daily_read/widgets/daily_read_screen_test.dart:63(stub updateInfo

请尽快补齐 mock 或更新测试逻辑,确保整个测试套件能够通过编译并正常运行。

🤖 Prompt for AI Agents
In test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart around lines 412
to 555, the generated MockAboutViewModel is missing the updateInfo getter which
causes tests that stub mockAboutViewModel.updateInfo to fail; fix by either
re-running the mock generation tool (e.g., run build_runner to regenerate mocks
so AboutViewModel's updateInfo getter is included) or as a temporary measure
update the test at test/ui/daily_read/widgets/daily_read_screen_test.dart line
~63 to stop stubbing updateInfo (use an existing stubbed member like
version/load or remove the stub) until mocks are regenerated. Ensure after
regenerating or editing tests you run the test compile to confirm the missing
getter is restored or the test adjusted.

@shadowfish07 shadowfish07 merged commit 3646797 into main Aug 27, 2025
6 checks passed
@shadowfish07 shadowfish07 deleted the fix/fab-scroll-functionality branch August 27, 2025 15:46
github-actions bot pushed a commit that referenced this pull request Aug 28, 2025
## [0.8.0](v0.7.0...v0.8.0) (2025-08-28)

### ✨ 新功能

* 书签卡片支持长按删除功能 ([#82](#82)) ([1013ece](1013ece)), closes [#81](#81)

### 🐛 Bug修复

* 修复 FAB 滚动隐藏功能在页面切换后失效的问题 ([#86](#86)) ([3646797](3646797))

### ♻️ 代码重构

* **settings:** 重构设置页面UI组件使用统一Material Design 3设计风格 ([#87](#87)) ([89fb145](89fb145))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FAB 滚动隐藏功能在页面切换后失效

2 participants