Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Composing ViewModels

Hakan Bagci
September 20, 2024
230

Composing ViewModels

In early times of Android development we used to have big activities. All view and business logic were being written in activities. Then fragments were introduced so that activities started to become leaner. Then several architectural patterns have started to appear in the Android scene: MVP, MVVM, MVI, etc. While all these were happening, ViewModel like classes started to get bigger and bigger. They have become the new big activities. However, we haven't often considered splitting view models into manageable self-contained granular ones.

Views are composable and they can be composed to build bigger views. What about introducing smaller ViewModels, namely UI models, and using them to build composable UI models/View Models?

In this talk, we will introduce the micro-feature architecture that leverages granular UI models. We'll demonstrate how this approach enables us to create leaner UI hosts by breaking down view models into smaller, self-contained logical UI models. Moreover, we'll explore the distinctions between a UI model and a Jetpack ViewModel.

We'll delve into the advantages offered by micro-feature architecture, particularly in terms of single responsibility, composability, reusability, and testability.

By the end of the talk, you'll have a comprehensive understanding of how adopting this architecture can significantly enhance your development process and empower you to build high-quality, scalable applications which has highly dynamic contextual screens.

Micro-feature architecture is used in several screens of 2 big apps in production which are now being used by 10M+ users. We will be also sharing our learnings about using this approach in these apps.

Hakan Bagci

September 20, 2024
Tweet

