Building a State Management Wrapper for Android Using Koin, and Jetpack Compose [2024]

Ruslan Gaivoronskii
5 min readNov 29, 2024

--

In this article, I describe StateWrapper — a solution for state management in Android applications. StateWrapper allows handling different states such as data loading, successful operation completion, and errors, with screen updates.

1. Defining Library Versions

To manage dependencies effectively, we use a centralized approach for versioning and grouping libraries:

[versions]
lifecycleRuntimeKtx = "2.8.7"
koin = "4.0.0"

[libraries]
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }

[bundles]
koin-bundle = [
"koin-core",
"koin-androidx-compose",
]

2. Adding Dependencies to the Application Module

In the build.gradle.kts file for the app module, add the necessary dependencies:

dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.bundles.koin.bundle)
}

3. Introducing the StateWrapper Sealed Interface

The StateWrapper interface is used to encapsulate different states of data, such as loading, success, or failure:

sealed interface StateWrapper<out T, out E> {

data object Loading : StateWrapper<Nothing, Nothing>

data class Success<T>(val data: T) : StateWrapper<T, Nothing>

data class Failure<E>(val error: E) : StateWrapper<Nothing, E>
}

fun <T, E> StateWrapper<T, E>.getData(): T = (this as StateWrapper.Success).data

fun <E> StateWrapper<*, E>.getErrorOrNull(): E? =
(this as? StateWrapper.Failure<E>)?.error

4. Repository Interface and Implementation

Define the repository and its implementation to provide data flow:

interface IRepository {
fun loadData(): Flow<StateWrapper<String, Any>>
}

class RepositoryImpl : IRepository {
override fun loadData(): Flow<StateWrapper<String, Any>> = flow {
emit(StateWrapper.Loading)
try {
emit(StateWrapper.Success("Data"))
} catch (e: Exception) {
emit(StateWrapper.Failure())
}
}
}

5. State Class to Represent the App’s State

The AppState class models the app's state, including loading, success, and error scenarios:

data class AppState(
val isSuccess: Boolean = false,
val isLoading: Boolean = false,
val error: Pair<Boolean, String?> = false to null,
val data: String = ""
)

6. Event Sealed Interface

The AppEvent sealed interface defines app events:

sealed interface AppEvent {
data object LoadData : AppEvent
}

7. ViewModel Implementation

The AppViewModel processes events and updates state based on repository responses:

class AppViewModel(
private val repository: IRepository
) : ViewModel() {

private val _state = MutableStateFlow(AppState())
val state = _state.asStateFlow()

init {
onEvent(AppEvent.LoadData)
}

private fun onEvent(event: AppEvent) {
when (event) {
AppEvent.LoadData -> {
viewModelScope.launch {
repository.loadData().collect { stateWrapper ->
when (stateWrapper) {
StateWrapper.Loading -> {
_state.update {
it.copy(isLoading = true)
}
}

is StateWrapper.Failure -> {
_state.update {
it.copy(
error = Pair(
true,
stateWrapper.getErrorOrNull().toString()
)
)
}
}

is StateWrapper.Success -> {
_state.update {
it.copy(
isLoading = false,
isSuccess = true,
data = stateWrapper.getData()
)
}
/**
* Simulate showing an animation
* */
delay(2000)

/**
* Reset the success state after the animation
* */
_state.update {
it.copy(isSuccess = false)
}
}
}
}
}
}
}
}
}


8. Koin Module Configuration

Define a Koin module for dependency injection:

val appModule = module {
singleOf(::RepositoryImpl) { bind<IRepository>() }
viewModelOf(::AppViewModel)
}

9. Application Class Setup

Integrate Koin into the application lifecycle:

class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@App)
androidLogger(Level.DEBUG)
modules(appModule)
}
}
}

10. Register the Application Class in AndroidManifest.xml

<application
android:name=".app.App"
... />

11. Enum for Screen States

Create an enum for managing screen transitions:

enum class CrossFade {
SUCCESS,
ERROR,
LOADING,
CONTENT
}

12. Composable Functions for UI States

Define reusable composable functions for each state:

//Loading
@Composable
internal fun LoadingScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
Text(text = "Loading...")
}
}
}

//Error
@Composable
internal fun ErrorScreen(
isError: Pair<Boolean, String?>
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = isError.second.orEmpty())
}
}
}

//Success
@Composable
internal fun SuccessScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Data loaded successfully")
}
}
}

//Content
@Composable
internal fun ContentScreen(
data: String
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = data)
}
}
}

13. MainActivity Implementation

Set up the UI with Crossfade for state transitions:

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate()
enableEdgeToEdge()
setContent {
val appViewModel = koinViewModel<AppViewModel>()

KoinAndroidContext {
StateWrapperComposeTheme {
val state by appViewModel.state.collectAsStateWithLifecycle()
val isLoading = state.isLoading
val isError = state.error
val isSuccess = state.isSuccess
val data = state.data

Crossfade(
targetState = when {
isError.first -> CrossFade.ERROR
isLoading -> CrossFade.LOADING
isSuccess -> CrossFade.SUCCESS
else -> CrossFade.CONTENT
},
label = ""
) { screenState ->
when (screenState) {
CrossFade.LOADING -> LoadingScreen()
CrossFade.CONTENT -> ContentScreen(data)
CrossFade.SUCCESS -> SuccessScreen()
CrossFade.ERROR -> ErrorScreen(isError = isError)
}
}
}
}
}
}
}

Next steps

We add a delay(3000) to simulate a long loading process,

class RepositoryImpl : IRepository {
override fun loadData(): Flow<StateWrapper<String, Any>> = flow {
emit(StateWrapper.Loading)
delay(3000) // add delay to simulate loading
try {
emit(StateWrapper.Success("Data"))
} catch (e: Exception) {
emit(StateWrapper.Failure())
}
}
}

allowing us to display the Loading state on the screen. The result will look like this:

Adding Error Simulation in the Repository

In this step, we introduce an artificial error to simulate a failure during data loading. To do this, we throw an exception in the loadData method using throw IllegalStateException("Error"). When the error occurs, we catch it in a catch block and pass the corresponding state to the data flow, allowing the UI to react to the error:

class RepositoryImpl : IRepository {
override fun loadData(): Flow<StateWrapper<String, Any>> = flow {
emit(StateWrapper.Loading)
try {
throw IllegalStateException("Error")// Simulate an error
emit(StateWrapper.Success("Data"))
} catch (e: Exception) {
emit(StateWrapper.Failure(e))
}
}
}

The result will look like this:

On success, we will see the success screen:

And finally, the screen with our content:

Now you have an understanding of how to use StateWrapper for handling different states in your application, such as loading, error, and success. This architecture helps you manage state centrally and provide a better user experience. I hope you can apply these approaches in your own projects to improve data handling and create more stable applications. Don't forget to experiment and adapt this approach to your specific needs!

Thank you for reading! You can find the full code on GitHub. 😊

--

--

Ruslan Gaivoronskii
Ruslan Gaivoronskii

Written by Ruslan Gaivoronskii

Technically I’m writer, Android - Developer, author of Evolvify - Habit Tracker app, beat maker, and rapper

Responses (1)