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

Managing State Beyond ViewModels and Hilt

Ralf
June 08, 2023

Managing State Beyond ViewModels and Hilt

Separation of concerns is a common best practice followed by all successful software projects. In Android applications, there is usually a UI layer, a data layer, and a domain layer. Given the infinite number of ways to implement these layers, it's not clear how to get started quickly. Therefore, many projects follow Google's high-level guide to app architecture, which suggests many reasonable defaults and best practices. But how do the proposed recommendations work in practice? How do you set up an architecture that is ready for your use cases with this guide in mind? Are suggestions such as using Hilt as the backbone for your architecture really a good recommendation?

This talk will discuss best practices for taking ownership of your own architecture, decoupling your business logic from Android components, and creating and managing scopes for your use cases. There will be concrete advice on how to loosely couple classes in the data and domain layers, how to prevent memory and thread leaks, and how to adopt the dependency injection framework of your choice.

Ralf

June 08, 2023
Tweet

More Decks by Ralf

Other Decks in Programming

Transcript

  1. Old architecture class Application class AbcViewModel class DefViewModel class GhiFragment

    class GhiViewModel class JklFragment class JklViewModel class AbcActivity class DefActivity
  2. Old architecture - Bottlenecks • Driven by Android and its

    window management and not by business logic • Different lifecycles with different teardown hooks • Custom lifecycles like user scope adjacent • Coroutine scopes provided by Android components
  3. Old architecture - Lifecycle Lifecycle managed by us Lifecycle managed

    by Android Business Logic UI rendering Activity Fragment ViewModel ViewModel flow: Flow operation() flow: Flow operation() Service objects
  4. Old architecture - Lifecycle class RouteViewModel : ViewModel() { fun

    sendRequest() { viewModelScope.launch { httpClient.post(request) } } }
  5. Old architecture - Lifecycle class RouteViewModel : ViewModel() { fun

    sendRequest() { viewModelScope.launch { httpClient.post(request) } } }
  6. Old architecture - Lifecycle class Application : Context { open

    fun onCreate() = Unit } class Activity : Context { open fun onCreate(..) = Unit open fun onDestroy() = Unit } class Fragment { open fun onCreate(..) = Unit open fun onDestroy() = Unit } class ViewModel { open fun onCleared() = Unit }
  7. Old architecture - Without unidirectional dataflow • Challenges synchronizing multiple

    screens • No holistic overview of the state of the application • Way too easy to mix business logic with UI code • Many services pushed into application scope
  8. Old architecture - Without unidirectional dataflow Lifecycle managed by us

    Lifecycle managed by Android Business Logic UI rendering Activity Fragment ViewModel ViewModel flow: Flow operation() flow: Flow operation() Service objects
  9. Old architecture - Without unidirectional dataflow ViewModel Activity operation1() operation2()

    operation3() flow1: Flow<Abc> flow2: Flow<Def> flow3: Flow<Ghj>
  10. Old architecture - Anti-pattern class AmazonApplication : Application() { @Inject

    lateinit var routingRepository: RoutingRepository @Inject lateinit var locationProvider: LocationProvider override fun onCreate() { super.onCreate() routingRepository.initialize() locationProvider.initialize() } }
  11. Old architecture - Anti-pattern class AmazonRoutingRepository @Inject constructor( @IODispatcher dispatcher:

    CoroutineDispatcher ) : RoutingRepository { private val coroutineScope = CoroutineScope(dispatcher) override fun initialize() { coroutineScope.launch { // Initialize } } override suspend fun loadRoute(): Route = ... }
  12. Old architecture - Anti-pattern class FakeRoutingRepository @Inject constructor() : RoutingRepository

    { override fun initialize() = Unit override suspend fun loadRoute(): Route = ... }
  13. Goals • Strongly decouple business logic from Android lifecycle •

    Features are device agnostic • Avoid thread and memory leaks
  14. Design principles - Modular design class AmazonApplication : Application() {

    @Inject lateinit var routingRepository: RoutingRepository @Inject lateinit var locationProvider: LocationProvider override fun onCreate() { super.onCreate() routingRepository.initialize() locationProvider.initialize() } }
  15. Design principles - Dependency inversion by default interface RoutingRepository {

    fun initialize() suspend fun loadRoute (): Route } class AmazonRoutingRepository @Inject constructor() : RoutingRepository { override fun initialize() = ... override suspend fun loadRoute(): Route = ... }
  16. Design principles - Dependency inversion by default interface RoutingRepository {

    fun initialize() suspend fun loadRoute (): Route } class AmazonRoutingRepository @Inject constructor() : RoutingRepository { override fun initialize() = ... override suspend fun loadRoute(): Route = ... }
  17. Design principles - Inject dependencies class AmazonRoutingRepository @Inject constructor( @IODispatcher

    dispatcher: CoroutineDispatcher ) : RoutingRepository { private val coroutineScope = CoroutineScope(dispatcher) override fun initialize() { coroutineScope.launch { // Initialize } } override suspend fun loadRoute(): Route = ... }
  18. Design principles - Inject dependencies class AmazonRoutingRepository @Inject constructor( @IODispatcher

    dispatcher: CoroutineDispatcher ) : RoutingRepository { private val coroutineScope = CoroutineScope(dispatcher) override fun initialize() { coroutineScope.launch { // Initialize } } override suspend fun loadRoute(): Route = ... }
  19. Design principles - Unidirectional dataflow to drive UI ViewModel Activity

    operation1() operation2() operation3() flow1: Flow<Abc> flow2: Flow<Def> flow3: Flow<Ghj>
  20. From Hilt to Anvil interface RoutingRepository { suspend fun loadRoute

    (): Route } @ContributesBinding(scope = AppScope::class) class AmazonRoutingRepository @Inject constructor() : RoutingRepository { override suspend fun loadRoute(): Route = ... }
  21. From Hilt to Anvil interface RoutingRepository { suspend fun loadRoute

    (): Route } @ContributesBinding(scope = AppScope::class) class AmazonRoutingRepository @Inject constructor() : RoutingRepository { override suspend fun loadRoute(): Route = ... }
  22. From Hilt to Anvil @Binds abstract fun bindRoutingRepository( repository: AmazonRoutingRepository

    ): RoutingRepository @ContributesBinding(AppScope::class) class AmazonRoutingRepository @Inject constructor( application: Application ) : RoutingRepository
  23. From Hilt to Anvil class AmazonApplication : Application() { lateinit

    var component: AppComponent override fun onCreate() { super.onCreate() component = DaggerAppComponent.factory().create(applicationContext = this) component.inject(this) } }
  24. From Hilt to Anvil class OpenSourceActivity : Activity() { private

    val viewModelFactory by viewModels<ViewModelFactory>() private val viewModel by viewModels<OpenSourceViewModel> { viewModelFactory } ... }
  25. From Hilt to Anvil class ViewModelFactory( application: Application ) :

    ViewModelProvider.Factory, AndroidViewModel(application) { private val viewModels: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> init { val component = application.component.createViewModelComponent() viewModels = component.viewModels() } override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return viewModels.getValue(modelClass).get() as T } override fun onCleared() { // Clean up any resources } }
  26. From Hilt to Anvil class ViewModelFactory( application: Application ) :

    ViewModelProvider.Factory, AndroidViewModel(application) { private val viewModels: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> init { val component = application.component.createViewModelComponent() viewModels = component.viewModels() } override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return viewModels.getValue(modelClass).get() as T } override fun onCleared() { // Clean up any resources } }
  27. From Hilt to Anvil class ViewModelFactory( application: Application ) :

    ViewModelProvider.Factory, AndroidViewModel(application) { private val viewModels: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> init { val component = application.component.createViewModelComponent() viewModels = component.viewModels() } override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return viewModels.getValue(modelClass).get() as T } override fun onCleared() { // Clean up any resources } }
  28. From Hilt to Anvil class ViewModelFactory( application: Application ) :

    ViewModelProvider.Factory, AndroidViewModel(application) { private val viewModels: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> init { val component = application.component.createViewModelComponent() viewModels = component.viewModels() } override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return viewModels.getValue(modelClass).get() as T } override fun onCleared() { // Clean up any resources } }
  29. Scope “Scopes define the boundary software components operate in. A

    scope is a space with a well-defined lifecycle that can be created and torn down. Scopes host other objects and can bind them to their lifecycle. Sub-scopes or child scopes have the same or a shorter lifecycle as their parent scope.”
  30. Scope “Scopes define the boundary software components operate in. A

    scope is a space with a well-defined lifecycle that can be created and torn down. Scopes host other objects and can bind them to their lifecycle. Sub-scopes or child scopes have the same or a shorter lifecycle as their parent scope.” – Ralf Wondratschek
  31. Scope • Android components as scope? • Coroutine scopes? •

    Dagger components as scope? • Implement your own?
  32. Scope interface Scope { val name: String val parent: Scope?

    fun buildChild(name: String, builder: (Builder.() -> Unit)? = null): Scope fun children(): Set<Scope> fun register(scoped: Scoped) fun isDestroyed(): Boolean fun destroy() fun <T : Any> getService(key: String): T? } https://github.com/square/mortar/blob/master/mortar/src/main/java/mortar/MortarScope.java
  33. Scope class Builder internal constructor( private val name: String, private

    val parent: Scope? ) { private val services = mutableMapOf<String, Any>() fun addService(key: String, service: Any) { services[key] = service } internal fun build(): Scope { return ScopeImpl(name, parent, services) } }
  34. Scope - Services interface Scope { fun <T : Any>

    getService(key: String): T? } inline fun <reified T : Any> Scope.daggerComponent(): T { return ... } fun Scope.Builder.addDaggerComponent(component: Any) { addService(DAGGER_COMPONENT_KEY, component) }
  35. Scope - Services interface Scope { fun <T : Any>

    getService(key: String): T? } inline fun <reified T : Any> Scope.daggerComponent(): T { return ... } fun Scope.Builder.addDaggerComponent(component: Any) { addService(DAGGER_COMPONENT_KEY, component) }
  36. Scope - Services private const val COROUTINE_SCOPE_KEY = "coroutineScope" fun

    Scope.coroutineScope(context: CoroutineContext? = null): CoroutineScope { val result = checkNotNull<CoroutineScopeScoped>(getService(COROUTINE_SCOPE_KEY)) { "Couldn't find CoroutineScopeScoped within scope $name." } check(result.isActive) { "Expected the coroutine scope ${result.coroutineContext[CoroutineName]?.name} " + "still to be active." } return result.createChild(context) } fun Scope.Builder.addCoroutineScope(coroutineScope: CoroutineScopeScoped) { addService(COROUTINE_SCOPE_KEY, coroutineScope) }
  37. Scope - Services private const val COROUTINE_SCOPE_KEY = "coroutineScope" fun

    Scope.coroutineScope(context: CoroutineContext? = null): CoroutineScope { val result = checkNotNull<CoroutineScopeScoped>(getService(COROUTINE_SCOPE_KEY)) { "Couldn't find CoroutineScopeScoped within scope $name." } check(result.isActive) { "Expected the coroutine scope ${result.coroutineContext[CoroutineName]?.name} " + "still to be active." } return result.createChild(context) } fun Scope.Builder.addCoroutineScope(coroutineScope: CoroutineScopeScoped) { addService(COROUTINE_SCOPE_KEY, coroutineScope) }
  38. Scope - Services private const val COROUTINE_SCOPE_KEY = "coroutineScope" fun

    Scope.coroutineScope(context: CoroutineContext? = null): CoroutineScope { val result = checkNotNull<CoroutineScopeScoped>(getService(COROUTINE_SCOPE_KEY)) { "Couldn't find CoroutineScopeScoped within scope $name." } check(result.isActive) { "Expected the coroutine scope ${result.coroutineContext[CoroutineName]?.name} " + "still to be active." } return result.createChild(context) } fun Scope.Builder.addCoroutineScope(coroutineScope: CoroutineScopeScoped) { addService(COROUTINE_SCOPE_KEY, coroutineScope) }
  39. Scope - Services rootScope = Scope.buildRootScope { addDaggerComponent(createDaggerComponent()) } application.rootScope

    .buildChild(name = this::class.java.simpleName) { addDaggerComponent( application.rootScope .daggerComponent<ActivityComponent.ParentComponent>() .createActivityComponent() ) }
  40. Scope - Callback interface Scope { fun register(scoped: Scoped) }

    interface Scoped { fun onEnterScope(scope: Scope) = Unit fun onExitScope() = Unit }
  41. Scope - Callback interface RoutingRepository { fun initialize() suspend fun

    loadRoute (): Route } class AmazonRoutingRepository : RoutingRepository { override fun initialize() = ... override suspend fun loadRoute(): Route = ... }
  42. Scope - Callback interface RoutingRepository { suspend fun loadRoute ():

    Route } class AmazonRoutingRepository : RoutingRepository, Scoped { override fun onEnterScope(scope: Scope) { scope.coroutineScope().launch { ... } } override suspend fun loadRoute(): Route = ... }
  43. Scope - Callback interface RoutingRepository { suspend fun loadRoute ():

    Route } @ContributesMultibindingScoped(AppScope::class) class AmazonRoutingRepository : RoutingRepository, Scoped { override fun onEnterScope(scope: Scope) { scope.coroutineScope().launch { ... } } override suspend fun loadRoute(): Route = ... }
  44. Scope - Callback @ContributesTo(AppScope::class) interface Component { @ForScope(AppScope::class) fun appScopedInstances():

    Set<Scoped> } rootScope = Scope.buildRootScope { addDaggerComponent(createDaggerComponent()) } rootScope.register(rootScope.daggerComponent<Component>().appScopedInstances())
  45. Scope - Callback @SingleIn(AppScope::class) @ContributesMultibindingScoped(AppScope::class) class ActivityListener @Inject constructor( private

    val application: Application ) : Scoped { val startedActivities: StateFlow<List<Activity>> = ... private val listener = object : Application.ActivityLifecycleCallbacks { ... } override fun onEnterScope(scope: Scope) { application.registerActivityLifecycleCallbacks(listener) } override fun onExitScope() { application.unregisterActivityLifecycleCallbacks(listener) } }
  46. Scope - Callback @SingleIn(AppScope::class) @ContributesMultibindingScoped(AppScope::class) class ActivityListener @Inject constructor( private

    val application: Application ) : Scoped { val startedActivities: StateFlow<List<Activity>> = ... private val listener = object : Application.ActivityLifecycleCallbacks { ... } override fun onEnterScope(scope: Scope) { application.registerActivityLifecycleCallbacks(listener) } override fun onExitScope() { application.unregisterActivityLifecycleCallbacks(listener) } }
  47. Scope - Callback @SingleIn(AppScope::class) @ContributesMultibindingScoped(AppScope::class) class ActivityListener @Inject constructor( private

    val application: Application ) : Scoped { val startedActivities: StateFlow<List<Activity>> = ... private val listener = object : Application.ActivityLifecycleCallbacks { ... } override fun onEnterScope(scope: Scope) { application.registerActivityLifecycleCallbacks(listener) } override fun onExitScope() { application.unregisterActivityLifecycleCallbacks(listener) } }
  48. Scope - Tests @Test fun `test routing repository registers for

    push updates`() { val repository = AmazonRoutingRepository() repository.onEnterScope(Scope.buildTestScope()) ... }
  49. Scope - Tests fun Scope.Companion.buildTestScope( name: String = "test", context:

    CoroutineContext? = null, builder: (Scope.Builder.() -> Unit)? = null ): Scope { var coroutineContext = SupervisorJob() + UnconfinedTestDispatcher() + CoroutineName(name) if (context != null) { coroutineContext += context } val coroutineScope = CoroutineScopeScoped(coroutineContext) val scope = buildRootScope(name = name) { addCoroutineScopeScoped(coroutineScope) builder?.invoke(this) } scope.register(coroutineScope) return scope }
  50. Scope - Recap class RouteViewModel : ViewModel() { fun sendRequest()

    { viewModelScope.launch { httpClient.post(request) } } }
  51. Scope - Recap class RouteViewModel : ViewModel() { fun sendRequest()

    { viewModelScope.launch { httpClient.post(request) } } }
  52. Scope - Recap class Application : Context { open fun

    onCreate() = Unit } class Activity : Context { open fun onCreate(..) = Unit open fun onDestroy() = Unit } class Fragment { open fun onCreate(..) = Unit open fun onDestroy() = Unit } class ViewModel { open fun onCleared() = Unit }
  53. Scope - Recap class Application : Context { open fun

    onCreate() = Unit } class Activity : Context { open fun onCreate(..) = Unit open fun onDestroy() = Unit } class Fragment { open fun onCreate(..) = Unit open fun onDestroy() = Unit } class ViewModel { open fun onCleared() = Unit }
  54. Scope - Recap class AmazonApplication : Application() { @Inject lateinit

    var routingRepository: RoutingRepository @Inject lateinit var locationProvider: LocationProvider override fun onCreate() { super.onCreate() routingRepository.initialize() locationProvider.initialize() } }
  55. Scope - Recap class AmazonApplication : Application() { @Inject lateinit

    var routingRepository: RoutingRepository @Inject lateinit var locationProvider: LocationProvider override fun onCreate() { super.onCreate() routingRepository.initialize() locationProvider.initialize() } }
  56. Scope - Recap class AmazonRoutingRepository @Inject constructor( @IODispatcher dispatcher: CoroutineDispatcher

    ) : RoutingRepository { private val coroutineScope = CoroutineScope(dispatcher) override fun initialize() { coroutineScope.launch { // Initialize } } override suspend fun loadRoute(): Route = ... }
  57. Scope - Recap class AmazonRoutingRepository @Inject constructor( @IODispatcher dispatcher: CoroutineDispatcher

    ) : RoutingRepository { private val coroutineScope = CoroutineScope(dispatcher) override fun initialize() { coroutineScope.launch { // Initialize } } override suspend fun loadRoute(): Route = ... }
  58. UI Engine Lifecycle managed by us Lifecycle managed by Android

    Business Logic UI rendering Activity Fragment ViewModel ViewModel flow: Flow operation() flow: Flow operation() Service objects
  59. UI Engine - Presenter Root presenter Onboarding presenter Delivery presenter

    Settings presenter Activity Login presenter Registration presenter
  60. UI Engine - Presenter Root presenter Onboarding presenter Delivery presenter

    Settings presenter Activity Login presenter Registration presenter
  61. UI Engine - Presenter Root presenter Onboarding presenter Delivery presenter

    Settings presenter Activity Login presenter Registration presenter
  62. UI Engine Lifecycle managed by us Lifecycle managed by Android

    Business Logic UI rendering Activity model events Service objects Presenter1 Presenter2 Root presenter
  63. UI Engine - Presenter interface MoleculePresenter<InputT : Any, ModelT :

    BaseModel> { @Composable fun present(input: InputT): ModelT } interface BaseModel https://github.com/cashapp/molecule
  64. UI Engine - Presenter interface Renderer<in ModelT : BaseModel> {

    fun render(model: ModelT) } abstract class ViewRenderer<T : BaseModel> : Renderer<T> { protected abstract fun inflate( activity: Activity, parent: ViewGroup, layoutInflater: LayoutInflater, initialModel: T ): View open fun onDetach() = Unit }
  65. UI Engine - Presenter example interface ItineraryListPresenter : MoleculePresenter<Unit, Model>

    { data class Model( val itinerary: List<ItineraryItem>, val onEvent: (Event) -> Unit, ) : BaseModel sealed interface Event { class OnSelectStop(val stop: Stop) : Event } }
  66. UI Engine - Presenter example interface ItineraryListPresenter : MoleculePresenter<Unit, Model>

    { data class Model( val itinerary: List<ItineraryItem>, val onEvent: (Event) -> Unit, ) : BaseModel sealed interface Event { class OnSelectStop(val stop: Stop) : Event } }
  67. UI Engine - Presenter example interface ItineraryListPresenter : MoleculePresenter<Unit, Model>

    { data class Model( val itinerary: List<ItineraryItem>, val onEvent: (Event) -> Unit, ) : BaseModel sealed interface Event { class OnSelectStop(val stop: Stop) : Event } }
  68. UI Engine - Presenter example @ContributesBinding(AppScope::class) class ItineraryListPresenterImpl @Inject constructor(

    private val itineraryRepository: ItineraryRepository ) : ItineraryListPresenter { @Composable override fun present(input: Unit): Model { val items by itineraryRepository.items.collectAsState() return Model( itinerary = items, onEvent = onEvent { when (it) { is Event.OnSelectStop -> { item -> itineraryRepository.onSelectStop(item.stop) } } } ) } }
  69. UI Engine - Presenter example @ContributesBinding(AppScope::class) class ItineraryListPresenterImpl @Inject constructor(

    private val itineraryRepository: ItineraryRepository ) : ItineraryListPresenter { @Composable override fun present(input: Unit): Model { val items by itineraryRepository.items.collectAsState() return Model( itinerary = items, onEvent = onEvent { when (it) { is Event.OnSelectStop -> { item -> itineraryRepository.onSelectStop(item.stop) } } } ) } }
  70. UI Engine - Presenter example @ContributesRenderer class ItineraryListRenderer : ViewBindingRenderer<ItineraryListPresenter.Model,

    FragmentItineraryListRedesignBinding>() { override fun inflateViewBinding( activity: Activity, parent: ViewGroup, layoutInflater: LayoutInflater, initialModel: ItineraryListPresenter.Model ): FragmentItineraryListRedesignBinding { val itineraryListLayoutBinding = FragmentItineraryListRedesignBinding.inflate(layoutInflater, parent, false) ... return itineraryListLayoutBinding } override fun renderModel(model: ItineraryListPresenter.Model) { ... } }
  71. UI Engine - Presenter example @ContributesRenderer class ItineraryListRenderer : ViewBindingRenderer<ItineraryListPresenter.Model,

    FragmentItineraryListRedesignBinding>() { override fun inflateViewBinding( activity: Activity, parent: ViewGroup, layoutInflater: LayoutInflater, initialModel: ItineraryListPresenter.Model ): FragmentItineraryListRedesignBinding { val itineraryListLayoutBinding = FragmentItineraryListRedesignBinding.inflate(layoutInflater, parent, false) ... return itineraryListLayoutBinding } override fun renderModel(model: ItineraryListPresenter.Model) { ... } }
  72. UI Engine - Presenter example @ContributesRenderer class ItineraryListRenderer : ViewBindingRenderer<ItineraryListPresenter.Model,

    FragmentItineraryListRedesignBinding>() { override fun inflateViewBinding( activity: Activity, parent: ViewGroup, layoutInflater: LayoutInflater, initialModel: ItineraryListPresenter.Model ): FragmentItineraryListRedesignBinding { val itineraryListLayoutBinding = FragmentItineraryListRedesignBinding.inflate(layoutInflater, parent, false) ... return itineraryListLayoutBinding } override fun renderModel(model: ItineraryListPresenter.Model) { ... } }
  73. UI Engine - Presenter example @Test fun `test presenter returns

    a model`() = runTest { val itineraryListPresenter = ItineraryListPresenterImpl(repository) itineraryListPresenter.test { val firstModel = awaitItem() assertThat(firstModel.itinerary).hasSize(3) firstModel.onEvent(ItineraryListPresenter.Event.OnSelectStop(stop1)) val secondModel = awaitItem() assertThat(secondModel.itinerary[1].isSelected()).isTrue() } } https://github.com/cashapp/turbine
  74. UI Engine - Presenter templates sealed interface Template : BaseModel

    { data class FullScreenTemplate( val model: BaseModel ) : Template data class ListDetailTemplate( val list: BaseModel, val detail: BaseModel, ) : Template } abstract class TemplateRenderer( ... ) : ViewRenderer<Template>()
  75. UI Engine - Presenter templates sealed interface Template : BaseModel

    { data class FullScreenTemplate( val model: BaseModel ) : Template data class ListDetailTemplate( val list: BaseModel, val detail: BaseModel, ) : Template } abstract class TemplateRenderer( ... ) : ViewRenderer<Template>()
  76. UI Engine - Presenter templates sealed interface Template : BaseModel

    { data class FullScreenTemplate( val model: BaseModel ) : Template data class ListDetailTemplate( val list: BaseModel, val detail: BaseModel, ) : Template } abstract class TemplateRenderer( ... ) : ViewRenderer<Template>()
  77. UI Engine - Presenter templates sealed interface Template : BaseModel

    { data class FullScreenTemplate( val model: BaseModel ) : Template data class ListDetailTemplate( val list: BaseModel, val detail: BaseModel, ) : Template } abstract class TemplateRenderer( ... ) : ViewRenderer<Template>()
  78. UI Engine - Presenter templates sealed interface Template : BaseModel

    { data class FullScreenTemplate( val model: BaseModel ) : Template data class ListDetailTemplate( val list: BaseModel, val detail: BaseModel, ) : Template } abstract class TemplateRenderer( ... ) : ViewRenderer<Template>()
  79. UI Engine - Presenter templates class TemplatePresenter @AssistedInject constructor( private

    val savedInstanceStateRegistry: SavedInstanceStateRegistry, @Assisted private val rootPresenter: MoleculePresenter<Unit, *>, ) : MoleculePresenter<Unit, Template> { @Composable override fun present(input: Unit): Template { return ReturningCompositionLocalProvider( LocalSavedInstanceStateRegistry provides savedInstanceStateRegistry, ) { rootPresenter.present(Unit).toTemplate() } } @AssistedFactory interface Factory { fun create(rootPresenter: MoleculePresenter<Unit, *>): TemplatePresenter } }
  80. UI Engine - Presenter templates class TemplatePresenter @AssistedInject constructor( private

    val savedInstanceStateRegistry: SavedInstanceStateRegistry, @Assisted private val rootPresenter: MoleculePresenter<Unit, *>, ) : MoleculePresenter<Unit, Template> { @Composable override fun present(input: Unit): Template { return ReturningCompositionLocalProvider( LocalSavedInstanceStateRegistry provides savedInstanceStateRegistry, ) { rootPresenter.present(Unit).toTemplate() } } @AssistedFactory interface Factory { fun create(rootPresenter: MoleculePresenter<Unit, *>): TemplatePresenter } }
  81. UI Engine - Presenter templates class TemplatePresenter @AssistedInject constructor( private

    val savedInstanceStateRegistry: SavedInstanceStateRegistry, @Assisted private val rootPresenter: MoleculePresenter<Unit, *>, ) : MoleculePresenter<Unit, Template> { @Composable override fun present(input: Unit): Template { return ReturningCompositionLocalProvider( LocalSavedInstanceStateRegistry provides savedInstanceStateRegistry, ) { rootPresenter.present(Unit).toTemplate() } } @AssistedFactory interface Factory { fun create(rootPresenter: MoleculePresenter<Unit, *>): TemplatePresenter } }
  82. UI Engine - Presenter template Child presenter Compute new model

    Template presenter Send model Combine models to template
  83. UI Engine - Presenter template Child presenter Compute new model

    Template presenter Send model Combine models to template Activity Send template
  84. UI Engine - Presenter template Child presenter Compute new model

    Template presenter Send model Combine models to template Activity Send template Renderer factory Send model from template
  85. UI Engine - Presenter template Child presenter Compute new model

    Template presenter Send model Combine models to template Activity Send template Renderer factory Send model from template Renderer Send model to right renderer
  86. UI Engine - Presenter template Child presenter Compute new model

    Template presenter Send model Combine models to template Activity Send template Renderer factory Send model from template Renderer Send model to right renderer Render model on screen
  87. UI Engine - Presenter template Child presenter Template presenter Activity

    Renderer factory Renderer Invoke callbacks from models Compute new model Render model on screen Send model Send template Combine models to template Send model from template Send model to right renderer
  88. UI Engine - Recap Lifecycle managed by us Lifecycle managed

    by Android Business Logic UI rendering Activity Fragment ViewModel ViewModel flow: Flow operation() flow: Flow operation() Service objects
  89. UI Engine - Recap Lifecycle managed by us Lifecycle managed

    by Android Business Logic UI rendering Activity model events Service objects Presenter1 Presenter2 Root presenter
  90. UI Engine - Recap Lifecycle managed by us Lifecycle managed

    by Android Business Logic UI rendering model events Service objects Presenter1 Presenter2 Root presenter model events Activity1 Activity2