← Back to list

Stop Building God ViewModels with “One Screen = One ViewModel”

Using the New Android ViewModel APIs for Lifecycle-Aware Compose Architecture

chanzmao in ProAndroidDev · 2026-05-24 15:53 · 8 claps · 3.7 min read
#jetpack-compose #kotlin #android #androiddev #android-app-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🏛️ · Architecture

Stop Building God ViewModels with “One Screen = One ViewModel”

Using the New Android ViewModel APIs for Lifecycle-Aware Compose Architecture

Image created by AI

Image created by AI

Why This Matters

In classic Android development, a ViewModel was usually scoped to an Activity, Fragment, or navigation destination.

But in modern Jetpack Compose apps, a single screen often contains:

  • dynamically appearing UI
  • nested composables
  • pagers
  • bottom sheets
  • temporary feature flows

In those cases, scoping everything to the entire screen becomes too broad.

Sometimes you want a ViewModel to live only as long as a specific composable exists.

This is exactly what the new ViewModel APIs solve.

The Problem with Traditional ViewModel Scoping

A normal viewModel() call uses the nearest ViewModelStoreOwner.

Usually that means:

Activity
 └─ NavBackStackEntry
 └─ Entire Screen

As a result:

  • ViewModels survive longer than necessary
  • temporary UI state remains in memory
  • pager pages may unintentionally share state
  • cleanup timing becomes unclear

This becomes especially problematic in highly dynamic Compose UIs.

[embed]ViewModel Scoping APIs | App architecture | Android Developers ViewModel lets you manage your UI's data in a lifecycle-aware fashion.developer.android.com

1. Scoping a ViewModel to a Specific Composable

The first API introduces a ViewModel lifecycle tied directly to a composable call site.

Example

@Composable
fun RememberViewModelStoreOwnerSample() {
    // Create a ViewModelStoreOwner scoped to this specific call site.
    // When this composable leaves the composition,
    // the associated ViewModelStore will be cleared.
    val scopedOwner = rememberViewModelStoreOwner() // *

    CompositionLocalProvider(LocalViewModelStoreOwner provides scopedOwner) {
        // This ViewModel is scoped to `scopedOwner`.
        // It will survive configuration changes but will be cleared when
        // the composable is removed from the UI tree.
        val viewModel = viewModel { TestViewModel("scoped_data") }

        // Use the ViewModel
    }
}

What Happens Internally

Screen
 └─ RememberViewModelStoreOwnerSample()
     └─ rememberViewModelStoreOwner()
         └─ LocalViewModelStoreOwner
             └─ TestViewModel

The important part is this:

Composable leaves composition
        ↓
ViewModelStore cleared
        ↓
ViewModel.onCleared()

This gives Compose something it historically lacked:

  • composable-level ViewModel ownership

Why This Is Powerful

This pattern is extremely useful for:

Instead of keeping state alive for the whole screen, the lifecycle now matches the UI itself.

Lifecycle Visualization

Time        ──────────────────────────────────────────▶

Screen      ██████████████████████████████████████████

Composable            ████████████████

ViewModel             ████████████████

The ViewModel only exists while the composable exists.

That alignment is the key improvement.

2. Independent ViewModels for Pagers, Tabs, and Lazy Lists

The second API becomes even more interesting for pagers.

Example

@Composable
fun RememberViewModelStoreProviderSample() {
    val storeProvider = rememberViewModelStoreProvider() // *
    val pages = listOf("Page 1", "Page 2", "Page 3")

    HorizontalPager(pageCount = pages.size) { page ->
        // Create a ViewModelStoreOwner for the specific page 
        // using the provider.
        val pageOwner = rememberViewModelStoreOwner(
            provider = storeProvider,
            key = page
        )

        CompositionLocalProvider(LocalViewModelStoreOwner provides pageOwner) {
            val pageViewModel = viewModel {
                TestViewModel(pages[page])
            }

            // Use pageViewModel
        }
    }
}

The Traditional Pager Problem

