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. Managing State Beyond
    ViewModels and Hilt
    June 8th, 2023
    Ralf Wondratschek
    @vRallev | @[email protected]

    View Slide

  2. Background

    View Slide

  3. Background

    View Slide

  4. Background

    View Slide

  5. Background

    View Slide

  6. https://www.youtube.com/watch?v=xESX82kwjn4

    View Slide

  7. View Slide

  8. https://www.youtube.com/watch?v=0T_zvUEqsD4

    View Slide

  9. Old architecture
    class Application

    View Slide

  10. Old architecture
    class Application
    class AbcActivity class DefActivity

    View Slide

  11. Old architecture
    class Application
    class GhiFragment class JklFragment
    class AbcActivity class DefActivity

    View Slide

  12. Old architecture
    class Application
    class AbcViewModel class DefViewModel class GhiFragment
    class GhiViewModel
    class JklFragment
    class JklViewModel
    class AbcActivity class DefActivity

    View Slide

  13. Old architecture - Bottlenecks

    View Slide

  14. 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

    View Slide

  15. 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

    View Slide

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

    View Slide

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

    View Slide

  18. 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
    }

    View Slide

  19. Old architecture - Lifecycle
    interface Application.ActivityLifecycleCallbacks {
    ...
    }
    interface DefaultLifecycleObserver {
    ...
    }

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. Old architecture - Without unidirectional dataflow
    ViewModel
    Activity
    operation1()
    operation2()
    operation3()
    flow1: Flow
    flow2: Flow
    flow3: Flow

    View Slide

  23. 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()
    }
    }

    View Slide

  24. Old architecture - Anti-pattern
    interface RoutingRepository {
    fun initialize()
    suspend fun loadRoute (): Route
    }

    View Slide

  25. 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 = ...
    }

    View Slide

  26. Old architecture - Anti-pattern
    class FakeRoutingRepository @Inject constructor() : RoutingRepository {
    override fun initialize() = Unit
    override suspend fun loadRoute(): Route = ...
    }

    View Slide

  27. Old architecture - Hilt
    ● Strong coupling to Android components
    ● Custom components with limitations

    View Slide

  28. Old architecture - Hilt

    View Slide

  29. Goals

    View Slide

  30. Goals
    ● Strongly decouple business logic from Android lifecycle
    ● Features are device agnostic
    ● Avoid thread and memory leaks

    View Slide

  31. Design principles - Modular design

    View Slide

  32. 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()
    }
    }

    View Slide

  33. Design principles - Composition over inheritance

    View Slide

  34. Design principles - Composition over inheritance
    AppCompatActivity
    BaseActivity
    InfoActivity
    ViewModel
    BaseViewModel
    InfoViewModel

    View Slide

  35. Design principles - Dependency inversion by default

    View Slide

  36. 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 = ...
    }

    View Slide

  37. 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 = ...
    }

    View Slide

  38. Design principles - Inject dependencies

    View Slide

  39. 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 = ...
    }

    View Slide

  40. 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 = ...
    }

    View Slide

  41. Design principles - Unidirectional dataflow to drive UI

    View Slide

  42. Design principles - Unidirectional dataflow to drive UI
    ViewModel
    Activity
    operation1()
    operation2()
    operation3()
    flow1: Flow
    flow2: Flow
    flow3: Flow

    View Slide

  43. From Hilt to Anvil

    View Slide

  44. From Hilt to Anvil
    https://www.droidcon.com/2022/06/28/dagger-anvil-learning-to-love-dependency-injection/

    View Slide

  45. From Hilt to Anvil
    @Module
    @ContributesTo(AppScope::class)
    object RouteModule
    @Module
    @ContributesTo(AppScope::class, replaces = [RouteModule::class])
    object TestRouteModule

    View Slide

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

    View Slide

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

    View Slide

  48. From Hilt to Anvil
    @Module
    @InstallIn(ActivityComponent::class)
    @ContributesTo(ActivityScope::class)
    class RouteModule

    View Slide

  49. From Hilt to Anvil
    class AmazonRoutingRepository @Inject constructor(
    @ApplicationContext context: Context
    application: Application
    ) : RoutingRepository

    View Slide

  50. From Hilt to Anvil
    @Binds
    abstract fun bindRoutingRepository(
    repository: AmazonRoutingRepository
    ): RoutingRepository
    @ContributesBinding(AppScope::class)
    class AmazonRoutingRepository @Inject constructor(
    application: Application
    ) : RoutingRepository

    View Slide

  51. 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)
    }
    }

    View Slide

  52. From Hilt to Anvil
    class OpenSourceActivity : Activity() {
    private val viewModelFactory by viewModels()
    private val viewModel by viewModels { viewModelFactory }
    ...
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. From Hilt to Anvil
    @ContributesMultibinding(ViewModelScope::class)
    @ViewModelKey(OpenSourceViewModel::class)
    class OpenSourceViewModel @Inject constructor(
    ...
    ) : ViewModel() {
    ...
    }

    View Slide

  58. Scope

    View Slide

  59. 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.”

    View Slide

  60. 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

    View Slide

  61. Scope
    ● Android components as scope?
    ● Coroutine scopes?
    ● Dagger components as scope?
    ● Implement your own?

    View Slide

  62. Scope
    interface Scope {
    val name: String
    val parent: Scope?
    fun buildChild(name: String, builder: (Builder.() -> Unit)? = null): Scope
    fun children(): Set
    fun register(scoped: Scoped)
    fun isDestroyed(): Boolean
    fun destroy()
    fun getService(key: String): T?
    }
    https://github.com/square/mortar/blob/master/mortar/src/main/java/mortar/MortarScope.java

    View Slide

  63. Scope
    class Builder internal constructor(
    private val name: String,
    private val parent: Scope?
    ) {
    private val services = mutableMapOf()
    fun addService(key: String, service: Any) {
    services[key] = service
    }
    internal fun build(): Scope {
    return ScopeImpl(name, parent, services)
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  66. Scope - Services
    private const val COROUTINE_SCOPE_KEY = "coroutineScope"
    fun Scope.coroutineScope(context: CoroutineContext? = null): CoroutineScope {
    val result = checkNotNull(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)
    }

    View Slide

  67. Scope - Services
    private const val COROUTINE_SCOPE_KEY = "coroutineScope"
    fun Scope.coroutineScope(context: CoroutineContext? = null): CoroutineScope {
    val result = checkNotNull(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)
    }

    View Slide

  68. Scope - Services
    private const val COROUTINE_SCOPE_KEY = "coroutineScope"
    fun Scope.coroutineScope(context: CoroutineContext? = null): CoroutineScope {
    val result = checkNotNull(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)
    }

    View Slide

  69. Scope - Services
    rootScope = Scope.buildRootScope {
    addDaggerComponent(createDaggerComponent())
    }
    application.rootScope
    .buildChild(name = this::class.java.simpleName) {
    addDaggerComponent(
    application.rootScope
    .daggerComponent()
    .createActivityComponent()
    )
    }

    View Slide

  70. Scope - Hierarchy
    AppScope
    UserScope LoggedOutScope
    ActivityScope

    View Slide

  71. Scope - Hierarchy in reality
    AppScope
    UserScope ActivityScope ViewModelScope

    View Slide

  72. Scope - Callback
    interface Scope {
    fun register(scoped: Scoped)
    }

    View Slide

  73. Scope - Callback
    interface Scope {
    fun register(scoped: Scoped)
    }
    interface Scoped {
    fun onEnterScope(scope: Scope) = Unit
    fun onExitScope() = Unit
    }

    View Slide

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

    View Slide

  75. 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 = ...
    }

    View Slide

  76. 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 = ...
    }

    View Slide

  77. Scope - Callback
    @ContributesTo(AppScope::class)
    interface Component {
    @ForScope(AppScope::class)
    fun appScopedInstances(): Set
    }
    rootScope = Scope.buildRootScope {
    addDaggerComponent(createDaggerComponent())
    }
    rootScope.register(rootScope.daggerComponent().appScopedInstances())

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  81. Scope - Tests
    @Test
    fun `test routing repository registers for push updates`() {
    val repository = AmazonRoutingRepository()
    repository.onEnterScope(Scope.buildTestScope())
    ...
    }

    View Slide

  82. 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
    }

    View Slide

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

    View Slide

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

    View Slide

  85. 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
    }

    View Slide

  86. 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
    }

    View Slide

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

    View Slide

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

    View Slide

  89. Scope - Recap
    interface RoutingRepository {
    fun initialize()
    suspend fun loadRoute (): Route
    }

    View Slide

  90. Scope - Recap
    interface RoutingRepository {
    fun initialize()
    suspend fun loadRoute (): Route
    }

    View Slide

  91. 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 = ...
    }

    View Slide

  92. 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 = ...
    }

    View Slide

  93. Scope - Recap

    View Slide

  94. Scope - Recap

    View Slide

  95. UI Engine

    View Slide

  96. 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

    View Slide

  97. UI Engine
    ViewModel
    Activity
    operation1()
    operation2()
    operation3()
    flow1: Flow
    flow2: Flow
    flow3: Flow

    View Slide

  98. UI Engine
    https://www.droidcon.com/2022/09/29/navigation-and-dependency-injection-in-compose/

    View Slide

  99. UI Engine - Presenter
    Activity

    View Slide

  100. UI Engine - Presenter
    Root presenter
    Activity

    View Slide

  101. UI Engine - Presenter
    Root presenter
    Onboarding presenter
    Activity

    View Slide

  102. UI Engine - Presenter
    Root presenter
    Onboarding presenter
    Activity
    Login presenter Registration presenter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  108. UI Engine - Presenter
    interface MoleculePresenter {
    @Composable
    fun present(input: InputT): ModelT
    }
    interface BaseModel
    https://github.com/cashapp/molecule

    View Slide

  109. interface Renderer {
    fun render(model: ModelT)
    }
    UI Engine - Presenter

    View Slide

  110. UI Engine - Presenter
    interface Renderer {
    fun render(model: ModelT)
    }
    abstract class ViewRenderer : Renderer {
    protected abstract fun inflate(
    activity: Activity,
    parent: ViewGroup,
    layoutInflater: LayoutInflater,
    initialModel: T
    ): View
    open fun onDetach() = Unit
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  114. 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)
    }
    }
    }
    )
    }
    }

    View Slide

  115. 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)
    }
    }
    }
    )
    }
    }

    View Slide

  116. UI Engine - Presenter example
    @ContributesRenderer
    class ItineraryListRenderer :
    ViewBindingRenderer() {
    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) {
    ...
    }
    }

    View Slide

  117. UI Engine - Presenter example
    @ContributesRenderer
    class ItineraryListRenderer :
    ViewBindingRenderer() {
    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) {
    ...
    }
    }

    View Slide

  118. UI Engine - Presenter example
    @ContributesRenderer
    class ItineraryListRenderer :
    ViewBindingRenderer() {
    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) {
    ...
    }
    }

    View Slide

  119. 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

    View Slide

  120. UI Engine - Presenter templates

    View Slide

  121. 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()

    View Slide

  122. 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()

    View Slide

  123. 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()

    View Slide

  124. 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()

    View Slide

  125. 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()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  129. UI Engine - Presenter template
    Child presenter

    View Slide

  130. UI Engine - Presenter template
    Child presenter
    Compute
    new model

    View Slide

  131. UI Engine - Presenter template
    Child presenter
    Compute
    new model
    Template presenter
    Send model

    View Slide

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

    View Slide

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

    View Slide

  134. 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

    View Slide

  135. 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

    View Slide

  136. 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

    View Slide

  137. 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

    View Slide

  138. UI Engine - Circuit
    https://www.youtube.com/watch?v=ZIr_uuN8FEw

    View Slide

  139. UI Engine - Recap
    ViewModel
    Activity
    operation1()
    operation2()
    operation3()
    flow1: Flow
    flow2: Flow
    flow3: Flow

    View Slide

  140. UI Engine - Recap
    ViewModel
    Activity
    operation1()
    operation2()
    operation3()
    flow1: Flow
    flow2: Flow
    flow3: Flow

    View Slide

  141. 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

    View Slide

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

    View Slide

  143. 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

    View Slide

  144. Managing State Beyond
    ViewModels and Hilt
    Ralf Wondratschek
    @vRallev | @[email protected]

    View Slide