The article describes handling state inside a ViewModel.

Other articles on the subject of performing work insine a ViewModel have been already published in this blog. This time I am addressing specifically handling the state, such as Idle, Pending, In Progress, Error, Success, where the number of available states depends on the tasks.

The problem

Write a demonstation of asynchronous execution of two different tasks - downloading data from the internet (more complex) and loading it from a local database (less complex).

The project

The project is Weather. In the project the user selects location on a map, or types in a city name. As soon as the location is thus selected, the program displays the weather using the service https://openweathermap.org/api.

The reader may read a full description in Polish of the task.

Other materials

The problem I am discussing in this blog has already beed mentioned in another blog:

You don’t usually extend LiveData. Let your activity or fragment tell the ViewModel when it’s time to start loading data.

The blog article also points to an example source file demonstrating where one may put a function launching the loading process.

In the present article I demonstrate launching data loading by a dedicated function (the more complex task), as well as doing it in the ViewModel’s constructor for a task that is expected to execute quickly and without problem.

I do understantd the merits of extending LiveData, and I did it once, although then I quickly refactored it to use the liveData() function.

The default state - Idle

By default the state of my ViewModel is State.Idle. It’s defined in the WeatherModel:

private var currentState: State = State.Idle
private val mutableLiveState = MutableLiveData(currentState)

It can be reset when the last presented weather report is no longer required by the GUI:

fun reset() {
    searchJob?.cancel()
    currentState = State.Idle
    mutableLiveState.value = currentState
}

The above reset() function is called when the user leaves the Fragment presenting the weather report:

requireActivity().onBackPressedDispatcher.addCallback(this) {
        viewModel.reset()
        back()
    }

The Idle state is specifically required when setting the weather report read form the local database. Because reading from the local database (as opposed to from the internet) doesn’t require handling failures, the weather result is set to Success directly from idle. The following function is never allowed to be called in another situation. For example, this function will throw in it is called while the data is already being dowloaded:

fun setWeather(weather: WeatherJson) {
    check(currentState == State.Idle) { "Only set fixed weather from Idle state" }
    currentState = State.Success(weather)
    mutableLiveState.value = currentState
}

Setting location

One of the following two functions are called when the user selects from the GUI ethrer the latitude and longitude of the desired location, or the city name:

fun pendingSearch(location: LatLng) { currentState = State.Pending( WeatherService.Query.Location(LatLng(location.latitude, location.longitude))) mutableLiveState.value = currentState }

fun pendingSearch(city: String) { currentState = State.Pending(WeatherService.Query.City(city)) mutableLiveState.value = currentState }

This is the code that launches the search inside the ViewModel right before navigating to another Fragment. The desired location should be already set using one of the two functions described in the previous section:

fun launchSearch() {
    viewModel.launchSearch()
    nav(R.id.action_form_to_presentation)
}

The above code is placed in the Fragment.

The code below is inside of the the ViewModel that (1) checks the current state, (2) sets the new state, (3) launches an asynchronous search:

fun launchSearch() {
    currentState.let { state ->
        check(state is State.Pending) { "Only launch search from Pending state" }
        currentState = State.InProgress
        mutableLiveState.value = currentState
        searchJob = viewModelScope.launch {
            val response = when (val query = state.query) {
                is WeatherService.Query.City -> weatherService.getCity(query.city)
                is WeatherService.Query.Location ->
                    weatherService.getLocation(query.location.latitude, query.location.longitude)
            }
            log.info(response.raw().toString())
            currentState = if (response.isSuccessful) {
                val weather = checkNotNull(response.body())
                withContext(NonCancellable) {
                    weatherDao.add(weather)
                }
                State.Success(weather)
            }
            else State.Error
            mutableLiveState.value = currentState
        }
    }
}

Please note that viewModelScope, since version 2.2.0-alpha04, by default uses Dispatchers.Main.immediate, so there is no need to explicitly tell it it should run on main thread (it already knows that). Retrofit by default uses Dispatchers.IO, so I never actually have to switch CoroutineContext to manually set the CoroutineDispatcher properly. I only write withContext(NonCancellable) when I want to prevent a portion of the code from being canceled. I discussed preventing coroutine from canceling in another article in this blog.

Handling the state

This is the code inside the Fragment that handles the state:

(viewModel.liveState to this) { state ->
    when (state) {
        is WeatherViewModel.State.InProgress -> inProgress()
        is WeatherViewModel.State.Success -> details(state.weather)
        is WeatherViewModel.State.Error -> error()
    }
}

Only states that affect the GUI must be handled in this when statement. Examples of handling other states have already been discussed in the previous sections.

Starting simple work from inside the ViewModel

In the above sections I discussed a rather sophisticated process of setting and reacting to state held by a ViewModel.

In the present section I will describe a more simple way of launching work from the ViewModel’s constructor. I only do so before I am sure of this condition: It is okay for the ViewModel to be in the In Progress state immediately after creation. This is the case because the data required by the ViewModel to perform its task is already available even before the ViewModel is created.

This is the entire ViewModel required to read data usinng Room. Notice how much more simple it is than the ViewModel discussed over the course of a handful of the preceeding sections:

class ArchiveViewModel : ViewModel() {

    private val mutableState = MutableLiveData<State>(State.InProgress)
    val liveState get() = mutableState as LiveData<State>
    private val weatherDao by instance<WeatherDao>()

    init {
        viewModelScope.launch {
            mutableState.value = State.Success(weatherDao.getAllEntities())
        }
    }

    sealed class State {
        object InProgress : State()
        data class Success(val list: List<WeatherEntity>) : State()
    }
}

Conclusion

This article is an example of doco of a small project I did the other day as a part of an unsuccessful recruitment process.

I demonstrate herein how to find a couple of the most important features of a small project and document them. Without the present article - which I expect to substantiate the project’s value - I wouldn’t be even inclined to keep the project on github.

This discussion explained some of the possible ways of handling state in two different instances - a relatively sophisticated process of passing data between a few Fragments, and a rather simple logic of loading data from local database in a ViewModel that is kept only within the scope of a single Fragment.

If the reader has enjoyed the present documentation, and has some bitcoin lying aroung, they are welcome to look at my donations page.

If any of the readers has suggestions regarding handling state in Android projects in Kotlin relying heavily on ViewModel, I kindly ask them to submit a ticket to this blog. Thank you!