Test CharacterListViewModel and CharacterDetailViewModel entirely through their
MVI surface with a FakeCharacterRepository (a fake, not a mock) and a directly
constructed SavedStateHandle, on StandardTestDispatcher. Coverage: happy path,
error -> UiText + snackbar Event, pagination end-reached, the in-flight and
duplicate next-page guards, process-death restore, and both branches of OnRetry.
Also a domain test for GetCharactersPageUseCase (delegation + error
propagation).
A runnable MVI screen (reached from the list overflow menu) that forces a real
DataError.Network case and routes it through the same pipeline a genuine call
uses: Result.Error -> onFailure -> DataError.toUiText() -> design-system
ErrorState. Three distinct cases (NO_INTERNET, NOT_FOUND, SERVER_ERROR) each
render their mapped message; Retry re-issues the last attempt via an Action; a
successful load clears the error. Wired as intra-feature navigation
(ErrorDemoRoute) and registered in Koin (incl. the UseCase factoryOf).
Add a domain UseCase (operator invoke) in :feature:characters:domain delegating
to CharacterRepository, and have CharacterListViewModel depend on it instead of
the repository directly. The UseCase is a deliberate thin pass-through that
documents the 'when to add a UseCase' convention (real logic / multi-source
composition vs. a single forwarded call).
Add a CharacterDetail MVI stack (State/Action/Event/ViewModel + CharacterDetailUi)
to the UI-agnostic :feature:characters:presentation. The detail ViewModel reads the
typed characterId from SavedStateHandle (populated by the type-safe CharacterDetailRoute),
so the module keeps zero navigation/Compose deps.
Add CharacterDetailScreen (Root/Screen, image header, attribute rows, loading/error)
and CharacterDetailRoute to :presentation-compose; refactor charactersGraph to drive
list->detail via NavController and expose About / Views entries as callbacks. Extract
shared CharacterStatus label/colour helpers; add an overflow menu to the list app bar.
Add material-icons-core to the compose bundle for the app-bar icons.
- Race: isLoadingNextPage was set inside the launched coroutine, so a rapid second
OnLoadNextPage passed the guard before the flag flipped -> the same page loaded twice and
items were appended twice. Set the loading flag synchronously before launching.
- Restore: when a middle page failed after earlier pages loaded, the error was swallowed
(error=null, no event). Now any restore failure emits a ShowSnackbar; partial restores show
the loaded list + snackbar, full failures show the error state.
Found by the milestone review.
- charactersPresentationModule: viewModelOf(::CharacterListViewModel) (in the UI-agnostic module).
- @Serializable CharacterListRoute + NavGraphBuilder.charactersGraph { composable<CharacterListRoute> }
in presentation-compose (serialization plugin added for type-safe routes).
- :app registers coreDataModule + charactersDataModule + charactersPresentationModule in startKoin,
and hosts a NavHost(startDestination = CharacterListRoute) calling charactersGraph.
- core:data manifest declares INTERNET (merges into :app) for live API calls.
- CharacterListState (characters, isLoading, isLoadingNextPage, currentPage, endReached, error: UiText?),
CharacterListAction (OnCharacterClick/OnRetry/OnLoadNextPage), CharacterListEvent (NavigateToDetail/ShowSnackbar).
- CharacterListViewModel: state via .update, one-time events via Channel, DataError -> UiText on failure,
pagination persisted in SavedStateHandle (rebuilds list up to the saved page after process death).
- CharacterUi + Character.toCharacterUi().
- NO Compose/Views deps: verified no androidx.compose on the compile classpath. Stability via
ImmutableList instead of @Stable (which would require compose-runtime) — the only compose-named
transitive is kotlinx-immutable's annotations-only stub, not the Compose framework.