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 full-size slide

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

    View full-size slide

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

    View full-size slide

  4. Old architecture
    class Application

    View full-size slide

  5. Old architecture
    class Application
    class AbcActivity class DefActivity

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. Old architecture - Bottlenecks

    View full-size slide

  9. 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 full-size slide

  10. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  13. 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 full-size slide

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

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

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

    View full-size slide

  18. 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 full-size slide

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

    View full-size slide

  20. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  23. Old architecture - Hilt

    View full-size slide

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

    View full-size slide

  25. Design principles - Modular design

    View full-size slide

  26. 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 full-size slide

  27. Design principles - Composition over inheritance

    View full-size slide

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

    View full-size slide

  29. Design principles - Dependency inversion by default

    View full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. Design principles - Inject dependencies

    View full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

  35. Design principles - Unidirectional dataflow to drive UI

    View full-size slide

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

    View full-size slide

  37. From Hilt to Anvil

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  45. 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 full-size slide

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

    View full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

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

    View full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

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

    View full-size slide

  55. 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 full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. 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 full-size slide

  60. 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 full-size slide

  61. 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 full-size slide

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

    View full-size slide

  63. Scope - Hierarchy
    AppScope
    UserScope LoggedOutScope
    ActivityScope

    View full-size slide

  64. Scope - Hierarchy in reality
    AppScope
    UserScope ActivityScope ViewModelScope

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

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

    View full-size slide

  71. 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 full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

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

    View full-size slide

  75. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

  80. 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 full-size slide

  81. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  84. 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 full-size slide

  85. 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 full-size slide

  86. Scope - Recap

    View full-size slide

  87. Scope - Recap

    View full-size slide

  88. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  91. UI Engine - Presenter
    Activity

    View full-size slide

  92. UI Engine - Presenter
    Root presenter
    Activity

    View full-size slide

  93. UI Engine - Presenter
    Root presenter
    Onboarding presenter
    Activity

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  102. 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 full-size slide

  103. 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 full-size slide

  104. 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 full-size slide

  105. 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 full-size slide

  106. 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 full-size slide

  107. 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 full-size slide

  108. 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 full-size slide

  109. 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 full-size slide

  110. 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 full-size slide

  111. 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 full-size slide

  112. UI Engine - Presenter templates

    View full-size slide

  113. 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 full-size slide

  114. 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 full-size slide

  115. 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 full-size slide

  116. 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 full-size slide

  117. 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 full-size slide

  118. 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 full-size slide

  119. 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 full-size slide

  120. 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 full-size slide

  121. UI Engine - Presenter template
    Child presenter

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  126. 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 full-size slide

  127. 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 full-size slide

  128. 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 full-size slide

  129. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  133. 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 full-size slide

  134. 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 full-size slide

  135. 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 full-size slide

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

    View full-size slide