Transcript

  1. Composing ViewModels Breaking ViewModels into smaller self-contained UI models {

    } Hakan Bagci hkn-bgc hknbgcdev Micro-Feature Architecture
  2. • Keep loose coupling between features/squads/domains • Foster portability by

    enabling composition of UIs in any host • Enable showing UI components that are backed by different data sources • Enable showing dynamic/contextual content on a single screen Does our current architecture support these requirements? Architecture Requirements
  3. Activity Fragment ViewModel Activity Fragment ViewModel Fragment ViewModel Fragment ViewModel

    Fragment ViewModel Fragment ViewModel Architecture Review Is it possible to reuse UI logic from the existing ViewModels?
  4. Is it possible to use existing UI components? Tightly coupled

    with the host fragment RecyclerView complexity One UI state mapper for whole screen Can we reuse existing domain/data layer? Observable repositories Domain use cases Not written in Compose Architecture Review
  5. Create an isolated screen Implement UI components specific to that

    screen Create self-contained host-agnostic UI components Compose UIs using these components Micro Features Paradigm Shift
  6. Micro-features Factory APIs Composable UI Model • Consists of a

    UI Model and a Composable • Self-contained, implements its own Unidirectional Data Flow (UDF) • Provides APIs (factory methods) to create its UI model and composable • Host agnostic, needs a host to start functioning • Fosters composition of UI Models and Composables What is a UI Model? How is it different from a ViewModel?
  7. Jetpack ViewModel UI Model Populate and publish state to the

    UI React to events from UI Persist data through configuration changes Platform independent Have coroutine scope access Easier test setup Hosted by ViewModel Hosted by ViewModel Jetpack ViewModel vs UI Model KMP
  8. Are we getting rid of ViewModels? Idea is to use

    the powers of ViewModels while keeping our UI models platform independent and lightweight ViewModelScope Configuration Change Survival SavedStateHandle Jetpack ViewModel vs UI Model
  9. Jetpack ViewModel UI Model #1 UI Model #2 UI Model

    #3 Composing ViewModels ✨ ViewModelScope CoroutineScope CoroutineScope SavedStateHandle SavedStateRepository SavedStateRepository Composing ViewModels
  10. Handle Events Events Map Local Data Source Remote Data Source

    UI State Composable View UI Model Repository Unidirectional Data Flow
  11. interface FooUiModel { val uiState: StateFlow<FooUiState> val action: ActionHandler<FooAction> fun

    onGoToBarClicked() interface Factory { fun create( coroutineScope: CoroutineScope, ): FooUiModel } } Micro-feature Sample - Api
  12. sealed class FooUiState { object Unavailable : FooUiState() object Loading

    : FooUiState() data class Available( val title: String, val description: String, val buttonText: String, ) : FooUiState() } sealed class FooAction { object GoToBar : FooAction() data class ShowError( val message: String, ) : FooAction() } Micro-feature Sample - Api
  13. class FooUiModelImpl @AssistedInject constructor( private val getFooUiState: GetFooUiState, override val

    action: ActionHandler<FooAction>, @Assisted private val coroutineScope: CoroutineScope, ) : FooUiModel { ... @AssistedFactory interface Factory : FooUiModel.Factory { override fun create( coroutineScope: CoroutineScope, ): FooUiModelImpl } } Micro-feature Sample - Implementation
  14. Micro-feature Sample - Implementation class FooUiModelImpl @AssistedInject constructor(...) : FooUiModel

    { private val _uiState: MutableStateFlow<FooUiState> = MutableStateFlow(Unavailable) override val uiState: StateFlow<FooUiState> = _uiState.asStateFlow() init { coroutineScope.launch { getFooUiState().collect { _uiState.value = it } } } override fun onGoToBarClicked() { action.update(FooAction.GoToBar) } ... }
  15. interface FooComposableFactory { fun create(): FooComposable } typealias FooComposable =

    @Composable ( uiModel: FooUiModel, showSnackbar: (String) -> Unit, ) -> Unit Micro-feature Sample - Api
  16. class FooComposableFactoryImpl @Inject constructor( private val barNavigator: BarNavigator, ) :

    FooComposableFactory { override fun create(): FooComposable = { uiModel, showSnackbar -> Foo( uiModel = uiModel, onGoToBarClicked = barNavigator::goToBar, showSnackbar = showSnackbar, ) } } Micro-feature Sample - Implementation
  17. @Composable fun Foo( uiModel: FooUiModel, onGoToBarClicked: () -> Unit, showSnackbar:

    (String) -> Unit, modifier: Modifier = Modifier, ) { ActionHandlerDisposableEffect( actionHandler = uiModel.action, ) { action -> when (action) { FooAction.GoToBar -> onGoToBarClicked() is FooAction.ShowError -> showSnackbar(action.message) } } ... } Micro-feature Sample - Implementation
  18. @Composable fun Foo( ... ) { ... val uiState =

    uiModel.uiState.collectAsStateWithLifecycle() when (uiState) { is Available -> FooContent( uiState = uiState, onGoToBarClicked = uiModel::onGoToBarClicked, modifier = modifier, ) Unavailable -> Unit Loading -> { // Show loading state } } } Micro-feature Sample - Implementation
  19. @Composable fun FooContent( uiState: FooUiState.Available, onGoToBarClicked: () -> Unit, modifier:

    Modifier = Modifier, ) { Column { Text(text = uiState.title) Text(text = uiState.description) Button( text = uiState.buttonText, onClick = onGoToBarClicked, ) } } How can we display this micro-feature? Micro-feature Sample - Implementation
  20. • App scaffold is composed of multiple hosts • Each

    host uses micro-feature factory APIs to populate its content • Hosts, as containers, may define rules to layout micro-features Map Floating Layer Bottom Sheet Nest Hosts
  21. class HomeViewModel @Inject constructor( bottomSheetUiModelFactory: BottomSheetUiModel.Factory, mapUiModelFactory: MapUiModel.Factory, floatingLayerUiModelFactory: FloatingLayerUiModel.Factory,

    ) : ViewModel() { val bottomSheetUiModel = bottomSheetUiModelFactory .create(viewModelScope) val mapUiModel = mapUiModelFactory .create(viewModelScope) val floatingLayerUiModel = floatingLayerUiModelFactory .create(viewModelScope) } Micro-feature Host Integration - UI Model
  22. class BottomSheetUiModelImpl @AssistedInject constructor( fooUiModelFactory: FooUiModel.Factory, barUiModelFactory: BarUiModel.Factory, bazUiModelFactory: BazUiModel.Factory,

    @Assisted coroutineScope: CoroutineScope, ) : BottomSheetUiModel { override val fooUiModel = fooUiModelFactory .create(coroutineScope) override val barUiModel = barUiModelFactory .create(coroutineScope) override val bazUiModel = bazUiModelFactory .create(coroutineScope) } Micro-feature Host Integration - UI Model
  23. @Composable fun BottomSheetContent( uiModel: BottomSheetUiModel, fooComposableFactory: FooComposableFactory, barComposableFactory: BarComposableFactory, bazComposableFactory:

    BazComposableFactory, modifier: Modifier = Modifier, showSnackbar: (String) -> Unit, ) { ... } Micro-feature Host Integration - Compose
  24. @Composable fun BottomSheetContent(...) { val foo = remember { fooComposableFactory.create()

    } val bar = remember { barComposableFactory.create() } val baz = remember { bazComposableFactory.create() } Column(modifier) { foo( uiModel = uiModel.fooUiModel, showSnackbar = showSnackbar, ) bar( uiModel = uiModel.barUiModel, ) baz( uiModel = uiModel.bazUiModel, ) } } Micro-feature Host Integration - Compose
  25. BottomSheetUiModel FooUiModel BarUiModel BazUiModel class BottomSheetUiModelImpl { val fooUiModel: FooUiModel

    val barUiModel: BarUiModel val bazUiModel: BazUiModel } XUiModel YUiModel class BottomSheetUiModelImpl { val fooUiModel: FooUiModel val barUiModel: BarUiModel val bazUiModel: BazUiModel val xUiModel: XUiModel val yUiModel: YUiModel } Is this solution scalable? Host Configuration
  26. ` interface ItemUiModel { val isDisplayable: StateFlow<Boolean> } class BottomSheetUiModelImpl(

    private val getBottomSheetConfiguration: GetBottomSheetConfiguration, private val coroutineScope: CoroutineScope, ) { init { coroutineScope.launch { getBottomSheetConfiguration().collect(::invalidateItems) } } private fun invalidateItems(configuration: Configuration) { // unregister items from old configuration // register items from new configuration } } class Configuration( val items: List<ItemUiModel>, ) Host Configuration
  27. Configuration2 Configuration1 BottomSheetUiModel FooUiModel BarUiModel BazUiModel XUiModel YUiModel ⚡ Context

    Changed Configuration1 Configuration2 ⚡ Context Changed BarUiModel BazUiModel FooUiModel XUiModel YUiModel Host Configuration
  28. Home BottomSheet Foo Map FloatingLayer Bar Baz ⚡ Context Changed

    Nest Bar Baz X Y Portability (Plug-and-Play)
  29. Unit Testing Screenshot Testing Integration Testing E2E Micro-Feature UiModels Micro-Feature

    Composables Micro-Feature Composables Host Micro-Feature Micro-Feature Interactive Snapshot UI Logic UI Flows Scope Testing Micro-features
  30. Learnings Do not over split micro-features Avoid deep UI model

    hierarchies Avoid high coupling with hosts