Blog Infos
Author
Published
Topics
, , ,
Published

In the Android/Kotlin world, there are 2 ways to provide the state from the ViewModel layer to the View layer: single state and multiple states. Which one is better? Let’s do some overview of the pros and cons of both methods.

High-level thoughts

Single state is nice at first glance. A single data class contains all fields, ensuring a consistent state. However, it requires additional effort under the hood: if there are several data sources for the state, then you need a synchronized section to update the state. Otherwise, you may end up with some concurrency-related issues. Also, this way may cause excessive updates or equality checks in the View layer, especially if we are talking about Jetpack Compose.

On the other hand, multiple states provide a more verbose external contract for ViewModel but there is no need for additional synchronization, each data field is independent by default, and Jetpack Compose can read state exactly where it’s needed.

The case

Let’s do a comparison on a small screen with 3 variables:

  • Title
  • Text from a text field
  • Loading state

When the screen opens, 2 data sources are loaded: title and text. During loading, the loading state must be true. Additionally, the text can be changed based on user input.

Code comparison

💡 Note: I’ll try to keep this as simple as possible. For this reason I won’t use redux-like methods. There are also other simplifications, such as no error handling. Please let me know in the comments if you think anything could be improved in this comparison.

💡 Note: I’ll skip the View layer to keep this article short.

One thing that will be constant for both cases is repository. With some simplifications, it will be like this:

class SomeRepository {

    suspend fun getTitle(): String {
        delay(500)
        return "Some Title"
    }

    suspend fun getSavedFieldText(): String {
        delay(1000)
        return "Saved Field Text"
    }
}

It’s just a stub that emulates some data loading. Now let’s move to the interesting part.

Single State

Screen state object:

data class ScreenState(
    val title: String,
    val text: String,
    val isLoading: Boolean,
) {
    companion object {
        fun default() = ScreenState(
            title = "",
            text = "",
            isLoading = false,
        )
    }
}

ViewModel:

💡 Note: I write the state as Flow because ViewModel should not deal with View layer entities like State from Compose.

class SomeViewModel(
    private val someRepository: SomeRepository,
) : ViewModel() {

    private val _screenState = MutableStateFlow(ScreenState.default())
    val screenState: StateFlow<ScreenState> = _screenState

    // Note: no more public fields here!

    private val _titleIsLoading = MutableStateFlow(false)
    private val _textIsLoading = MutableStateFlow(false)

    private val stateLock = Mutex()

    init {
        observeLoadingState()
        loadTitle()
        loadSavedFieldText()
    }

    fun onTextChanged(newValue: String) {
        viewModelScope.launch {
            updateState {
                copy(text = newValue)
            }
        }
    }

    private fun loadTitle() {
        viewModelScope.launch {
            _titleIsLoading.value = true
            val title = someRepository.getTitle()
            _titleIsLoading.value = false
            updateState {
                copy(title = title)
            }
        }
    }

    private fun loadSavedFieldText() {
        viewModelScope.launch {
            _textIsLoading.value = true
            val text = someRepository.getSavedFieldText()
            _textIsLoading.value = false
            updateState {
                copy(text = text)
            }
        }
    }

    private fun observeLoadingState() {
        viewModelScope.launch {
            _titleIsLoading
                .combine(_textIsLoading) { titleIsLoading, textIsLoading ->
                    titleIsLoading || textIsLoading
                }
                .collect { isLoading ->
                    updateState {
                        copy(isLoading = isLoading)
                    }
                }
        }
    }

    private suspend fun updateState(updater: ScreenState.() -> ScreenState) {
        stateLock.withLock {
            _screenState.value = _screenState.value.updater()
        }
    }
}

OUR VIDEO RECOMMENDATION

Jobs

No results found.

Pros:

  • You can always find the state of the whole screen in one place.
  • Easy to write tests for the state.

Cons:

  • Additional code to keep the state synchronized.
  • Excessive copying of all fields when only one field was updated.
Multiple States

Screen state object:

Not this time. Fields are inside ViewModel.

ViewModel:

class SomeViewModel(
    private val someRepository: SomeRepository,
) : ViewModel() {

    private val _title = MutableStateFlow("")
    val title: StateFlow<String> = _title

    private val _text = MutableStateFlow("")
    val text: StateFlow<String> = _text

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    private val _titleIsLoading = MutableStateFlow(false)
    private val _textIsLoading = MutableStateFlow(false)

    init {
        observeLoadingState()
        loadTitle()
        loadSavedFieldText()
    }

    fun onTextChanged(newValue: String) {
        _text.value = newValue
    }

    private fun loadTitle() {
        viewModelScope.launch {
            _titleIsLoading.value = true
            val title = someRepository.getTitle()
            _titleIsLoading.value = false
            _title.value = title
        }
    }

    private fun loadSavedFieldText() {
        viewModelScope.launch {
            _textIsLoading.value = true
            val text = someRepository.getSavedFieldText()
            _textIsLoading.value = false
            _text.value = text
        }
    }

    private fun observeLoadingState() {
        viewModelScope.launch {
            _titleIsLoading
                .combine(_textIsLoading) { titleIsLoading, textIsLoading ->
                    titleIsLoading || textIsLoading
                }
                .collect { isLoading ->
                    _isLoading.value = isLoading
                }
        }
    }

    // Note: no state sync and copying here!
}

Pros:

  • Each field is independent from each other by default. Low coupling.
  • No need in excessive sync sections and fields copying.
  • Optimised for Jetpack Compose: it will read .value exactly where it’s needed, not on the highest possible level of composition.

Cons:

  • External contract of ViewModel becomes a bit more verbose.
  • Unit tests become a bit less concise.
Conclusion

Both methods have pros and cons and both methods work. One makes the external contract more concise, the other reduces the coupling within the ViewModel.

I’ve worked with both approaches, and it’s important to know that, as always, there are trade-offs.

Please share your thoughts and your cases in the comments.

Thanks for reading!

This blog is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu