REDI-94: GetCharactersPageUseCase + inject into list ViewModel

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).
This commit is contained in:
2026-06-10 15:00:17 +02:00
parent c17a1c163b
commit 0542d4dc1d
2 changed files with 31 additions and 4 deletions

View File

@@ -0,0 +1,27 @@
package com.example.architecture.feature.characters.domain.usecase
import com.example.architecture.core.domain.DataError
import com.example.architecture.core.domain.Result
import com.example.architecture.feature.characters.domain.CharacterRepository
import com.example.architecture.feature.characters.domain.model.CharactersPage
/**
* Loads one page of characters.
*
* **When to add a UseCase (convention note):** introduce a UseCase when a screen needs business
* logic that does NOT belong in the ViewModel — non-trivial rules, or *composition* of several
* repositories/sources into one domain operation. When the ViewModel would merely forward a single
* repository call, skipping the UseCase and injecting the repository directly is perfectly fine.
*
* This particular UseCase is a **thin pass-through, included for illustration**: it adds no logic
* beyond delegating to [CharacterRepository]. It earns its place only as a showcase of the
* convention (domain-owned, `operator fun invoke`, constructor-injected). In a real app you would
* grow it the moment list loading gained real behaviour (filtering, merging a local cache, …) — or
* delete it and let the ViewModel call the repository.
*/
class GetCharactersPageUseCase(
private val characterRepository: CharacterRepository,
) {
suspend operator fun invoke(page: Int): Result<CharactersPage, DataError> =
characterRepository.getCharacters(page)
}

View File

@@ -8,7 +8,7 @@ import com.example.architecture.core.domain.onFailure
import com.example.architecture.core.domain.onSuccess
import com.example.architecture.core.presentation.UiText
import com.example.architecture.core.presentation.toUiText
import com.example.architecture.feature.characters.domain.CharacterRepository
import com.example.architecture.feature.characters.domain.usecase.GetCharactersPageUseCase
import com.example.architecture.feature.characters.presentation.model.CharacterUi
import com.example.architecture.feature.characters.presentation.model.toCharacterUi
import kotlinx.collections.immutable.toImmutableList
@@ -25,7 +25,7 @@ import kotlinx.coroutines.launch
* via a [Channel], maps failures to [UiText], and persists the loaded page in [SavedStateHandle].
*/
class CharacterListViewModel(
private val characterRepository: CharacterRepository,
private val getCharactersPage: GetCharactersPageUseCase,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
@@ -62,7 +62,7 @@ class CharacterListViewModel(
var page = 1
while (page <= targetPage) {
when (val result = characterRepository.getCharacters(page)) {
when (val result = getCharactersPage(page)) {
is Result.Success -> {
accumulated += result.data.characters.map { it.toCharacterUi() }
lastLoadedPage = page
@@ -123,7 +123,7 @@ class CharacterListViewModel(
// get appended twice.
_state.update { it.copy(isLoadingNextPage = true, error = null) }
viewModelScope.launch {
characterRepository.getCharacters(page)
getCharactersPage(page)
.onSuccess { pageData ->
_state.update { state ->
state.copy(