Brifing

ViewModel and State Management in Compose UI

A new approach to solve ViewModel complexity in large screens

ProAndroidDev - Medium2 min

1Problems of Passing ViewModel Deep into UI

In Compose UI, passing ViewModel directly to components ties them to a specific ViewModel type, reducing reusability and making isolated testing difficult. It can also break previews as they don't replicate the real app runtime. The state hoisting approach recommends moving state up to the lowest common owner, exposing immutable state down, and sending events back up when UI wants changes.

2Managing Long Callback Lists

Large screens with separate callbacks for each user action can quickly bloat screen functions. For example, a HomeScreen composable might include callbacks for refresh, retry, source selection etc. Grouping all user actions into sealed classes/interfaces and handling them through a single ViewModel method effectively simplifies the UI API while keeping all possible actions explicit in one place.

3The Growing ViewModel Problem

While grouping callbacks simplifies the UI layer, it shifts complexity to ViewModel with large when statements. As screens grow, these when statements become unmanageable. Sometimes business logic, data mapping and validation responsibilities get mixed in ViewModels, leading to the 'God ViewModel' anti-pattern where ViewModels become dumping grounds for various unrelated responsibilities.

4Compose Philosophy and Logic Separation

Compose encourages breaking UI into small, reusable components. The same principle can be applied to screen logic - having focused controllers for each UI component's behavior instead of one massive ViewModel. This allows not just UI components but also their underlying logic to be composed together, following the same modular approach that makes Compose UI so powerful.

5Solution with Controller Pattern

The controller pattern defines separate behavior units for each UI component or screen section. For example, SourcesController handles source selection while ArticlesController manages article loading operations. Instead of one giant ViewModel, each component has its own focused controller managing specific behavior. Controllers communicate with the screen's main ViewModel but maintain their own focused responsibilities.

6Data Sharing with State Store

Data sharing between controllers can lead to inconsistencies and race conditions. A centralized StateStore serves as the single source of truth for screen state that all controllers interact with. The StateStore manages all state updates, ensuring screen consistency while allowing controllers to work independently. This architecture prevents duplicate requests and maintains data consistency across the screen.

7Example Application: News App

In a news app, separate controllers can manage different features like source selection (SourcesController), search operations (SearchController), etc. All controllers share state through a HomeStateStore. This architecture reduces ViewModel complexity in large screens while enabling isolated testing of each feature. The pattern scales well as new features can be added with their own controllers without bloating existing components.

8Benefits and Conclusions

This approach provides an effective solution for ViewModel complexity in large Compose screens. Separating UI components and their logic makes code more modular and testable. Having dedicated controllers for each feature enables parallel team work. The pattern scales well for complex screens while maintaining clean architecture. It's particularly valuable for large apps where multiple teams work on different screen sections simultaneously.

Original article: ProAndroidDev - Medium — Compose UI'da ViewModel ve State Yönetimi