$30 off During Our Annual Pro Sale. View Details »

Managing State Beyond ViewModels and Hilt (Updated)

Ralf
September 15, 2023

Managing State Beyond ViewModels and Hilt (Updated)

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

September 15, 2023
Tweet

More Decks by Ralf

Other Decks in Programming

Transcript

  1. Managing State Beyond
    ViewModels and Hilt
    September 15th, 2023
    Ralf Wondratschek
    @vRallev | @[email protected] | @ralf_dev

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

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

    View Slide

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

    View Slide

  29. Old architecture - Hilt

    View Slide

  30. Goals

    View Slide

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

    View Slide

  32. Design principles - Modular design

    View Slide

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

  34. Design principles - Composition over inheritance

    View Slide

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

    View Slide

  36. Design principles - Dependency inversion by default

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

  39. Design principles - Inject dependencies

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

  42. Design principles - Unidirectional dataflow to drive UI

    View Slide

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

    View Slide

  44. From Hilt to Anvil

    View Slide

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

    View Slide

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

    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
    interface RoutingRepository {
    suspend fun loadRoute (): Route
    }
    @ContributesBinding(scope = AppScope::class)
    class AmazonRoutingRepository @Inject constructor() : RoutingRepository {
    override suspend fun loadRoute(): Route = ...
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

    View Slide

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

    View Slide

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

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

    View Slide

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

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

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

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

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

    View Slide

  71. Scope - Hierarchy
    AppScope

    View Slide

  72. Scope - Hierarchy
    AppScope
    UserScope LoggedOutScope
    ActivityScope

    View Slide

  73. Scope - Hierarchy in reality
    AppScope
    UserScope ActivityScope ViewModelScope

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

  86. Scope - Callback
    @SingleIn(AppScope::class)
    @ContributesMultibindingScoped(AppScope::class)
    class QeCommandReceiver @Inject constructor(
    private val application: Application,
    ) : BroadcastReceiver(), Scoped {
    override fun onEnterScope(scope: Scope) {
    application.registerReceiver(this, IntentFilter("..."))
    }
    override fun onExitScope() {
    application.unregisterReceiver(this)
    }
    override fun onReceive(context: Context, intent: Intent) {
    when (intent.getStringExtra("cmd")) {
    ...
    }
    }
    }

    View Slide

  87. Scope - Callback
    @SingleIn(AppScope::class)
    @ContributesMultibindingScoped(AppScope::class)
    class QeCommandReceiver @Inject constructor(
    private val application: Application,
    ) : BroadcastReceiver(), Scoped {
    override fun onEnterScope(scope: Scope) {
    application.registerReceiver(this, IntentFilter("..."))
    }
    override fun onExitScope() {
    application.unregisterReceiver(this)
    }
    override fun onReceive(context: Context, intent: Intent) {
    when (intent.getStringExtra("cmd")) {
    ...
    }
    }
    }

    View Slide

  88. Scope - Callback
    @SingleIn(AppScope::class)
    @ContributesMultibindingScoped(AppScope::class)
    class QeCommandReceiver @Inject constructor(
    private val application: Application,
    ) : BroadcastReceiver(), Scoped {
    override fun onEnterScope(scope: Scope) {
    application.registerReceiver(this, IntentFilter("..."))
    }
    override fun onExitScope() {
    application.unregisterReceiver(this)
    }
    override fun onReceive(context: Context, intent: Intent) {
    when (intent.getStringExtra("cmd")) {
    ...
    }
    }
    }

    View Slide

  89. Scope - Callback
    @SingleIn(AppScope::class)
    @ContributesMultibindingScoped(AppScope::class)
    class QeCommandReceiver @Inject constructor(
    private val application: Application,
    ) : BroadcastReceiver(), Scoped {
    override fun onEnterScope(scope: Scope) {
    application.registerReceiver(this, IntentFilter("..."))
    }
    override fun onExitScope() {
    application.unregisterReceiver(this)
    }
    override fun onReceive(context: Context, intent: Intent) {
    when (intent.getStringExtra("cmd")) {
    ...
    }
    }
    }

    View Slide

  90. Scope - Constructor?
    If a class part of DriverSessionScope implements Scoped and injects DriverSessionService (which is
    app-scoped), then we end up in a livelock and the app freezes. There is nothing wrong with this setup and it should be
    supported.
    The chain of events is as follows: the app scope gets created and someone injects DriverSessionService. Dagger
    attempts to create DriverSessionService. In the constructor the service immediately creates the
    DriverSessionScope, when the driver is already logged in. As part of this routine the service injects all Scoped
    instances contributed to the DriverSessionScope. If one of these instances injects DriverSessionService (again,
    from the app scope and that's valid), then Dagger cannot provide the DriverSessionService, because the previous
    instance hasn't been created yet and is still in the constructor call. Therefore, Dagger creates another
    DriverSessionService and the whole loop repeats.
    TL;DR: Don't run any logic in the constructor.
    The solution is implementing Scoped and moving all logic from the constructor call into onEnterScope().

    View Slide

  91. Scope - Constructor?
    If a class part of DriverSessionScope implements Scoped and injects DriverSessionService (which is
    app-scoped), then we end up in a livelock and the app freezes. There is nothing wrong with this setup and it should be
    supported.
    The chain of events is as follows: the app scope gets created and someone injects DriverSessionService. Dagger
    attempts to create DriverSessionService. In the constructor the service immediately creates the
    DriverSessionScope, when the driver is already logged in. As part of this routine the service injects all Scoped
    instances contributed to the DriverSessionScope. If one of these instances injects DriverSessionService (again,
    from the app scope and that's valid), then Dagger cannot provide the DriverSessionService, because the previous
    instance hasn't been created yet and is still in the constructor call. Therefore, Dagger creates another
    DriverSessionService and the whole loop repeats.
    TL;DR: Don't run any logic in the constructor.
    The solution is implementing Scoped and moving all logic from the constructor call into onEnterScope().

    View Slide

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

    View Slide

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

  94. Scope - Recap

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

    View Slide

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

    View Slide

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

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

  105. Scope - Recap

    View Slide

  106. Scope - Recap

    View Slide

  107. UI Engine

    View Slide

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

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

    View Slide

  110. Previously on droidcon…

    View Slide

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

    View Slide

  112. UI Engine - Presenter
    Activity

    View Slide

  113. UI Engine - Presenter
    Root presenter
    Activity

    View Slide

  114. UI Engine - Presenter
    Root presenter
    Onboarding presenter
    Activity

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

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

  133. UI Engine - Presenter example navigation
    class UserScopePresenter @Inject constructor(
    private val loggedInPresenter: Provider,
    private val loggedOutPresenter: Provider,
    private val userSessionService: UserSessionService,
    ) : MoleculePresenter {
    @Composable
    override fun present(input: Unit): BaseModel {
    val session by userSessionService.sessionState.collectAsState()
    return if (session.isLogginIn) {
    val presenter = remember { loggedInPresenter.get() }
    presenter.present(userSessionService.scope)
    } else {
    val presenter = remember { loggedOutPresenter.get() }
    presenter.present(Unit)
    }
    }
    }

    View Slide

  134. UI Engine - Presenter example navigation
    class UserScopePresenter @Inject constructor(
    private val loggedInPresenter: Provider,
    private val loggedOutPresenter: Provider,
    private val userSessionService: UserSessionService,
    ) : MoleculePresenter {
    @Composable
    override fun present(input: Unit): BaseModel {
    val session by userSessionService.sessionState.collectAsState()
    return if (session.isLogginIn) {
    val presenter = remember { loggedInPresenter.get() }
    presenter.present(userSessionService.scope)
    } else {
    val presenter = remember { loggedOutPresenter.get() }
    presenter.present(Unit)
    }
    }
    }

    View Slide

  135. UI Engine - Presenter example navigation
    class UserScopePresenter @Inject constructor(
    private val loggedInPresenter: Provider,
    private val loggedOutPresenter: Provider,
    private val userSessionService: UserSessionService,
    ) : MoleculePresenter {
    @Composable
    override fun present(input: Unit): BaseModel {
    val session by userSessionService.sessionState.collectAsState()
    return if (session.isLogginIn) {
    val presenter = remember { loggedInPresenter.get() }
    presenter.present(userSessionService.scope)
    } else {
    val presenter = remember { loggedOutPresenter.get() }
    presenter.present(Unit)
    }
    }
    }

    View Slide

  136. UI Engine - Presenter example navigation
    class UserScopePresenter @Inject constructor(
    private val loggedInPresenter: Provider,
    private val loggedOutPresenter: Provider,
    private val userSessionService: UserSessionService,
    ) : MoleculePresenter {
    @Composable
    override fun present(input: Unit): BaseModel {
    val session by userSessionService.sessionState.collectAsState()
    return if (session.isLogginIn) {
    val presenter = remember { loggedInPresenter.get() }
    presenter.present(userSessionService.scope)
    } else {
    val presenter = remember { loggedOutPresenter.get() }
    presenter.present(Unit)
    }
    }
    }

    View Slide

  137. UI Engine - Presenter templates

    View Slide

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

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

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

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

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

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

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

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

  146. UI Engine - Presenter template
    Child presenter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

    View Slide