State of Compose 2023 results are in! Click here to learn more
Published on

🚀 Why you need ViewModels and why you don't

Authors

Happy Saturday 👋 Alex Styl here.

In today's tutorial we are covering everything you need to know about ViewModels in Jetpack Compose. What they are, why you need them and why you don't. ViewModels are popular in the Android world. What if you want to use them outside of Android thought? What are the alternatives?

Let's find out folks.

What is an Android ViewModel?

ViewModel is an AndroidX API that makes it simpler for Android developers to keep data across configuration changes.

At the same time, ViewModels are the place where you want to keep your business logic. When the user interacts with your UI (ie taps on a button in your app) you can forward that intent to the ViewModel, do any calculations and emit a new state to your UI for it to render.

They are convenient to use as the ViewModel instance itself is kept across configuration changes. This means that you can safely perform asynchronous operations inside of them and emit a new state when needed.

Why do ViewModels exist?

When Android first came out, ViewModels were not a thing. There were a few available APIs that you could pass the data you would like to keep in configuration changes as part of the Activity (the main component Android uses to render your screen in).

Back then the notion was for your Activity to hold all the logic of your screen. As apps became more interactive, data rich and user expectation kept growing, the code you would normally keep in your Activity became more complex. There were a few solutions to this problem, with the introduction of Fragments, where they were meant to be mini Activities. This simplified things a bit but it came with their own set of problems as Fragments have their own lifecycle you need to manage.

Fast-forward to today, after a lot of architecture discussions in the Android community, the most common architecture design is to use a single Activity to host your main part of the app, and one ViewModel per screen holding its UI state and business logic.

How to use ViewModels in your Jetpack Compose Android app

ViewModel is part of the lifecycle-viewmodel library, which is already included in the activity-compose library (along with the Activity.setContent {} function).

In order to use ViewModel within your composables you also need the lifecycle-viewmodel-compose dependency, which brings the viewModel {} composable function.

Include the following dependencies in your project:

// build.gradle

dependencies {
    implementation 'androidx.activity:activity-compose:1.7.1'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
}

Next, create a ViewModel class that will hold our screen's business logic.

For this example we will build a basic calculator app.

The layout of the calculator app

Our ViewModel is going to hold the current sum of all calculations we have done so far. We will also expose a function that will increase the sum given a number:

class CalculatorViewModel : ViewModel() {
    var sum by mutableStateOf(0)
        private set

    fun addNumber(number: Int) {
        sum += number
    }
}

In order to use this ViewModel in our composable function/screen, we would need to use the viewModel {} function. This function is will create a instance of the passing ViewModel, or will return an existing instance if it exists in the scope you are creating the ViewModel in (either an Activity or a Fragment). This is how it will always return the same ViewModel across configuration changes:

val viewModel = viewModel { CalculatorViewModel() }

we can now use the sum and the addNumber() function in our UI:

val viewModel = viewModel { CalculatorViewModel() }
Column(
    Modifier
        .fillMaxSize()
        .padding(10.dp)
) {
    Text(
        viewModel.sum.toString(),
        modifier = Modifier.align(Alignment.CenterHorizontally),
        style = MaterialTheme.typography.headlineMedium
    )
    Spacer(Modifier.height(24.dp))
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        repeat(3) { y ->
            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                repeat(3) { x ->
                    val number = ((y * 3) + x) + 1
                    key(number) {
                        FloatingActionButton(
                            onClick = { viewModel.addNumber(number) },
                            modifier = Modifier.weight(1f)
                        ) {
                            Text(number.toString())
                        }
                    }
                }
            }
        }
    }
}

as soon as the user clicks on a button, the number will be forwarded to the ViewModel, the new sum will be calculated and then emitted to the UI.

Using the ViewModel we will not have to worry about losing the sum during configuration changes.

How long does a ViewModel "live"?

The viewModel {} function will make sure to create the ViewModel when required. It will also make sure to clean up your ViewModel as soon as the activity or the process is killed.

There are two moments you need to be aware of: initialization and cleanup.

You can get notified when your ViewModel is being initialized using Kotlin's init {} block. I find it particularly useful to start loading data I need about the screen there (such as loading from the database or fetch data from the Web).

Likewise, you can be notified about the ViewModel being cleared up by overriding the onCleared() function.

class MyViewModel : Viewmodel {
    init {
        viewModelScope.launch {
            // TODO load data and emit new state
        }
    }

    override fun onCleared() {
        super.onCleared()
        // TODO release any resources related to this ViewModel
    }
}

Keep in mind that if you are using the viewModelScope to launch coroutines from your ViewModel, the scope is going to be cancelled in the onCleared() call. This means that the viewModelScope is not a good scope to run long-running operations in, as it can be wiped without the operation to be completed. To perform long-running operations in Android, have a look at the Job Scheduler or Service Android APIs and Kotlin's GlobalScope.

Do you really need a ViewModel?

Technically speaking, you do not need a ViewModel. ViewModels are an API provided to simplify the life of Android developers by not having to worry about persisting your data across configuration changes. It also provides a nice place to keep your business logic in one place.

On the one hand, you can persist your state instance across configuration changes using the rememberSaveable {} function. This function works like remember {} but it will survive the activity or process recreation.

On the other hand, there is currently no Jetpack Compose coroutine related function/API that persists orientation change. Functions such as LaunchedEffect() or rememberCoroutineScope() will cancel their scope as soon as the composable exits the composition. You could however create a coroutine scope tied to the screen, like ViewModels do.

I will personally continue using ViewModels in my Android apps as they are a nice place to keep my business logic and connecting it to my UI feels straightforward for my needs. A lot of architecture and wiring thinking is handled for you, so that you can focus on your app's unique features. It is not a one solution fits all, however. The architecture of your app is strongly tied to your business and team's requirements.

What are the alternatives to a ViewModel outside of Android?

It is possible to create your own version of ViewModel. In fact that's what I use for the Desktop version of Ubidrop.

I have created an abstract class called AbstractStore that keeps a CoroutineScope reference:

abstract class AbstractStore {
    protected val storeScope = CoroutineScope(
        Dispatchers.Main.immediate
                    + SupervisorJob()
                    + CoroutineExceptionHandler{ coroutineContext, throwable->
            // TODO track the throwable
            exitProcess(1) // terminate the app
        }
    )

    open fun onCleared() {
        storeScope.cancel()
    }
}

the way to use it is similar to AndroidX' ViewModel. You can subclass the store, use the storeScope to launch your coroutines and handle any business logic you need. As there is no viewModel {} function outside of Android, we would need to create our own version of it:

@Composable
fun <T : AbstractStore> store(createStore: () -> T): T {
    val store = remember { createStore() }
    DisposableEffect(Unit) {
        onDispose {
            store.onCleared()
        }
    }
    return store
}

which you can use in your composable like this:

val store = store { CalculatorStore() }

In a JVM app (Java desktop for example) you do not have to worry about configuration changes or activity recreations. Because of this, things are straightforward.

There are also third party libraries such as Decompose that create a platform-agnostic version of the ViewModel so that you can use it from any platform.


That's all for today's tutorial on ViewModels. We went through how ViewModels came as the solution to the problem of maintaining data across configuration changes in Android and how they help running asynchronous operations. We also saw alternatives to ViewModels and using them outside of Android.

Until the next one!