Skip to content

Android Jetpack Components Explained: LiveData, ViewModel, and Navigation for Beginners

Android Jetpack Components

I was building an Android app that displayed a list of articles. Every time I rotated my phone, the app crashed. The data I had loaded from the network just… vanished. I spent hours trying to fix it with onSaveInstanceState, bundles, and manual state restoration. It was a mess.

Then I discovered Jetpack’s architecture components. Three months later, I’ve rewritten the entire app using LiveData, ViewModel, and Navigation. No more crashes on rotation. No more memory leaks. And navigation code that I can actually read.

The Three Problems That Drove Me Crazy

Before Jetpack, I dealt with these issues on every project:

The Old Way Problems
┌─────────────────────────────────────────────────────────────┐
│ PROBLEM 1: Screen Rotation = Data Loss │
│ ────────────────────────────────────────────────────────────│
│ Rotate phone → Activity destroyed → Activity recreated │
│ → Network data gone → User sees blank screen → Crash? │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PROBLEM 2: Memory Leaks from Async Callbacks │
│ ────────────────────────────────────────────────────────────│
│ Activity starts network request → User exits Activity │
│ → Callback fires → Tries to update destroyed Activity │
│ → Memory leak or crash │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PROBLEM 3: Fragment Navigation Spaghetti │
│ ────────────────────────────────────────────────────────────│
│ FragmentTransaction.add().replace().addToBackStack() │
│ → 50 lines of boilerplate → Deep links nightmare │
│ → Back stack bugs everywhere │
└─────────────────────────────────────────────────────────────┘

I tried various solutions. EventBus for data propagation (memory leaks). Static variables (more memory leaks). onRetainNonConfigurationInstance (deprecated). Nothing worked cleanly.

ViewModel: The Survivor

ViewModel was the first Jetpack component that made me say “oh, that’s how it should work.”

The key insight: ViewModel survives configuration changes. When you rotate your phone, the Activity is destroyed and recreated. But the ViewModel stays alive.

ArticleViewModel.kt
class ArticleViewModel(
private val repository: ArticleRepository
) : ViewModel() {
// Backing property - mutable internally
private val _articles = MutableLiveData<List<Article>>()
// Public property - immutable externally
// Other classes can only observe, not modify
val articles: LiveData<List<Article>> = _articles
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun loadArticles(topicId: String) {
viewModelScope.launch {
_loading.value = true
_articles.value = repository.fetchArticles(topicId)
_loading.value = false
}
}
}

Notice the pattern: _articles is private and mutable, while articles is public and immutable. This prevents other classes from accidentally modifying your data. I learned this the hard way after a junior developer directly modified my LiveData and caused weird UI bugs.

ViewModel Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ │
│ Activity A created │
│ │ │
│ ▼ │
│ ViewModel created ─────────────────┐ │
│ │ │ │
│ ▼ │ │
│ Activity A started │ │
│ │ │ │
│ ▼ │ │
│ User rotates phone │ ViewModel lives on! │
│ │ │ │
│ ▼ │ │
│ Activity A destroyed │ │
│ │ │ │
│ ▼ │ │
│ Activity B created │ │
│ │ │ │
│ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ Activity B gets SAME ViewModel instance │
│ │ │
│ ▼ │
│ Data still there! No reload needed │
│ │
└──────────────────────────────────────────────────────────────┘

LiveData: The Lifecycle-Aware Observer

LiveData solves the second problem: updating UI safely. It only notifies active observers. When your Activity is stopped or destroyed, LiveData stops sending updates.

ArticleActivity.kt
class ArticleActivity : AppCompatActivity() {
// Lazy initialize ViewModel
private val viewModel: ArticleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_article)
// LiveData automatically handles lifecycle
// This observer ONLY receives updates when Activity is STARTED or RESUMED
viewModel.articles.observe(this) { articles ->
// This won't crash if Activity is destroyed
recyclerView.adapter = ArticleAdapter(articles)
}
viewModel.loading.observe(this) { isLoading ->
progressBar.isVisible = isLoading
}
// Trigger data load
viewModel.loadArticles("jetpack")
}
// No need to manually remove observers - LiveData handles it!
// No need to check if Activity is destroyed - LiveData handles it!
}

The magic is in observe(this). The this refers to the Activity’s lifecycle. LiveData knows when the Activity is active and only sends updates then.

LiveData vs EventBus
┌─────────────────────────────────────────────────────────────┐
│ EventBus (Old Way) │ LiveData (Jetpack) │
├──────────────────────────────────────┼─────────────────────┤
│ Fire events regardless of lifecycle │ Lifecycle-aware │
│ Manual unregistration needed │ Auto-cleanup │
│ Memory leaks common │ No memory leaks │
│ Crashes on destroyed Activities │ Safe by design │
│ Testing is painful │ Easy to test │
└─────────────────────────────────────────────────────────────┘

