Slide 1

Slide 1 text

Composing ViewModels Breaking ViewModels into smaller self-contained UI models { } Hakan Bagci hkn-bgc hknbgcdev Micro-Feature Architecture

Slide 2

Slide 2 text

500K+ Monthly Active Riders 70+ Countries Rider Application

Slide 3

Slide 3 text

Rider Application - Before Bottom navigation with isolated features

Slide 4

Slide 4 text

Rider Application - New Design Single shared contextual screen hosting all features

Slide 5

Slide 5 text

● 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

Slide 6

Slide 6 text

Activity Activity Fragment Fragment Fragment Activity Fragment Fragment Fragment ViewModel ViewModel ViewModel Architecture Review

Slide 7

Slide 7 text

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?

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

What is a Micro-Feature?

Slide 13

Slide 13 text

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?

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Jetpack ViewModel UI Model #1 UI Model #2 UI Model #3 Composing ViewModels ✨ ViewModelScope CoroutineScope CoroutineScope SavedStateHandle SavedStateRepository SavedStateRepository Composing ViewModels

Slide 17

Slide 17 text

Handle Events Events Map Local Data Source Remote Data Source UI State Composable View UI Model Repository Unidirectional Data Flow

Slide 18

Slide 18 text

Blueprint of a Micro-feature

Slide 19

Slide 19 text

Sample Micro-Feature

Slide 20

Slide 20 text

interface FooUiModel { val uiState: StateFlow val action: ActionHandler fun onGoToBarClicked() interface Factory { fun create( coroutineScope: CoroutineScope, ): FooUiModel } } Micro-feature Sample - Api

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Micro-feature Sample - Implementation class FooUiModelImpl @AssistedInject constructor(...) : FooUiModel { private val _uiState: MutableStateFlow = MutableStateFlow(Unavailable) override val uiState: StateFlow = _uiState.asStateFlow() init { coroutineScope.launch { getFooUiState().collect { _uiState.value = it } } } override fun onGoToBarClicked() { action.update(FooAction.GoToBar) } ... }

Slide 24

Slide 24 text

interface FooComposableFactory { fun create(): FooComposable } typealias FooComposable = @Composable ( uiModel: FooUiModel, showSnackbar: (String) -> Unit, ) -> Unit Micro-feature Sample - Api

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

@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

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

● 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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

HomeViewModel BottomSheetUiModel FooUiModel MapUiModel FloatingLayerUiModel BarUiModel BazUiModel UI Model Composition Tree

Slide 33

Slide 33 text

@Composable fun BottomSheetContent( uiModel: BottomSheetUiModel, fooComposableFactory: FooComposableFactory, barComposableFactory: BarComposableFactory, bazComposableFactory: BazComposableFactory, modifier: Modifier = Modifier, showSnackbar: (String) -> Unit, ) { ... } Micro-feature Host Integration - Compose

Slide 34

Slide 34 text

@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

Slide 35

Slide 35 text

HomeViewModel BottomSheetUiModel FooUiModel MapUiModel FloatingLayerUiModel BarUiModel BazUiModel <> BottomSheetContent <> Foo <> Bar <> Baz HomeActivity Composition Tree

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

` interface ItemUiModel { val isDisplayable: StateFlow } 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, ) Host Configuration

Slide 38

Slide 38 text

Configuration2 Configuration1 BottomSheetUiModel FooUiModel BarUiModel BazUiModel XUiModel YUiModel ⚡ Context Changed Configuration1 Configuration2 ⚡ Context Changed BarUiModel BazUiModel FooUiModel XUiModel YUiModel Host Configuration

Slide 39

Slide 39 text

Aspects of Micro-Features

Slide 40

Slide 40 text

Home BottomSheet Foo Map FloatingLayer Bar Baz ⚡ Context Changed Nest Bar Baz X Y Portability (Plug-and-Play)

Slide 41

Slide 41 text

HomeViewModel BottomSheetUiModel FooUiModel MapUiModel FloatingLayerUiModel BarUiModel BazUiModel KMP KMP KMP KMP KMP KMP iOS Multiplatform - UI Model

Slide 42

Slide 42 text

Micro-Feature Micro-Feature Micro-Feature Configuration Host ⚡ Context Changed Repository Component Config Remote Layout Config Server Driven UI

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Learnings Do not over split micro-features Avoid deep UI model hierarchies Avoid high coupling with hosts

Slide 45

Slide 45 text

>75 Micro-Features 6 Hosts 7 Cross-Functional Teams ~30 Mobile Engineers

Slide 46

Slide 46 text

Thank You hkn-bgc hknbgcdev