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

Micro-Features: Composing ViewModels - London D...

Micro-Features: Composing ViewModels - London Droidcon 2023

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

October 27, 2023
Tweet

More Decks by Hakan Bagci

Other Decks in Programming

Transcript

  1. • 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?
  2. 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?
  3. 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
  4. 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 ✨
  5. 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?
  6. 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
  7. 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
  8. Composing ViewModels Jetpack ViewModel UI Model #1 UI Model #2

    UI Model #3 Composing ViewModels ✨ ViewModelScope CoroutineScope CoroutineScope SavedStateHandle SavedStateRepository SavedStateRepository
  9. Unidirectional Data Flow Handle Events Events Map Local Data Source

    Remote Data Source UI State Composable View UI Model Repository
  10. Micro-feature Sample - Api interface FooUiModel { val uiState: StateFlow<FooUiState>

    val action: ActionHandler<FooAction> fun onGoToBarClicked() interface Factory { fun create( coroutineScope: CoroutineScope, ): FooUiModel } }
  11. 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() }
  12. Micro-feature Sample - Implementation 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 } }
  13. 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) } //. }
  14. Micro-feature Sample - Api interface FooComposableFactory { fun create(): FooComposable

    } typealias FooComposable = @Composable ( uiModel: FooUiModel, showSnackbar: (String) /> Unit, ) /> Unit
  15. 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, ) } }
  16. 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) } } ... }
  17. 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 } } }
  18. 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?
  19. 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
  20. 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) }
  21. 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) }
  22. Micro-feature Host Integration - Compose @Composable fun BottomSheetContent( uiModel: BottomSheetUiModel,

    fooComposableFactory: FooComposableFactory, barComposableFactory: BarComposableFactory, bazComposableFactory: BazComposableFactory, modifier: Modifier = Modifier, showSnackbar: (String) /> Unit, ) { //. }
  23. 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, ) } }
  24. Composition Tree HomeViewModel BottomSheetUiModel FooUiModel MapUiModel FloatingLayerUiModel BarUiModel BazUiModel <<Composable>>

    BottomSheetContent <<Composable>> Foo <<Composable>> Bar <<Composable>> Baz HomeActivity
  25. 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?
  26. ` Host Configuration 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>, )
  27. Configuration2 Configuration1 Host Configuration BottomSheetUiModel FooUiModel BarUiModel BazUiModel XUiModel YUiModel

    ⚡ Context Changed Configuration1 Configuration2 ⚡ Context Changed BarUiModel BazUiModel FooUiModel XUiModel YUiModel
  28. Server Driven UI Micro-feature Micro-feature Micro-feature Configuration Host ⚡ Context

    Changed Repository Component Config Remote Layout Config
  29. 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
  30. What’s next? Focusing on developer experience Sharing a KMP micro-feature

    between Android and iOS Moving towards server driven UI