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

Using Jetpack Compose with Square’s Molecule Library

Mohit S
February 16, 2022

Using Jetpack Compose with Square’s Molecule Library

Building StateFlows with Jetpack Compose.

Mohit S

February 16, 2022
Tweet

More Decks by Mohit S

Other Decks in Technology

Transcript

  1. Mohit Sarveiya
    Using Jetpack Compose with Square's Molecule Library

    @heyitsmohit

    View full-size slide

  2. Using Jetpack Compose with Square's Molecule Library
    ● How to setup and use Molecule

    ● Testing with Molecule

    ● Molecule Internals

    ● Example App

    View full-size slide

  3. Molecule
    Problems, Purpose

    View full-size slide

  4. Problem
    Flow 1

    View full-size slide

  5. Problem
    Flow 1
    Flow 2

    View full-size slide

  6. Problem
    @Composable

    fun Profile(flow1, flow2) {

    val state1 by flow1.collectAsState(null)

    val state2 by flow2.collectAsState(0L)

    ...


    }

    View full-size slide

  7. Problem
    @Composable

    fun Profile(viewModel: ProfilesViewModel) {

    val state1 by flow1.collectAsState(null)

    val state2 by flow2.collectAsState(0L)

    ...


    }

    Mixes business logic with view

    View full-size slide

  8. Approach
    Presenter

    (Composable)

    View full-size slide

  9. Approach
    Events
    Presenter

    (Composable)

    View full-size slide

  10. Approach
    Events
    Presenter

    (Composable)
    Molecule

    View full-size slide

  11. Approach
    Events
    Presenter

    (Composable)
    Molecule

    View full-size slide

  12. Approach
    Events
    Presenter

    (Composable)
    Molecule
    State Flow

    View full-size slide

  13. Molecule
    Purpose

    Build a StateFlow using Jetpack Compose

    View full-size slide

  14. Compose vs Compose UI

    View full-size slide

  15. Compose
    ● General purpose tool for managing tree of nodes.

    View full-size slide

  16. Compose
    ● General purpose tool for managing tree of nodes.

    View full-size slide

  17. Compose
    ● General purpose tool for managing tree of nodes.
    Node can be anything

    View full-size slide

  18. Compose UI
    ● UI Toolkit

    View full-size slide

  19. Compose UI
    ● UI Toolkit
    Views, ViewGroup, etc…

    View full-size slide

  20. Resources
    ● A Jetpack Compose by any other name - Jake Wharton

    ● Compose From First Principles - Leland Richardson

    View full-size slide

  21. Molecule
    ● Kotlin Compiler Plugin

    ● Continually recomposes based on clock that controls


    when recomposition occurs

    View full-size slide

  22. How to use Molecule
    Setup & Launch molecules

    View full-size slide

  23. Problem
    Flow 1
    Flow 2

    View full-size slide

  24. Setup
    View Molecule

    View full-size slide

  25. Setup
    View Molecule
    Presenter

    (Composable)
    State

    View full-size slide

  26. Setup
    View Molecule
    Presenter

    (Composable)
    Events

    View full-size slide

  27. Presenter
    @Composable

    fun profilePresenter(
    ...
    ) {

    }

    View full-size slide

  28. Model
    sealed class ProfilesModel {

    object Loading: ProfilesModel()

    data class Data(
    ...
    ): ProfilesModel()

    }

    View full-size slide

  29. Presenter
    @Composable

    fun profilePresenter(
    ...
    ): ProfileModel {

    }

    View full-size slide

  30. Problem
    Flow 1
    Flow 2

    View full-size slide

  31. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  32. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  33. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  34. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  35. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  36. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  37. Setup
    Molecule
    Presenter

    (Composable)

    View full-size slide

  38. Setup
    Molecule
    Presenter

    (Composable)
    StateFlow

    View full-size slide

  39. Launching Molecule
    ● Define a coroutine scope

    View full-size slide

  40. Launching Molecule
    val scope = CoroutineScope(Dispatchers.Main)

    val models: StateFlow = scope.launchMolecule {

    userPresenter(postsFlow, likesFlow)

    }

    View full-size slide

  41. Launching Molecule
    ● Define a coroutine scope

    ● Specify frame clock

    View full-size slide

  42. Monotomic Frame Clock
    ● Controls when recomposition occurs

    ● AndroidUiDispatcher

    ● BroadcastFrameClock

    View full-size slide

  43. Launching Molecule
    val scope = CoroutineScope(

    Dispatchers.Main + AndroidUiDispatcher.Main

    )

    View full-size slide

  44. Launching Molecule
    ● Define a coroutine scope

    ● Specify frame clock

    ● Use launchMolecule method to return state from presenter

    View full-size slide

  45. Launching Molecule
    val models: StateFlow = scope.launchMolecule {

    profilePresenter(postsFlow, likesFlow)

    }

    View full-size slide

  46. Setup
    View Molecule

    View full-size slide

  47. View
    @Composable

    fun Profile(models: StateFlow) {


    val model by models.collectAsState()

    when(model) {

    UsersModel.Loading
    -> ...




    UsersModel.Success
    -> ...

    }



    }

    View full-size slide

  48. View
    @Composable

    fun Profile(models: StateFlow) {


    val model by models.collectAsState()

    when(model) {

    UsersModel.Loading
    -> ...




    UsersModel.Success
    -> ...

    }



    }

    View full-size slide

  49. View
    @Composable

    fun Profile(models: StateFlow) {


    val model by models.collectAsState()

    when(model) {

    ProfileModel.Loading
    -> ...




    ProfileModel.Success
    -> ...

    }



    }

    View full-size slide

  50. Setup
    View
    Presenter

    (Composable)

    View full-size slide

  51. Setup
    View View Model

    View full-size slide

  52. Resources
    ● Try out Molecule in Tivi App by Chris Banes

    View full-size slide

  53. View Model
    class ProfileViewModel: ViewModel() {


    private val moleculeScope = CoroutineScope(

    viewModelScope.coroutineContext +

    AndroidUiDispatcher.Main

    )

    }

    View full-size slide

  54. View Model
    class ProfileViewModel: ViewModel() {


    private val moleculeScope = CoroutineScope(

    viewModelScope.coroutineContext +

    AndroidUiDispatcher.Main

    )

    }

    View full-size slide

  55. View Model
    class ProfileViewModel: ViewModel() {


    val stateFlow: StateFlow

    }

    View full-size slide

  56. View Model
    class ProfileViewModel: ViewModel() {


    val stateFlow = moleculeScope.launchMolecule {

    }

    }

    View full-size slide

  57. View Model
    class ProfileViewModel: ViewModel() {


    val stateFlow = moleculeScope.launchMolecule {

    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    View full-size slide

  58. View Model
    class ProfileViewModel: ViewModel() {


    val stateFlow = moleculeScope.launchMolecule {

    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    View full-size slide

  59. Molecule
    ● How to setup presenter

    ● How to launch molecules

    View full-size slide

  60. Testing
    val models: StateFlow = scope.launchMolecule {

    profilePresenter(postsFlow, likesFlow)

    }

    View full-size slide

  61. Test Cases
    Presenter Model

    View full-size slide

  62. Testing
    ● Uses Turbine

    ● Turbine is a library for testing flows

    View full-size slide

  63. Testing
    testImplementation "app.cash.molecule:molecule-testing"

    View full-size slide

  64. Presenter
    @Test

    fun `should get profiles`() {

    }

    View full-size slide

  65. Presenter
    @Test

    fun `should get profiles`() {

    val posts = MutableSharedFlow()

    val likes = MutableSharedFlow()

    }

    View full-size slide

  66. Presenter
    @Test

    fun `should get profiles`() {

    testMolecule({

    })

    }

    View full-size slide

  67. Presenter
    @Test

    fun `should get profiles`() {

    testMolecule({

    profilePresenter(posts, likes)

    })

    }

    View full-size slide

  68. Presenter
    @Test

    fun `should get profiles`() {

    testMolecule({

    profilePresenter(posts, likes)

    }) {

    assertEquals(

    ProfileModel.Loading, awaitItem()

    )

    }

    }

    View full-size slide

  69. Presenter
    @Test

    fun `should get profiles`() {

    testMolecule({

    profilePresenter(posts, likes)

    }) {

    posts.emit(data)

    }

    }

    View full-size slide

  70. Presenter
    @Test

    fun `should get profiles`() {

    testMolecule({

    profilePresenter(posts, likes)

    }) {

    posts.emit(data)

    assertEquals(

    ProfileModel.Success(data), awaitItem()

    )

    View full-size slide

  71. Testing
    ● Setup tests

    ● How to use Turbine

    View full-size slide

  72. Molecule Internals

    View full-size slide

  73. Internals
    ● Setup Recomposer

    ● Schedule recomposition

    ● Run composition on the passed in body

    View full-size slide

  74. Internals
    fun CoroutineScope.launchMolecule(

    emitter: (value: T)
    ->
    Unit,

    body: @Composable ()
    ->
    T,

    )

    View full-size slide

  75. Internals
    fun CoroutineScope.launchMolecule(

    emitter: (value: T)
    ->
    Unit,

    body: @Composable ()
    ->
    T,

    )

    View full-size slide

  76. Launching Molecule
    val models: StateFlow = scope.launchMolecule {

    profilePresenter(events)

    }

    View full-size slide

  77. Internals
    var flow: MutableStateFlow? = null

    View full-size slide

  78. Internals
    var flow: MutableStateFlow? = null

    launchMolecule(

    emitter = { value
    ->


    outputFlow.value = value

    },

    ...


    )

    View full-size slide

  79. Internals
    fun CoroutineScope.launchMolecule(

    emitter: (value: T)
    ->
    Unit,

    body: @Composable ()
    ->
    T,

    )

    View full-size slide

  80. Internals
    ● Setup Recomposer

    View full-size slide

  81. Internals
    launchMolecule() {

    val recomposer = Recomposer(coroutineContext)

    }
    Scheduler for performing
    recomposition

    View full-size slide

  82. Internals
    launchMolecule() {

    val recomposer = Recomposer(coroutineContext)

    val composition = Composition(recomposer)

    }
    Used to apply composition

    View full-size slide

  83. Internals
    ● Setup Recomposer

    ● Schedule recomposition

    View full-size slide

  84. Internals
    launchMolecule() {

    launch {

    recomposer.runRecomposeAndApplyChanges()

    }

    }

    Suspends


    - Await the invalidation of any

    associated composers.

    View full-size slide

  85. Internals
    launchMolecule() {

    Snapshot.registerGlobalWriteObserver {

    Snapshot.sendApplyNotifications()

    }

    }
    Register an observer

    Called when a state object is modified

    View full-size slide

  86. Internals
    ● Setup Recomposer

    ● Schedule recomposition

    ● Run composition on the passed in body

    View full-size slide

  87. Internals
    launchMolecule() {

    composition.setContent {

    emitter(body())

    }

    } Update composition

    View full-size slide

  88. Launching Molecule
    val models: StateFlow = scope.launchMolecule {

    profilePresenter(events)

    }

    View full-size slide

  89. Android App Example
    ● How to use it in multiple presenters

    ● Launching Molecules

    View full-size slide

  90. Screens
    ● Displays scores

    ● Player info

    ● Weather info

    View full-size slide

  91. Multiplatform App
    Shared
    Android iOS
    GraphQL

    View full-size slide

  92. Architecture
    View Presenter
    Events

    View full-size slide

  93. Architecture
    View Presenter
    Model

    View full-size slide

  94. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  95. Questions
    ● How do you setup the Presenter with Dagger?

    View full-size slide

  96. Presenter
    class ScoresPresenter @Inject constructor() {

    }

    View full-size slide

  97. Presenter
    @Composable

    fun profilePresenter(postsFlow, likesFlow): ProfileModel {


    val posts by postsFlow.collectAsState(null)

    val likes by likesFlow.collectAsState(0L)

    return if (posts
    ==
    null) {

    ProfileModel.Loading

    } else {

    ProfileModel.Success(posts, likes)

    }

    }

    View full-size slide

  98. Questions
    ● How do you setup the composable?

    View full-size slide

  99. Presenter
    class ScoresPresenter @Inject constructor() {

    @Composable

    fun present(events): ScoresModel {

    }

    }

    View full-size slide

  100. Screens
    ● Displays scores

    ● Player info

    ● Weather info

    View full-size slide

  101. Presenter
    class PlayersPresenter @Inject constructor() {

    @Composable

    fun present(events): PlayersModel {

    }

    }

    View full-size slide

  102. Questions
    ● How do you create a pattern for setting up multiple


    presenter?

    View full-size slide

  103. Presenter
    interface MoleculePresenter {

    @Composable

    fun present(events: Flow): Model

    }

    View full-size slide

  104. Presenter
    class ScoresPresenter: MoleculePresenter {

    @Composable

    override fun present(events): ScoresModel {

    }

    }

    View full-size slide

  105. Questions
    ● How do you launch molecules for the presenter?

    View full-size slide

  106. Navigation
    Players
    Navigator
    Scores Weather

    View full-size slide

  107. Navigator
    @Composable

    fun AppNavigator(

    scoresModels: StateFlow,

    playersModels: StateFlow,

    weatherModels: StateFlow

    )

    View full-size slide

  108. Navigator
    class MainActivity : ComponentActivity() {

    override fun onCreate() {

    setContent {

    }

    }

    }

    View full-size slide

  109. Navigator
    class MainActivity : ComponentActivity() {

    override fun onCreate() {

    setContent {

    AppNavigator(
    ...
    )

    }

    }

    }

    View full-size slide

  110. class MainActivity : ComponentActivity() {

    val scoresModels = scope.launchMolecule {

    scoresPresenter.present(events = scoresFlow)

    }

    }
    Launching Molecules

    View full-size slide

  111. class MainActivity : ComponentActivity() {

    val scoresModels = scope.launchMolecule {

    scoresPresenter.present(events = scoresFlow)

    }

    val playerModels = scope.launchMolecule {

    playersPresenter.present(events = playersFlow)

    }

    }
    Launching Molecules

    View full-size slide

  112. Navigator
    class MainActivity : ComponentActivity() {

    override fun onCreate() {

    setContent {

    AppNavigator(

    playersModels,

    scoresModels

    )

    }

    }

    }

    View full-size slide

  113. Approach
    Players
    Main Activity
    Scores Weather

    View full-size slide

  114. Approach
    Players
    Main Activity
    Scores Weather Screen
    ……

    View full-size slide

  115. class MainActivity : ComponentActivity() {

    val scoresModels = scope.launchMolecule {

    scoresPresenter.present(events = scoresFlow)

    }

    }
    Launching Molecules

    View full-size slide

  116. Questions
    ● How do you setup launching molecules for many


    presenters?

    View full-size slide

  117. class MainActivity : ComponentActivity() {

    @Inject

    lateinit var scoresPresenter: ScoresPresenter

    @Inject

    lateinit var weatherPresenter: WeatherPresenter

    }
    Dependency Injection

    View full-size slide

  118. Questions
    ● How do you setup DI with single activity multiple


    composable?

    View full-size slide

  119. Feedback
    val scoresFlow = remember { simulator.simulate(speed = 1000) }

    val scoresModel = scoresPresenter.present(events = scoresFlow)

    ScoresScreen(

    tournamentInfoModel,

    scoresModel

    )

    View full-size slide

  120. Feedback
    val scoresFlow = remember { simulator.simulate(speed = 1000) }

    val scoresModel = scoresPresenter.present(events = scoresFlow)

    ScoresScreen(

    tournamentInfoModel,

    scoresModel

    )

    View full-size slide

  121. Feedback
    val scoresFlow = remember { simulator.simulate(speed = 1000) }

    val scoresModel = scoresPresenter.present(events = scoresFlow)

    ScoresScreen(

    scoresModel

    )

    View full-size slide

  122. Using Jetpack Compose with Square's Molecule Library
    ● How to setup and use Molecule

    ● Testing with Molecule

    ● Molecule Internals

    ● Example App

    View full-size slide

  123. Resources
    ● Using Jetpack Compose with Molecule Library

    ● Molecule Library

    View full-size slide

  124. Thank You!
    www.codingwithmohit.com
    @heyitsmohit

    View full-size slide