Skip to Content
Course content

110: ViewModel and LiveData in Kotlin

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start building Android apps, it's tempting to treat your Activity or Fragment as the brain of your operation. You fetch some data, store it in a local variable, and update the UI. It works perfectly—until the user rotates their phone. Suddenly, the Activity is destroyed and recreated, your variables are wiped clean, and your app either flickers or crashes. I've seen countless developers try to fix this by stuffing everything into onSaveInstanceState, but that's a nightmare for anything more complex than a single string or integer.

The Rotation Wipeout and Memory Leaks

Imagine we're building a simple User Profile screen. In a naive implementation, you'd probably have a var userProfile: User? = null right inside your ProfileActivity. You trigger a network call in onCreate, and when the result comes back, you call textView.text = userProfile?.name. This feels intuitive, but it's fragile.

If the user rotates the screen while that network call is in flight, the original Activity instance is killed. When the network response finally arrives, the callback tries to update a UI element that no longer exists in the current window. You've just created a memory leak, and if you're unlucky, a NullPointerException. Even if you handle the crash, the new Activity instance starts from scratch, triggering the network call all over again. It's wasteful and creates a jarring experience for the user.

// The "Naive" Way - Don't do this for business logic
class ProfileActivity : AppCompatActivity() {
    private var userName: String? = null // This vanishes on rotation

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)
        
        fetchUserData { name ->
            this.userName = name
            findViewById<TextView>(R.id.nameText).text = name
        }
    }
}


Letting the ViewModel Outlive the Activity

This is where the ViewModel comes in. Think of the ViewModel as a data warehouse that sits just outside the Activity's lifecycle. When the Activity is destroyed during a configuration change, the ViewModel stays put in memory. When the new Activity instance spins up, it simply reconnects to the existing ViewModel.

But a ViewModel alone isn't enough. If the ViewModel just held a raw variable, the Activity would have to manually poll it or "ask" for the data every time it restarted. That's where LiveData fills the gap. LiveData is a data holder class that is "lifecycle-aware." It doesn't just hold the value; it allows the Activity to observe the value. The magic here is that LiveData knows when the Activity is active. If the Activity is in the background or destroyed, LiveData stops sending updates, preventing those nasty crashes we talked about.

// The Professional Way
class ProfileViewModel : ViewModel() {
    private val _userName = MutableLiveData<String>()
    val userName: LiveData<String> = _userName // Exposed as immutable

    fun loadUser() {
        // Simulate network call
        fetchUserData { name ->
            _userName.value = name
        }
    }
}

class ProfileActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)

        val viewModel = ViewModelProvider(this).get(ProfileViewModel::class.java)
        
        // We observe the data. This automatically handles rotation 
        // and prevents leaks because it knows the Activity's lifecycle.
        viewModel.userName.observe(this) { name ->
            findViewById<TextView>(R.id.nameText).text = name
        }

        viewModel.loadUser()
    }
}


The Trade-off: Boilerplate vs. Stability

I'll be honest with you: this approach requires more files and more code upfront. You're creating a separate class and wrapping your variables in MutableLiveData. It can feel like overkill for a tiny app. However, the trade-off is an app that feels "solid."

By separating the state (ViewModel) from the view (Activity), you've achieved a clean separation of concerns. Your ViewModel doesn't know anything about TextViews or Contexts, which makes it incredibly easy to unit test. You can test your business logic in the ViewModel using a standard JUnit test without needing to launch an Android emulator. In my experience, that's the real win—spending an extra ten minutes on the architecture now saves you ten hours of debugging weird lifecycle crashes later.




📋 Practical Task

Exercise: Migrating the "Live Score Tracker"

You have been handed a legacy piece of code for a sports app. Currently, the ScoreActivity fetches the current game score from a simulated API and stores it in a local variable. Every time the user rotates the screen, the app makes a fresh API call, causing the screen to flicker and the score to jump.

Your Task:

  • Create a ScoreViewModel class.
  • Move the currentScore: String variable into the ViewModel, wrapping it in a MutableLiveData object.
  • Implement a function in the ViewModel to update the score.
  • Modify the ScoreActivity to remove the local variable and instead observe the LiveData from the ViewModel.
  • Ensure that the API call (the score update) is only triggered once, not every time onCreate is called during a rotation.

Goal: The score should remain visible and stable on the screen even after multiple device rotations, without triggering redundant network requests.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.