DEV Community

katz
katz

Posted on

Implementing Jetpack Compose + Orbit MVI

I created a Pokémon Libary application with Jetpack Compose + Orbit MVI. Orbit MVI is easy to use, try it.

Image.png

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.

architecture-OVERALL.drawio.png

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.

Data Flow for UI (UI State Managment)

This application data flow is undirectional, as shown in the bottom figures.

  1. MVI View sends Action(intent) to MVI Model.
  2. MVI Model executes UseCase when received Action
  3. UseCase accesses to Repository to get Pokémon Data.
  4. UseCase returns Pokémon Data to MVI Model.
  5. MVI Model creates new state from Pokémon Data.
  6. MVI Model sends created new states or new events to MVI View.

architecture-DATAFLOW.drawio.png

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()) }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

References

Discussion (0)