Navigation component was the final piece. I used to have 50+ lines of FragmentTransaction code just to navigate between screens. Now it’s one line.

First, define your navigation graph in XML:

res/navigation/nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.app.HomeFragment"
android:label="Home">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.app.DetailFragment"
android:label="Detail">
<argument
android:name="articleId"
app:type="string"/>
</fragment>
</navigation>

Then navigate with a single line:

HomeFragment.kt
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = Navigation.findNavController(view)
articleAdapter.setOnItemClickListener { article ->
// Single line instead of FragmentTransaction boilerplate
navController.navigate(
R.id.action_home_to_detail,
bundleOf("articleId" to article.id)
)
}
}
}
Navigation Flow Diagram
┌─────────────────────────────────────────────────────────────┐
│ nav_graph.xml │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ action_home_to_detail │
│ │ Home │─────────────────────────────┐ │
│ │ Fragment │ │ │
│ └─────────────┘ ▼ │
│ ┌─────────────┐ │
│ │ Detail │ │
│ │ Fragment │ │
│ └─────────────┘ │
│ ▲ │
│ │ │
│ Arguments passed: articleId (string) ──────┘ │
│ │
│ Animation: slide_in_right / slide_out_left │
│ │
└─────────────────────────────────────────────────────────────┘

How They Work Together: MVVM Architecture

These three components form the foundation of MVVM (Model-View-ViewModel) architecture on Android:

MVVM with Jetpack Components
┌────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ Model │ │ ViewModel │ │ View │ │
│ │ (Repo + │────────▶│ (LiveData │────────▶│ (Activity│ │
│ │ Data) │ │ + Logic) │ │ + XML) │ │
│ └─────────────┘ └─────────────┘ └──────────┘ │
│ ▲ │ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Navigation │◀─────────────────┘ │
│ │ │ Component │ │
│ │ └─────────────┘ │
│ │ │ │
│ └───────────────────────┘ │
│ (Navigate to detail) │
│ │
└────────────────────────────────────────────────────────────────┘

The View observes the ViewModel via LiveData. The ViewModel holds UI state and survives rotations. Navigation handles all screen transitions declaratively.

Common Mistakes I Made

Mistake 1: Exposing MutableLiveData

Wrong: Exposing mutable data
class BadViewModel : ViewModel() {
// DON'T DO THIS
val articles = MutableLiveData&lt;List&lt;Article&gt;&gt;()
// Now any class can modify this directly
}
Correct: Immutable exposure
class GoodViewModel : ViewModel() {
private val _articles = MutableLiveData&lt;List&lt;Article&gt;&gt;()
val articles: LiveData&lt;List&lt;Article&gt;&gt; = _articles
// Only the ViewModel can modify _articles
// Others can only observe articles
}

Mistake 2: Putting Everything in ViewModel

I initially stored network responses, database objects, and business logic in ViewModel. Wrong. ViewModel should only hold UI-related state.

ViewModel Responsibility
┌─────────────────────────────────────────────────────────────┐
│ ViewModel SHOULD hold: │ ViewModel should NOT: │
├──────────────────────────────────┼─────────────────────────┤
│ UI state (loading, error, etc.) │ Database objects │
│ Display data (formatted strings) │ Raw API responses │
│ Navigation events │ Business logic │
│ User input validation state │ Network configuration │
└─────────────────────────────────────────────────────────────┘

Mistake 3: Ignoring Navigation Back Stack

Navigation handles the back stack automatically. I wasted time manually managing it.

Wrong: Manual back stack management
// DON'T DO THIS
fragmentManager.popBackStack()
fragmentManager.beginTransaction()
.replace(R.id.container, DetailFragment())
.addToBackStack(null)
.commit()
Correct: Let Navigation handle it
// Just navigate - Navigation handles everything
navController.navigate(R.id.action_home_to_detail)
// Back button works automatically
// Deep links work automatically
// Animations work automatically

When to Use Each Component

ComponentUse WhenDon’t Use When
ViewModelUI data survives rotation, share data between fragmentsSimple static UI, no configuration change concerns
LiveDataNeed lifecycle-aware updates, observing data changesOne-time events (use SharedFlow/Channel instead)
NavigationComplex fragment flow, deep links, animationsSingle-activity app with one screen, simple Viewpager

What’s Next

Once you’re comfortable with these three, explore other Jetpack components:

  • Room: SQLite abstraction with LiveData support
  • Paging: Load large datasets incrementally
  • WorkManager: Background tasks that survive app restarts
  • DataBinding: Connect UI directly to ViewModel

I started with LiveData, ViewModel, and Navigation. Now I can’t imagine building Android apps without them. The code is cleaner, bugs are fewer, and I spend less time fighting the framework and more time building features.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments