fix(search): initialise the list before the collector that scrolls it

Opening search from any seeded screen crashed the app on the spot:

    NullPointerException: Attempt to invoke virtual method
    LazyListState.scrollToItem(...) on a null object reference
      at SearchBarViewModel.updateDataSource(SearchBarViewModel.kt:479)
      at SearchBarViewModel.<init>(SearchBarViewModel.kt:131)

`sourceWatcher` shares its flow `Eagerly`, so `onEach { updateDataSource(...) }`
runs while the constructor is still executing, and `updateDataSource` scrolls
`listState` -- which was declared *after* it. Kotlin initialises properties in
declaration order, so at that moment the field is still null.

It was latent until this branch. `updateDataSource` returns early on a blank
term, and the box always opened blank, so the scroll was never reached during
construction. Seeding the field with the screen's own filter makes the term
non-blank on the very first pass, which turns the ordering bug into a crash the
moment search opens from any seeded feed.

Moving the declaration above the collector fixes it, and the comment says why it
has to stay there -- the next person to tidy these fields alphabetically would
put it back.

Reproduced deterministically before the change (Reads -> search, fresh crash
buffer, one FATAL) and confirmed gone after, on the same tap sequence. The
feature it was blocking now works: search opened from Reads seeds `kind:article`
as a chip, tapping the chip offers Change/Remove, and Change cuts it to `kind:`
and opens the new kind picker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-09-09 18:58:35 -04:00
co-authored by Claude Opus 5
parent ab13369fd1
commit 6f83f6c5ed
@@ -124,14 +124,20 @@ class SearchBarViewModel(
followPlusAllMineWithSearchRelays = account.followPlusAllMineWithSearch.flow,
)
// Declared before [sourceWatcher], and it must stay there. That collector is Eagerly
// shared, so it runs `updateDataSource` during construction, and `updateDataSource` scrolls
// this list -- Kotlin initialises properties in declaration order, so from below it would
// still be null. It never showed while the box opened empty, because a blank term returns
// before the scroll; seeding the field from the screen's filter made the term non-blank on
// the very first pass and turned that into an NPE the moment search opened.
val listState: LazyListState = LazyListState(0, 0)
@Suppress("unused")
val sourceWatcher =
source
.onEach { updateDataSource(searchValue) }
.stateIn(viewModelScope, SharingStarted.Eagerly, SearchSource.RELAYS)
val listState: LazyListState = LazyListState(0, 0)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
val directNip05Resolver: Flow<User?> =
searchTerm