|

How to service the data on screen orientation change in MVVM android

Sharing is caring!

Handling screen orientation changes is a classic challenge in Android, and the MVVM architecture with Android Jetpack provides an elegant and robust solution.

MVVM Core Principle: Separate UI from Data

The ViewModel is designed to survive configuration changes (like rotation). The UI (Activity/Fragment) is destroyed and recreated, but the same ViewModel instance is used for the new UI. This allows the ViewModel to hold the data and serve it to the new UI instance immediately.

Example of Android Jetpack Solution

Here’s how to implement it correctly using ViewModel, LiveData/StateFlow, and the ViewBindingproperty delegate.

1. The ViewModel (Holds the Data)

The ViewModel is the workhorse. It survives configuration changes and manages the data.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class MyViewModel(private val repository: DataRepository) : ViewModel() {

    // Using StateFlow (Modern approach)
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState

    // Using LiveData (Still a valid approach)
    // private val _data = MutableLiveData<String>()
    // val data: LiveData<String> = _data

    init {
        loadData()
    }

    fun loadData() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val result = repository.fetchData() // This is a suspend function
                _uiState.value = UiState.Success(result)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "An error occurred")
            }
        }
    }
}

// Sealed class to represent the UI State
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: String) : UiState()
    data class Error(val message: String) : UiState()
}

2. The Fragment (Observes the Data)

The Fragment is configuration-aware. It reconnects to the existing ViewModel and re-subscribes to the data flow when recreated.

Key: Use the by viewModels() property delegate to get the correct ViewModel instance.

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class MyFragment : Fragment(R.layout.fragment_my) {

    // The 'by viewModels()' Kotlin property delegate ensures the same
    // ViewModel instance is retained across configuration changes.
    private val viewModel: MyViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Collect the StateFlow from the ViewModel in a lifecycle-aware manner
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle: Safely collects the flow only when the view is at least STARTED.
            // It cancels the collection when the lifecycle falls below STARTED (e.g., STOPPED),
            // saving resources. This is the recommended practice.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    // Update the UI based on the state
                    when (state) {
                        is UiState.Loading -> {
                            showProgressBar()
                        }
                        is UiState.Success -> {
                            hideProgressBar()
                            updateUiWithData(state.data)
                        }
                        is UiState.Error -> {
                            hideProgressBar()
                            showError(state.message)
                        }
                    }
                }
            }
        }
    }

    private fun showProgressBar() { /* ... */ }
    private fun hideProgressBar() { /* ... */ }
    private fun updateUiWithData(data: String) { /* ... */ }
    private fun showError(message: String) { /* ... */ }
}

3. The Repository (Single Source of Truth)

The Repository handles data operations. It doesn’t care about the UI lifecycle.

class DataRepository {
    // This could be a network call, database query, etc.
    suspend fun fetchData(): String {
        // Simulate a network call
        kotlinx.coroutines.delay(2000)
        return "Fresh data from the server!"
    }
}

Please check the below diagram shows the seamless data flow during an orientation change, highlighting the ViewModel’s role as the persistent data holder.

This diagram illustrates the following key points:

  1. Initial Data Load: The portrait UI connects to the ViewModel and triggers a data load. The ViewModel fetches the data and updates its internal state (StateFlow/LiveData), which is then emitted back to the UI for display.

  2. Rotation Occurs: The portrait UI is destroyed due to the configuration change.

  3. ViewModel Persists: The ViewModel instance survives the destruction of the portrait UI.

  4. Automatic Data Restoration: The new landscape UI connects to the same ViewModel instance. The ViewModel immediately emits the last known state (the successfully fetched data), allowing the new UI to be populated instantly without making a new network request.

  5. Seamless Experience: The user sees no interruption; the data is simply redrawn in the new orientation.

Why This Works Perfectly

  1. ViewModel Survival: The ViewModel is stored in a special scope tied to the host (Activity/Fragment) but not the UI itself. It remains in memory during configuration changes.

  2. LiveData/StateFlow Awareness: These are observable holders. When the new Fragment subscribes (collect), it immediately receives the last emitted value, instantly repopulating the UI.

  3. No Duplicate Work: The network call or database query was already triggered by the first Fragment and is managed by the ViewModel’s viewModelScope. The new Fragment simply receives the result when it reconnects.

  4. Lifecycle Safety: Using repeatOnLifecycle(Lifecycle.State.STARTED) ensures the Flow collection doesn’t waste resources when the view is not on screen.

Additional Best Practices

  • Use SavedStateHandle for Process Death: While ViewModel survives configuration changes, it is destroyed if the Android system kills your app’s process to free up memory. To survive this, use SavedStateHandle inside your ViewModel to store minimal critical data (like a user ID).
class MyViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    init {
        val userId = savedStateHandle.get<String>("user_id") ?: ""
        // Load data based on saved user ID
    }
}
  • Avoid References to the View: Never store a reference to an Activity, Fragment, or View in the ViewModel. This creates a memory leak because the ViewModel outlives the UI components.

0 0 votes
Article Rating

Similar Posts

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments