I created a Pokémon Libary application with Jetpack Compose + Orbit MVI. Orbit MVI is easy to use, try it.
Link
This Sample Source Code
About Jetpack Compose
About Orbit MVI
Feature
- Manage Pokémon data.
- Display multiple Pokémon in a list.
- Display a Pokémon details.
Architecture
This application architecture is based on the MVVM + Repository pattern. But the view state management use MVI, because introduced Jetpack Compose and Orbit MVI.
Module
This application consists of a multi-module structure. There are three modules, and each module plays the role described in the figure below.
Name | Content |
---|---|
App | Store View and ViewModel to manage view state. |
Domain | Store UseCase to process business logic. |
Data | Store Repository and Dao to manage Pokémon database. |
Library
This application is created by the library in the bottom figures.
Name | Link |
---|---|
Koin | https://insert-koin.io |
Jetpack Compose | https://developer.android.com/jetpack/compose |
Navigation Compose | https://developer.android.com/jetpack/compose/navigation |
Orbit MVI | https://orbit-mvi.org |
Coil | https://coil-kt.github.io/coil/ |
Room | https://developer.android.com/training/data-storage/room?hl=ja |
Kotlin-Serialiazation | https://github.com/Kotlin/kotlinx.serialization |
PokemonGO-Pokedex | https://github.com/Biuni/PokemonGO-Pokedex |
Data Flow for UI (UI State Managment)
This application data flow is undirectional, as shown in the bottom figures.
- MVI View sends Action(intent) to MVI Model.
- MVI Model executes UseCase when received Action
- UseCase accesses to Repository to get Pokémon Data.
- UseCase returns Pokémon Data to MVI Model.
- MVI Model creates new state from Pokémon Data.
- MVI Model sends created new states or new events to MVI View.
This application data flow is implmented by Orbit MVI. (For more information on how to create an MVI View or MVI Model using Orbit MVI, please check here.)
MVI Model
data class InitState(
val status: UiStatus? = null
)
sealed class InitSideEffect {
object Completed : InitSideEffect()
}
class InitViewModel(
private val fetchAllPokemonUseCase: FetchAllPokemonUseCase
) : ContainerHost<InitState, InitSideEffect>, ViewModel() {
override val container = container<InitState, InitSideEffect>(
InitState()
)
init {
fetchData()
}
fun retry() {
if (container.stateFlow.value.status != UiStatus.Loading) {
fetchData()
}
}
private fun fetchData() {
intent {
reduce { state.copy(status = UiStatus.Loading) }
if (fetchAllPokemonUseCase()) {
reduce { state.copy(status = UiStatus.Success) }
postSideEffect(InitSideEffect.Completed)
} else {
reduce { state.copy(status = UiStatus.Failed()) }
}
}
}
}
MVI View
@Composable
fun InitPage(
viewModel: InitViewModel,
onCompleted: () -> Unit
) {
val state by viewModel.container.stateFlow.collectAsState()
LaunchedEffect(viewModel) {
viewModel.container.sideEffectFlow.collect {
when (it) {
is InitSideEffect.Completed -> onCompleted()
}
}
}
Scaffold {
Box(modifier = Modifier.fillMaxSize()) {
when (state.status) {
UiStatus.Loading -> {
DownloadingMessage(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center)
)
}
is UiStatus.Failed -> {
DownloadRetryMessage(
onRetry = { viewModel.retry() },
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center)
)
}
UiStatus.Success -> Unit
}
}
}
}
Top comments (0)