The ViewModel Performance Trap: When State Management Bites Back
SinanKOZAK
46 views
54 slides
Oct 22, 2025
Slide 1 of 54
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
About This Presentation
ViewModels are a lifesaver—until they’re not. In growing codebases, they can become a subtle source of latency and instability. Logic inside a ViewModel can kick off too eagerly, drift across threads, or execute far too late—making it a hidden bottleneck in your app’s startup and rendering p...
ViewModels are a lifesaver—until they’re not. In growing codebases, they can become a subtle source of latency and instability. Logic inside a ViewModel can kick off too eagerly, drift across threads, or execute far too late—making it a hidden bottleneck in your app’s startup and rendering paths.
This talk dissects how misuse of ViewModel scope, threading, and initialization patterns can degrade performance. We’ll demonstrate how to detect these patterns using traces, logs, and custom metrics, and we’ll share sustainable strategies to keep ViewModels lean, responsive, and maintainable. Ideal for teams striving for long-term stability, responsiveness, and architectural clarity.
Size: 3.65 MB
Language: en
Added: Oct 22, 2025
Slides: 54 pages
Slide Content
Sinan Kozak
The ViewModel
Performance Trap
Sinan
Kozak
Staff Android Developer
Google Developer Expert
“I develop Android
for 13 years, I am still
not bored thanks to
performance issues”
3
Sinan Kozak
Google Developer Expert &
Staff Android Developer
13 years Android experience
4
We deliver
food over
70 countries
with a single rider application
The ViewModel in
the title is a
clickbait.
Similar performance
issues can be found
everywhere.
Green is good
Red is bad
What is
ViewModel
ViewModel
The ViewModel class is a business logic or screen level state holder.
It exposes state to the UI and encapsulates related business logic.
Its principal advantage is that it caches state and persists it through
configuration changes. This means that your UI doesn’t have to
fetch data again when navigating between activities, or following
configuration changes, such as when rotating the screen.
class FeatureViewModel @Inject constructor(
private val feedProvider: FeedProvider,
private val uiComponentMapper: UiComponentMapper,
private val feedPaginationSource: FeedPaginationSource,
private val featureDataContainer: FeatureDataContainer,
private val navigationBarFactory: NavigationBarFactory,
private val searchUseCase: SearchUseCase,
private val locationProvider: LocationProvider,
private val performanceTracker: PerformanceTracker,
private val featurePerformanceTracker : FeaturePerformanceTracker,
private val contentStateManager: ContentStateManager,
private val favoritesInteractor: FavoritesInteractor,
private val featureConfig: FeatureConfig,
private val analyticsTrackerFactory : AnalyticsTrackerFactory,
private val filtersInteractor: FiltersInteractor,
private val featureFiltersUseCase : FeatureFiltersUseCase,
private val authEventBus: AuthEventBus,
private val contentStartParamsProvider : ContentStartParamsProvider,
private val impressionProvider: ImpressionProvider,
private val adsUseCase: AdsUseCase,
private val uiAdComponentMapper: UiAdComponentMapper,
private val actionViewHelper: ActionViewHelper,
private val rewardsInteractor: RewardsInteractor,
private val migrationStatusProvider : MigrationStatusProvider,
private val eventManager: EventManager,
private val experimentDataStore: ExperimentDataStore,
private val categoryManager: CategoryManager,
private val specialOfferComponent : SpecialOfferComponent,
private val subscriptionApi: SubscriptionApi,
private val specialOfferManager: SpecialOfferManager,
…,
) : ViewModel(), FiltersInteractor by filtersInteractor {
}
Basic suggestions
does not scale
Every new feature accumulates the tech dept.
We don’t have best practice how to solve
increased complexity.
What if your
ViewModel is
slow to create?
class FeatureViewModel @Inject constructor(
private val feedProvider: FeedProvider,
private val uiComponentMapper: UiComponentMapper,
private val feedPaginationSource: FeedPaginationSource,
private val featureDataContainer: FeatureDataContainer,
private val navigationBarFactory: NavigationBarFactory,
private val searchUseCase: SearchUseCase,
private val locationProvider: LocationProvider,
private val performanceTracker: PerformanceTracker,
private val featurePerformanceTracker : FeaturePerformanceTracker,
private val contentStateManager: ContentStateManager,
private val favoritesInteractor: FavoritesInteractor,
private val featureConfig: FeatureConfig,
private val analyticsTrackerFactory : AnalyticsTrackerFactory,
private val filtersInteractor: FiltersInteractor,
private val featureFiltersUseCase : FeatureFiltersUseCase,
private val authEventBus: AuthEventBus,
private val contentStartParamsProvider : ContentStartParamsProvider,
private val impressionProvider: ImpressionProvider,
private val adsUseCase: AdsUseCase,
private val uiAdComponentMapper: UiAdComponentMapper,
private val actionViewHelper: ActionViewHelper,
private val rewardsInteractor: RewardsInteractor,
private val migrationStatusProvider : MigrationStatusProvider,
private val eventManager: EventManager,
private val experimentDataStore: ExperimentDataStore,
private val categoryManager: CategoryManager,
private val specialOfferComponent : SpecialOfferComponent,
private val subscriptionApi: SubscriptionApi,
private val specialOfferManager: SpecialOfferManager,
…,
) : ViewModel(), FiltersInteractor by filtersInteractor {
}
val homeViewModel =
lifecycleScope.async(Dispatchers.IO) {
viewModels<HomeViewModel>().value
}
homeViewModel.await()
You could end up
having multiple
VM instance
Multiple instance problem
When multiple view model requested
before finishing the first one,
ViewModelProvider will start creating new
ones. The ViewModelProvider is not thread
safe.
Suspend ViewModel
creation
We would like to be do other operations
until ViewModel ready, the suspend api
requires boilerplate code and error prone
if someone uses runBlocking.
Parallel VM injection?
Use Lazy
injection to
sweep it under
the rug
class HomeViewModel @Inject constructor(
private val feed: Lazy<HomeFeed>,
private val componentMapper: Lazy<HomeUiComponentMapper>,
private val homeFeedTokenPageSource : Lazy<HomeFeedTokenPageSource>,
private val homeDataContainer: Lazy<HomeDataContainer>,
private val navBarFactory: Lazy<HomeNavBar.UiModel.Factory>,
private val searchBarUseCase: Lazy<SearchBarUseCase>,
private val addressProvider: Lazy<AddressProvider>,
private val performanceTracker: Lazy<PerformanceTrackingManager>,
private val homePerformanceTracker : Lazy<HomePerformanceTracker>,
Use Lazy for
injection?
Find the root
cause of
slowness…
fun <K, T> K.letAndMethodTrace(name: String, block: ( K) /> T): T {
val bufferSize = 100 * 1024 * 1024
Debug.startMethodTracing(name, bufferSize)
// Fetch trace file from device and inspect in Perfetto or AndroidStudio
// onCreate - before any usage
val homeViewModel = viewModels<HomeViewModel>()
.letAndMethodTrace("HomeViewModel ${System.currentTimeMillis() }") {
it.value
}
Investigate big
chunks in
Main Thread
Don’t hesitate
Dig deeper
Single usage
We can make the parsers logic lazy
inside so we delay the init cost until it is
actually used.
Multiple usage
Consider caching the instance and
reuse. After caching, consider warming
up the expensive operation in
background thread before it is actually
needed
Lazy or Cache or Warm up
Using init block
class FooViewModel(
val repository: Repository,
) : ViewModel() {
val state: MutableStateFlow<String?> = …
init {
viewModelScope.launch {
val data = repository.getData()
state.value = data
}
}
}
getData
Value assignment
Main Thread operations
init can delay
ViewModel creation
Field assignments
init block
onCreate
scope.launch
class FooViewModel(
val repository: Repository,
) : ViewModel() {
val state: MutableStateFlow<String?> = …
init {
viewModelScope.launch {
val data = repository.fetchData()
state.value = data
}
}
}
Value assignment
Wait until
main thread
idle
“onStart,
onResume”
fetchData uses
IO internally
Lost time
ViewModel creation
Field assignments
init block
onCreate
scope.launch
First frame
class FooViewModel(
val repository: Repository,
) : ViewModel() {
val state: MutableStateFlow<String?> = …
init {
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
state.value = data
}
}
}
ViewModel creation
Field assignments
init block
onCreate
scope.launch
fetchData uses
IO internally
Value assigned
in IO without
waiting main
Main Thread
operations
Gain
First frame
viewModelScope
and lifecycleScope
use main.immediate
Dispatchers.main
It is single main thread. There is event
queue to run one by one. All coroutines
in main post a runnable to message
queue Dispatchers.main.immediate
Mostly same as main, but runs the
execution immediately on same thread if it
is already on the Main Thread.
main vs main.immediate
The default constructor of the ViewModel create a coroutine scope.
How to change
viewModelScope
CloseableCoroutineScope( /
coroutineContext = Dispatchers.Main.immediate + SupervisorJob() /
) /
public actual constructor (viewModelScope: CoroutineScope) { /
impl = ViewModelImpl(viewModelScope) /
} /
By default ViewModel will use Main.immediate.
Second constructor of ViewModel accepts CoroutineScope.
class FooViewModel(
val repository: Repository,
val coroutineScope: CoroutineScope,
) : ViewModel(coroutineScope) {
val state: MutableStateFlow<String?> = …
init {
viewModelScope.launch {
val data = repository.fetchData()
state.value = data
}
}
}
ViewModel creation
Field assignments
init block
onCreate
scope.launch
fetchData uses
IO internally
Value assigned
in IO without
waiting main
Main Thread
operations
Gain
First frame
.stateIn
There are WhileSubscribed,
Eager and Lazily options to
create a state flow.
Depending on our need of
cache and timing of the
operation, we can fine tune.
Cold flow
Start long operation in a cold
flow. Use onStart function of
flow to set the default state.
This will load the data on
demand with collection.
No need to call the VM in
lifecycle functions
During Navigation
Do not wait for ViewModel to
created/used, start your logic
in a repository during the
navigation early as possible.
The ViewModel can observe
the result.
Minimum dependency on
ViewModel and Lifecycle.
Dispatcher change?
When is better to do context change?
Only when it is necessary
at lower levels
Run in parallel
and keep the
control
Your features
just need a
state holder
ViewModel's core role is configuration change persistence. Complex
state/data logic can be delegated to a UiModel (or other Data
Holder/UseCase/Repository) that does not inherit from ViewModel. A
single VM can manage the lifecycle and delegate UiModel creation or
heavy initialization to a background thread safely.
ViewModel is a
Holder Pattern
class UiModels @Inject constructor(
private val homeUiModelFactory: HomeUiModel.Factory,
coroutineScope: CoroutineScope, // Dispatchers.IO
) : ViewModel(coroutineScope) {
val homeUiModel: StateFlow<HomeUiModel?> =
flow {
emit(value = homeUiModelFactory.create())
}
.stateIn(
scope = viewModelScope,
// Or Lazily if you want cold or Eagerly
started = WhileSubscribed(
stopTimeoutMillis = 5000
),
initialValue = null
)
}
class Home {
@Composable
fun Content(uiModels: UiModels) {
val homeUiModel =
uiModels
.homeUiModel
.collectAsStateWithLifecycle (null)
}
}
class HomeUiModel @Inject constructor(
// other expensive injections
private val coroutineScope: CoroutineScope,
// Dispatchers.IO
): ViewModel(coroutineScope) {
// UI logic
interface Factory {
fun create(): HomeUiModel
}
}
Performance is not just
about executing logic async;
it's about removing
unnecessary waits and
unexpected behavior.
Asynchronous Work
Use the recommended lazy
observation pattern (onStart +
stateIn). Carefully manage
context switching
(withContext) and understand
the immediate vs. queued
nature of Main Dispatchers.
Injection Speed
Ensure dependencies are fast
and side-effect free in the
constructor/init phase. Avoid
starting long-running
requests in the init block.
Pay Attention Timing
It is important where your work
starts (init vs. observation),
where it runs (Main vs.
Default/IO), and whether it is
observed when the job is done.
Performance Checklist
Thank you
Do you have any questions?
github.com/kozaxinan
linkedin.com/in/sinankozak
strava.com/athletes/sinankozak
@snnkzk