Skip to content

Use Flow to debounce search instead of search delay#883

Merged
Crustack merged 1 commit intomainfrom
fix/879
Mar 4, 2026
Merged

Use Flow to debounce search instead of search delay#883
Crustack merged 1 commit intomainfrom
fix/879

Conversation

@Crustack
Copy link
Owner

@Crustack Crustack commented Mar 3, 2026

Fixes #879

Summary by CodeRabbit

  • New Features

    • Search now delivers live, debounced results with clearer loading state and a reliable results view.
  • Bug Fixes

    • List updates now refresh only affected items, reducing flicker and improving perceived speed.
    • Prevents redundant keyword updates to avoid unnecessary UI churn.
    • Empty-state visibility improved during loading.
  • Refactor

    • Under-the-hood reactive improvements for smoother, more consistent search and list behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

The PR converts DAO and model layers from LiveData to Kotlin Flow, introduces debounced search via a Flow-based SearchResult, adds Flow-to-LiveData conversion helpers, and optimizes adapter updates to refresh only affected items during keyword changes.

Changes

Cohort / File(s) Summary
Build Configuration
app/build.gradle.kts
Added androidx.lifecycle:lifecycle-livedata-ktx:2.8.7 to enable Flow ↔ LiveData conversion utilities.
Data Access Layer
app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
Changed DAO return types from LiveData<List<BaseNote>> to Flow<List<BaseNote>> for label/keyword queries; replaced LiveData.map usages with Flow.map.
Data Model: Content
app/src/main/java/com/philkes/notallyx/data/model/Content.kt
Added secondary constructor accepting Flow<List<BaseNote>>, a transform, and a CoroutineScope, converting the Flow to LiveData via asLiveData.
Data Model: SearchResult
app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt
Refactored from extending LiveData to using MutableStateFlow/resultFlow with debounce + flatMapLatest; exposes results LiveData, isLoading, fetch(...), and observe(...). Added SearchParams state and loading/error flow handling.
ViewModel Layer
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
Updated SearchResult and Content constructor calls to include additional app / scope arguments; removed external debounce usage, delegating to SearchResult.
Presentation Layer
app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt, app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt
notesAdapter visibility widened to protected. SearchFragment now observes searchResults.results, sets adapter keyword on item emission, and toggles empty-state visibility based on isLoading and isEmpty.
UI / Adapter
app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt
Replaced full dataset refresh with targeted per-item updates in setSearchKeyword; added matchesKeyword helper to determine which items to refresh.

Sequence Diagram

sequenceDiagram
    participant User as User Input
    participant SearchFrag as SearchFragment
    participant SearchResult as SearchResult
    participant DAO as BaseNoteDao
    participant Adapter as BaseNoteAdapter
    participant UI as UI List

    User->>SearchFrag: type query
    SearchFrag->>SearchResult: fetch(keyword, folder, label)
    activate SearchResult
    SearchResult->>SearchResult: update searchParams (MutableStateFlow)
    SearchResult->>SearchResult: debounce -> flatMapLatest -> map(transform)
    deactivate SearchResult

    SearchResult->>DAO: request matching notes (Flow)
    activate DAO
    DAO-->>SearchResult: Flow<List<BaseNote>>
    deactivate DAO

    SearchResult->>SearchFrag: emit results (as LiveData)
    SearchFrag->>Adapter: setSearchKeyword(keyword)
    activate Adapter
    Adapter->>Adapter: matchesKeyword per item
    Adapter->>UI: notifyItemChanged (targeted)
    deactivate Adapter
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through flows and chased the spin,
Debounced the queries, let the streams begin,
Notes now whisper, no more frantic race,
Only matching items hop into place. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing search delay with Flow-based debouncing to address stuttering search issues.
Linked Issues check ✅ Passed The PR implements Flow-based debouncing for search using MutableStateFlow with debounce and flatMapLatest operators, directly addressing issue #879's search stuttering problem.
Out of Scope Changes check ✅ Passed All changes are scoped to search functionality: DAO Flow migration, Content constructor updates, SearchResult Flow implementation, and adapter optimization. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/879

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 3

🧹 Nitpick comments (1)
app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt (1)

74-95: Consolidate duplicated keyword-matching logic.

