Slide 1

Slide 1 text

Composing ViewModels Breaking ViewModels into smaller self-contained UI models Micro Features ✨ Hakan Bagci hkn-bgc hknbgcdev

Slide 2

Slide 2 text

Rider Application 500K+ Monthly Active Riders 70+ Countries

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 Architecture Requirements Does our current architecture support these requirements?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Architecture Review Activity Fragment ViewModel Activity Fragment ViewModel Fragment ViewModel Fragment ViewModel ❌ Fragment ViewModel Fragment ViewModel Is it possible to reuse UI logic from the existing ViewModels?

Slide 8

Slide 8 text

Architecture Review 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

Slide 9

Slide 9 text

Paradigm Shift 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 ✨

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 vs UI Model 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

Slide 15

Slide 15 text

Jetpack ViewModel vs UI Model Are we getting rid of ViewModels? Idea is to use the powers of ViewModel while decreasing the platform dependency ViewModelScope Configuration Change Survival SavedStateHandle

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Blueprint of a Micro-feature

Slide 19

Slide 19 text

Sample micro-feature

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Micro-feature Sample - Api 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() }

Slide 22

Slide 22 text

Micro-feature Sample - Implementation 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 } }

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

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

Slide 25

Slide 25 text

Micro-feature Sample - Implementation class FooComposableFactoryImpl @Inject constructor( private val barNavigator: BarNavigator, ) : FooComposableFactory { override fun create(): FooComposable = { uiModel, showSnackbar .> Foo( uiModel = uiModel, onGoToBarClicked = barNavigator/:goToBar, showSnackbar = showSnackbar, ) } }

Slide 26

Slide 26 text

Micro-feature Sample - Implementation @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) } } ... }

Slide 27

Slide 27 text

Micro-feature Sample - Implementation @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 } } }

Slide 28

Slide 28 text

Micro-feature Sample - Implementation @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?

Slide 29

Slide 29 text

Hosts ● 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 Dashboard

Slide 30

Slide 30 text

Micro-feature Host Integration - UI Model 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) }

Slide 31

Slide 31 text

Micro-feature Host Integration - UI Model 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) }

Slide 32

Slide 32 text

UI Model Composition Tree HomeViewModel BottomSheetUiModel FooUiModel MapUiModel FloatingLayerUiModel BarUiModel BazUiModel

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Micro-feature Host Integration - Compose @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, ) } }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Host Configuration 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?

Slide 37

Slide 37 text

` Host Configuration 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, )

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Aspects of micro-features

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Testing Micro-features 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

Slide 44

Slide 44 text

What’s next? Focusing on developer experience Sharing a KMP micro-feature between Android and iOS Moving towards server driven UI

Slide 45

Slide 45 text

Thank you! hkn-bgc hknbgcdev