Without page-scoped ViewModels:

Pager
 ├─ Page 1
 ├─ Page 2
 └─ Page 3

All pages
 └─ Shared ViewModel

This can lead to:

  • mixed UI state
  • accidental data sharing
  • difficult restoration logic
  • unclear ownership

With rememberViewModelStoreProvider

Now each page gets its own lifecycle container.

Pager
 ├─ Page 1
 │   └─ ViewModelStoreOwner
 │       └─ ViewModel
 │
 ├─ Page 2
 │   └─ ViewModelStoreOwner
 │       └─ ViewModel
 │
 └─ Page 3
     └─ ViewModelStoreOwner
         └─ ViewModel

Each page becomes independently stateful.

Why This Is Important

This pattern works especially well for:

  • tab UIs
  • onboarding flows
  • media carousels
  • large lazy pagers
  • multi-step forms
  • independently cached content

Each page can now:

  • preserve its own state
  • clear independently
  • avoid cross-page contamination

Configuration Changes Still Work

An important detail:

These scoped ViewModels still survive configuration changes.

Rotation
   ↓
Composable recreated
   ↓
Same ViewModel restored

But once the composable permanently disappears:

Composable removed
   ↓
ViewModel cleared

This creates a very natural lifecycle model.

Why This Fits the Compose Philosophy

Jetpack Compose encourages UI-driven architecture.

Traditionally:

Lifecycle defines UI

Now:

UI defines lifecycle

That inversion is significant.

The lifecycle becomes a consequence of composition structure.

This is much closer to how Compose actually works.

Best Practices

Prefer Scoped Ownership for Temporary UI

Good candidates:

  • dialogs
  • sheets
  • pager pages
  • expandable panels
  • nested flows

Avoid using the screen-level owner for everything.

Keep Business Domains Isolated

Instead of:

One giant screen ViewModel

Prefer:

Screen - ViewModel
 ├─ Toolbar
 ├─ Pager
 |  ├─ Page - ViewModel
 |  └─ Page - ViewModel
 ├─ Detail
 └─ BottomSheet - ViewModel

Smaller lifecycles reduce unintended coupling.

Avoid Over-Scoping

Do not create unnecessary ViewModel scopes for tiny stateless UI pieces.

A scoped ViewModel should represent meaningful UI state ownership.

Final Thoughts

**rememberViewModelStoreOwner()and`rememberViewModelStoreProvider()`**introduce something Compose developers have needed for a long time:

composable-level lifecycle ownership

This enables:

  • more precise memory management
  • isolated state containers
  • cleaner architecture
  • better pager patterns
  • UI-aligned lifecycle handling

As Compose applications become more dynamic, these APIs will likely become foundational patterns for advanced state management.

[embed]Stop Passing ViewModels to Your Composables? Stop Passing ViewModels to Your Composables? Rethinking Screen Design in Jetpack Compose Navigation 3 If you're…proandroiddev.com

[embed]Scoping ViewModels in Compose Lifecycle ViewModel 2.11.0-alpha02 introduces rememberViewModelStoreOwner, an API to scope ViewModelStore directly…marcellogalhardo.dev

[embed]Beyond the Screen: Component-Level ViewModels in Compose Until Lifecycle 2.11, ViewModelStore scoping was tied to navigation destinations, activities, or fragments. There was…saurabharora.dev

Thanks for reading! 👏 Clap to support the article ⭐ Follow me for more Android and Jetpack Compose tips 🔔 Turn on email notifications so you never miss a post


메타데이터
post_id
569fe4cb3784
slug
stop-building-god-viewmodels-with-one-screen-one-viewmodel-569fe4cb3784
url
https://proandroiddev.com/stop-building-god-viewmodels-with-one-screen-one-viewmodel-569fe4cb3784
canonical_url
https://proandroiddev.com/stop-building-god-viewmodels-with-one-screen-one-viewmodel-569fe4cb3784
author_url
https://medium.com/@chanzmao
status
ok
fetched_at
2026-06-09 15:37:30