matchesKeyword is duplicated here and in app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt (Line 326-344). Keeping two implementations in sync is error-prone and can desync filtering vs highlighting behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt`
around lines 74 - 95, The duplicated keyword-matching logic in the
matchesKeyword function should be consolidated into a single reusable routine:
extract the current implementation into a shared function (for example a
top-level function like fun matchesKeyword(baseNote: BaseNote, keyword: String):
Boolean or an extension fun BaseNote.matchesKeyword(keyword: String): Boolean)
that preserves behavior (return false when keyword.isBlank(), case-insensitive
checks against baseNote.title, baseNote.body, each label, and each item.body),
then replace the local matchesKeyword in BaseNoteAdapter and the duplicate
implementation in BaseNoteDao with calls to that single shared function so both
filtering and highlighting use the same code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt`:
- Around line 37-47: The loading flag can get stuck true if
baseNoteDao.getBaseNotesByKeyword throws before emitting; inside the
flatMapLatest branch where you call
baseNoteDao.getBaseNotesByKeyword(params.keyword, params.folder, params.label)
(and then .map { transform(it) }.onEach { _isLoading.value = false }), add error
and completion handling so _isLoading.value is set to false on both error and
normal completion — e.g. attach a .catch { /* set _isLoading.value = false and
rethrow/log if needed */ } and/or an .onCompletion { _isLoading.value = false }
after the DAO flow (or replace onEach with onCompletion to guarantee cleanup),
ensuring references to _isLoading, baseNoteDao.getBaseNotesByKeyword, transform,
flatMapLatest, and onEach are preserved and updated accordingly.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt`:
- Around line 225-228: The callback is using requireNotNull(text, ...) which can
crash if Android passes a null Editable; change the null handling to a null-safe
conversion by replacing the requireNotNull usage with text?.toString().orEmpty()
so newKeyword becomes a safe String, then keep the existing
comparison/assignment to model.keyword (i.e., update the code around newKeyword
and the if (model.keyword != newKeyword) block to use the null-safe value).

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt`:
- Line 53: The line binding?.ImageView?.isVisible = !isLoading incorrectly shows
the empty-state image whenever loading finishes regardless of results; change
the visibility logic so the empty image is only visible when loading is false
AND the results list is empty (e.g., use the fragment's adapter.itemCount == 0
or viewModel.searchResults.value?.isEmpty() check). Update the code in
SearchFragment.kt where binding?.ImageView is set to compute isVisible =
!isLoading && (adapter.itemCount == 0 or searchResults.isEmpty()), and ensure
you update this whenever the adapter or searchResults change.

---

Nitpick comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt`:
- Around line 74-95: The duplicated keyword-matching logic in the matchesKeyword
function should be consolidated into a single reusable routine: extract the
current implementation into a shared function (for example a top-level function
like fun matchesKeyword(baseNote: BaseNote, keyword: String): Boolean or an
extension fun BaseNote.matchesKeyword(keyword: String): Boolean) that preserves
behavior (return false when keyword.isBlank(), case-insensitive checks against
baseNote.title, baseNote.body, each label, and each item.body), then replace the
local matchesKeyword in BaseNoteAdapter and the duplicate implementation in
BaseNoteDao with calls to that single shared function so both filtering and
highlighting use the same code path.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f09828 and 9fe1b4c.

📒 Files selected for processing (8)
  • app/build.gradle.kts
  • app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
  • app/src/main/java/com/philkes/notallyx/data/model/Content.kt
  • app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

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.

🧹 Nitpick comments (1)
app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt (1)

84-85: The isEmpty extension returns false when value is null, which may be misleading.

The expression this?.value?.isEmpty() == true returns false when this is null or when value is null. This means a SearchResult with no results yet (null value) is considered "not empty", which could cause the empty-state image to not show during initial load or after errors.

Consider whether the semantics should treat null as empty:

♻️ Suggested alternative if null should be treated as empty
 val SearchResult?.isEmpty: Boolean
-    get() = this?.value?.isEmpty() == true
+    get() = this?.value?.isEmpty() != false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt` around
lines 84 - 85, The current SearchResult?.isEmpty extension treats a null
SearchResult or a null value as not empty; change the logic so null is
considered empty by returning true when the receiver is null or when value is
null/empty — update the getter on SearchResult?.isEmpty (referring to the
SearchResult class and its value property) to return true if this == null ||
this.value.isNullOrEmpty(), otherwise false, so the empty-state UI behaves
correctly on initial load or after errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt`:
- Around line 84-85: The current SearchResult?.isEmpty extension treats a null
SearchResult or a null value as not empty; change the logic so null is
considered empty by returning true when the receiver is null or when value is
null/empty — update the getter on SearchResult?.isEmpty (referring to the
SearchResult class and its value property) to return true if this == null ||
this.value.isNullOrEmpty(), otherwise false, so the empty-state UI behaves
correctly on initial load or after errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6c3cd53c-0589-4b33-99e6-a61a66be200b

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe1b4c and 86951f5.

📒 Files selected for processing (8)
  • app/build.gradle.kts
  • app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
  • app/src/main/java/com/philkes/notallyx/data/model/Content.kt
  • app/src/main/java/com/philkes/notallyx/data/model/SearchResult.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/philkes/notallyx/data/model/Content.kt
  • app/build.gradle.kts
  • app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt

@Crustack Crustack merged commit 0a37cdc into main Mar 4, 2026
1 check passed
@Crustack Crustack deleted the fix/879 branch March 4, 2026 17:52
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.

Stuttering search

1 participant