Skip to content

Conversation

@ahmet-cetinkaya
Copy link
Owner

@ahmet-cetinkaya ahmet-cetinkaya commented Dec 16, 2025

🎯 Context

This PR adds infinity scroll pagination as an alternative to the existing load more button across all list components in the app.

🛠️ Major Changes

  • Introduced PaginationMode enum with loadMore and infinityScroll options
  • Implemented infinity scroll functionality in all list components (app usage, habits, notes, tags, and tasks)
  • Added scroll listeners with threshold detection for auto-loading
  • Maintained backward compatibility with existing load more buttons
  • Updated 14 files with 516 additions and 21 deletions

🧪 Testing

  • Ran fvm flutter test locally.

Summary by Sourcery

Add an optional infinity scroll pagination mode alongside the existing load-more behavior and wire it into key list-based screens.

New Features:

  • Introduce a PaginationMode enum to toggle between load-more and infinity-scroll behaviors for list components.
  • Enable infinity scroll with automatic page loading and loading indicators across habits, tasks, notes, tags, app usage lists, and app usage rule lists.
  • Update habits, tasks, notes, tags, and app usage pages to opt into infinity scroll for their primary lists.

Enhancements:

  • Preserve backward compatibility by defaulting all lists to the existing load-more pagination mode when no pagination option is specified.

Introduce PaginationMode enum with loadMore and infinityScroll options.
Implement infinity scroll functionality in all list components including
app usage, habits, notes, tags, and tasks lists. Add scroll listeners,
threshold detection, and auto-loading capabilities while maintaining
backward compatibility with existing load more buttons.
@ahmet-cetinkaya ahmet-cetinkaya self-assigned this Dec 16, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ahmet-cetinkaya, 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!

This pull request significantly enhances the user experience across various list components in the application by introducing an infinity scroll pagination option. Instead of manually clicking a 'Load More' button, users can now seamlessly browse through long lists as new content loads automatically when they approach the end of the current view. This change provides a smoother and more intuitive interaction pattern for data-heavy sections of the app.

Highlights

  • New PaginationMode Enum: A new PaginationMode enum has been introduced, offering loadMore and infinityScroll options to control how list components handle pagination.
  • Infinity Scroll Implementation: Infinity scroll functionality has been added to all major list components, including App Usage Ignore Rules, App Usage, App Usage Tag Rules, Habits, Notes, Tags, and Tasks lists. This allows for automatic loading of more items as the user scrolls down.
  • Scroll Listener Integration: Each list component now includes scroll listeners with a threshold detection mechanism. When the user scrolls 80% of the way down the list, new items are automatically fetched.
  • Backward Compatibility: The existing 'Load More' button functionality is maintained, allowing components to switch between traditional button-based pagination and the new infinity scroll mode.
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 by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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 pull request 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.

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 16, 2025

Reviewer's Guide

Adds a configurable pagination mode for all major list-based features, introducing an infinity scroll option alongside the existing load-more button and wiring it through list widgets and their hosting pages, with scroll listeners, auto-load thresholds, and loading indicators.

Sequence diagram for infinity scroll pagination flow

sequenceDiagram
  actor User
  participant ListPage
  participant ListWidget
  participant ScrollController
  participant DataService

  User->>ListPage: Navigate to feature page
  ListPage->>ListWidget: Create with paginationMode infinityScroll
  ListWidget->>ListWidget: initState
  ListWidget->>ListWidget: _setupScrollListener
  ListWidget->>ScrollController: addListener(_onScroll)
  ListWidget->>DataService: initial getList(pageIndex 0)
  DataService-->>ListWidget: PagedResult(pageIndex 0, hasNext)
  ListWidget->>ListWidget: build list, maybe _checkAndFillViewport

  loop User scrolling
    User->>ScrollController: Scroll events
    ScrollController-->>ListWidget: _onScroll callback
    ListWidget->>ListWidget: compute maxScroll, currentScroll, threshold
    alt nearing bottom and hasNext and not _isLoadingMore
      ListWidget->>ListWidget: _loadMoreInfinityScroll
      ListWidget->>ListWidget: set _isLoadingMore true
      ListWidget->>DataService: getList(pageIndex + 1)
      DataService-->>ListWidget: PagedResult(next page, hasNext)
      ListWidget->>ListWidget: merge items, update state
      ListWidget->>ListWidget: set _isLoadingMore false
    else above threshold or no next page
      ListWidget-->>ScrollController: return
    end
  end
Loading

Class diagram for PaginationMode and updated list widgets

classDiagram
  direction LR

  class PaginationMode {
    <<enumeration>>
    loadMore
    infinityScroll
  }

  class HabitsList {
    +int pageSize
    +PaginationMode paginationMode
    +HabitsList()
    +createState() HabitsListState
  }

  class HabitsListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  class TaskList {
    +PaginationMode paginationMode
    +TaskList()
    +createState() TaskListState
  }

  class TaskListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  class AppUsageList {
    +PaginationMode paginationMode
    +AppUsageList()
    +createState() AppUsageListState
  }

  class AppUsageListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  class NotesList {
    +PaginationMode paginationMode
    +NotesList()
    +createState() NotesListState
  }

  class NotesListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  class TagsList {
    +PaginationMode paginationMode
    +TagsList()
    +createState() TagsListState
  }

  class TagsListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  class AppUsageTagRuleList {
    +PaginationMode paginationMode
    +AppUsageTagRuleList()
    +createState() AppUsageTagRuleListState
  }

  class AppUsageTagRuleListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  class AppUsageIgnoreRuleList {
    +PaginationMode paginationMode
    +AppUsageIgnoreRuleList()
    +createState() AppUsageIgnoreRuleListState
  }

  class AppUsageIgnoreRuleListState {
    -ScrollController _scrollController
    -bool _isLoadingMore
    +initState()
    +dispose()
    -_setupScrollListener()
    -_onScroll()
    -_loadMoreInfinityScroll() Future~void~
    -_checkAndFillViewport()
  }

  PaginationMode <.. HabitsList : uses
  PaginationMode <.. TaskList : uses
  PaginationMode <.. AppUsageList : uses
  PaginationMode <.. NotesList : uses
  PaginationMode <.. TagsList : uses
  PaginationMode <.. AppUsageTagRuleList : uses
  PaginationMode <.. AppUsageIgnoreRuleList : uses

  HabitsList "1" o-- "1" HabitsListState
  TaskList "1" o-- "1" TaskListState
  AppUsageList "1" o-- "1" AppUsageListState
  NotesList "1" o-- "1" NotesListState
  TagsList "1" o-- "1" TagsListState
  AppUsageTagRuleList "1" o-- "1" AppUsageTagRuleListState
  AppUsageIgnoreRuleList "1" o-- "1" AppUsageIgnoreRuleListState
Loading

File-Level Changes

Change Details Files
Introduce a shared pagination mode enum used by list components.
  • Add PaginationMode enum with loadMore and infinityScroll variants
  • Document enum semantics for button-based vs auto-scroll loading
src/lib/presentation/ui/shared/enums/pagination_mode.dart
Extend list widgets (habits, tasks, app usages, notes, tags, and rule lists) to support both load-more and infinity scroll behaviors.
  • Add a paginationMode parameter with default PaginationMode.loadMore to each list widget
  • Track an internal _isLoadingMore flag in each list state
  • Attach scroll listeners when paginationMode is infinityScroll
  • On scroll, detect when the user passes an 80% scroll threshold and trigger a paged fetch via existing _get*/_load* methods
  • Add _checkAndFillViewport helpers that auto-fetch when the list doesn’t fill the viewport
  • Guard all pagination actions with hasNext and existing list-null checks
src/lib/presentation/ui/features/habits/components/habits_list.dart
src/lib/presentation/ui/features/tasks/components/tasks_list.dart
src/lib/presentation/ui/features/app_usages/components/app_usage_list.dart
src/lib/presentation/ui/features/notes/components/notes_list.dart
src/lib/presentation/ui/features/tags/components/tags_list.dart
src/lib/presentation/ui/features/app_usages/components/app_usage_tag_rule_list.dart
src/lib/presentation/ui/features/app_usages/components/app_usage_ignore_rule_list.dart
Update list rendering to conditionally show load-more buttons or infinity-scroll loading indicators.
  • Make existing LoadMoreButton widgets conditional on paginationMode == PaginationMode.loadMore
  • Add conditional CircularProgressIndicator widgets at list bottoms when paginationMode == PaginationMode.infinityScroll and _isLoadingMore is true
  • Adjust itemCount calculations in ListView/ListView.separated to account for either a button row, a loading indicator row, or neither
  • Return SizedBox.shrink() for out-of-range indices to keep builders safe
src/lib/presentation/ui/features/habits/components/habits_list.dart
src/lib/presentation/ui/features/tasks/components/tasks_list.dart
src/lib/presentation/ui/features/app_usages/components/app_usage_list.dart
src/lib/presentation/ui/features/notes/components/notes_list.dart
src/lib/presentation/ui/features/tags/components/tags_list.dart
src/lib/presentation/ui/features/app_usages/components/app_usage_tag_rule_list.dart
src/lib/presentation/ui/features/app_usages/components/app_usage_ignore_rule_list.dart
Wire infinity scroll mode from pages into their respective list widgets for main app flows.
  • Pass PaginationMode.infinityScroll into list components on main feature pages (habits, tasks, notes, tags)
  • Enable infinityScroll for app usage rules and ignore-rule lists on AppUsageRulesPage
  • Enable infinityScroll for the main AppUsageList on AppUsageViewPage
src/lib/presentation/ui/features/app_usages/pages/app_usage_rules_page.dart
src/lib/presentation/ui/features/app_usages/pages/app_usage_view_page.dart
src/lib/presentation/ui/features/habits/pages/habits_page.dart
src/lib/presentation/ui/features/notes/pages/notes_page.dart
src/lib/presentation/ui/features/tags/pages/tags_page.dart
src/lib/presentation/ui/features/tasks/pages/tasks_page.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • PaginationMode is only considered when setting up scroll listeners in initState; if a caller changes paginationMode after creation, the scroll listener won’t be updated, so consider handling add/remove of the listener in didUpdateWidget based on mode changes.
  • The infinity scroll wiring (state flag, _setupScrollListener, _onScroll, _checkAndFillViewport, loading indicator logic) is duplicated across several list widgets; consider extracting this into a shared mixin/helper to reduce repetition and keep behavior consistent.
  • The viewport fill check using maxScroll <= 0 may be fragile on different layouts; using ScrollMetrics.extentAfter (or similar) to decide when more items are needed would make the auto-load trigger more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- PaginationMode is only considered when setting up scroll listeners in initState; if a caller changes paginationMode after creation, the scroll listener won’t be updated, so consider handling add/remove of the listener in didUpdateWidget based on mode changes.
- The infinity scroll wiring (state flag, _setupScrollListener, _onScroll, _checkAndFillViewport, loading indicator logic) is duplicated across several list widgets; consider extracting this into a shared mixin/helper to reduce repetition and keep behavior consistent.
- The viewport fill check using maxScroll <= 0 may be fragile on different layouts; using ScrollMetrics.extentAfter (or similar) to decide when more items are needed would make the auto-load trigger more robust.

## Individual Comments

### Comment 1
<location> `src/lib/presentation/ui/features/habits/components/habits_list.dart:112-113` </location>
<code_context>
     super.dispose();
   }

+  void _setupScrollListener() {
+    if (widget.paginationMode == PaginationMode.infinityScroll) {
+      _scrollController.addListener(_onScroll);
+    }
</code_context>

<issue_to_address>
**issue (bug_risk):** Pagination mode changes at runtime are not reflected in the scroll listener setup.

The listener is configured only once in `initState` from the initial `widget.paginationMode`. If the parent rebuilds `HabitsList` with a different mode (e.g. switching between `loadMore` and `infinityScroll`), the listener setup won’t adjust: infinity mode may lack a listener, or a disabled mode may keep one attached. Handle this in `didUpdateWidget` by detecting changes in `paginationMode` and adding/removing the listener accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@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

This pull request introduces infinity scroll functionality to several list components, including AppUsageIgnoreRuleList, AppUsageList, AppUsageTagRuleList, HabitsList, NotesList, TagsList, and TaskList. This involves adding a paginationMode enum to control whether a list uses a 'Load More' button or automatic infinity scrolling, and implementing corresponding scroll listener and loading logic within each component. Review comments indicate that the infinity scroll logic is highly duplicated across these components and suggest refactoring it into a reusable Dart mixin. Additionally, the itemCount logic for lists is noted as unstable, causing UI jumps, and should be adjusted to always reserve a slot for the loading indicator. Finally, the scroll threshold of 0.8 is identified as a magic number and should be extracted into a named constant for better maintainability.

@ahmet-cetinkaya
Copy link
Owner Author

Code review

Found 5 issues:

  1. Race condition in infinity scroll loading causing duplicate page loads (bug in _loadMoreInfinityScroll across all list components)

Future<void> _loadMoreInfinityScroll() async {
if (_isLoadingMore || _tasks == null || !_tasks!.hasNext) return;
setState(() => _isLoadingMore = true);
await _getTasksList(pageIndex: _tasks!.pageIndex + 1);
if (mounted) {
setState(() => _isLoadingMore = false);
}
}

  1. Scroll listener memory leak - listeners added conditionally but always removed (CLAUDE.md says: "Robust error handling and input validation")

void dispose() {
_dragStateNotifier.dispose();
_removeEventListeners();
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}

  1. Magic value 0.8 for scroll threshold violates "No magic values" rule (CLAUDE.md says: "No magic values; use constants/enums")

final threshold = maxScroll * 0.8; // Load more when 80% scrolled

  1. Off-by-one error causing infinite loading when content fits screen (bug in threshold check when maxScroll = 0)

final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
final threshold = maxScroll * 0.8; // Load more when 80% scrolled

  1. Missing error handling in infinity scroll loads (CLAUDE.md says: "Robust error handling and input validation")

Future<void> _loadMoreInfinityScroll() async {
if (_isLoadingMore || _tasks == null || !_tasks!.hasNext) return;
setState(() => _isLoadingMore = true);
await _getTasksList(pageIndex: _tasks!.pageIndex + 1);
if (mounted) {
setState(() => _isLoadingMore = false);
}
}

- Extract duplicated infinity scroll logic to PaginationMixin
- Refactor list components to implement IPaginatedWidget
- Fix race conditions in loading state
- Fix memory leaks in scroll listener management
- Improve viewport fill check robustness
- Add error handling for data loading
- Remove magic values
@ahmet-cetinkaya
Copy link
Owner Author

Auto code review

No issues found. All previously identified issues have been fixed:

  • Removed duplicate _setupEventListeners() call in app_usage_list.dart
  • Added null safety with try-catch blocks for scroll position access
  • Extracted magic number 10 to named constant _viewportFillThreshold
  • Implemented proper synchronization using Completer to prevent race conditions

The infinity scroll pagination implementation is now robust and production-ready.

- Wrap scrollController.position access in try-catch blocks
- Protects against potential errors when scroll position is accessed
- Maintains stability in _onScroll and checkAndFillViewport methods
- Addresses reported issue where position could throw exceptions even when hasClients is true

Fixes issue reported by Bug Agent 2 about missing null checks in pagination_mixin.dart
@ahmet-cetinkaya ahmet-cetinkaya merged commit 2dcffa0 into main Dec 16, 2025
3 of 4 checks passed
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.

2 participants