Building StateFlows with Jetpack Compose.
Mohit SarveiyaUsing Jetpack Compose with Square's Molecule Library@heyitsmohit
View Slide
Using Jetpack Compose with Square's Molecule Library● How to setup and use Molecule● Testing with Molecule● Molecule Internals● Example App
MoleculeProblems, Purpose
Problem
ProblemFlow 1
ProblemFlow 1Flow 2
Problem@Composable fun Profile(flow1, flow2) {val state1 by flow1.collectAsState(null)val state2 by flow2.collectAsState(0L)...}
Problem@Composable fun Profile(viewModel: ProfilesViewModel) {val state1 by flow1.collectAsState(null)val state2 by flow2.collectAsState(0L)...} Mixes business logic with view
ApproachPresenter (Composable)
ApproachEventsPresenter (Composable)
ApproachEventsPresenter (Composable)Molecule
ApproachEventsPresenter (Composable)MoleculeState Flow
MoleculePurpose Build a StateFlow using Jetpack Compose
Compose vs Compose UI
Compose● General purpose tool for managing tree of nodes.
Compose● General purpose tool for managing tree of nodes.Node can be anything
Compose UI● UI Toolkit
Compose UI● UI ToolkitViews, ViewGroup, etc…
Resources● A Jetpack Compose by any other name - Jake Wharton● Compose From First Principles - Leland Richardson
Molecule● Kotlin Compiler Plugin● Continually recomposes based on clock that controls when recomposition occurs
How to use MoleculeSetup & Launch molecules
SetupView
SetupView Molecule
SetupView MoleculePresenter (Composable)State
SetupView MoleculePresenter (Composable)Events
Presenter@Composable fun profilePresenter(...) {}
Modelsealed class ProfilesModel {object Loading: ProfilesModel()data class Data(...): ProfilesModel()}
Presenter@Composable fun profilePresenter(...): ProfileModel {}
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)}}
SetupMoleculePresenter (Composable)
SetupMoleculePresenter (Composable)StateFlow
Launching Molecule● Define a coroutine scope
Launching Moleculeval scope = CoroutineScope(Dispatchers.Main)val models: StateFlow = scope.launchMolecule {userPresenter(postsFlow, likesFlow)}
Launching Molecule● Define a coroutine scope● Specify frame clock
Monotomic Frame Clock● Controls when recomposition occurs● AndroidUiDispatcher ● BroadcastFrameClock
Launching Moleculeval scope = CoroutineScope(Dispatchers.Main + AndroidUiDispatcher.Main)
Launching Molecule● Define a coroutine scope● Specify frame clock● Use launchMolecule method to return state from presenter
Launching Moleculeval models: StateFlow = scope.launchMolecule {profilePresenter(postsFlow, likesFlow)}
View@Composable fun Profile(models: StateFlow) { val model by models.collectAsState()when(model) {UsersModel.Loading-> ...UsersModel.Success-> ... }}
View@Composable fun Profile(models: StateFlow) { val model by models.collectAsState()when(model) {ProfileModel.Loading-> ...ProfileModel.Success-> ... }}
SetupViewPresenter (Composable)
SetupView View Model
Resources● Try out Molecule in Tivi App by Chris Banes
View Modelclass ProfileViewModel: ViewModel() { private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext +AndroidUiDispatcher.Main)}
View Modelclass ProfileViewModel: ViewModel() { val stateFlow: StateFlow}
View Modelclass ProfileViewModel: ViewModel() { val stateFlow = moleculeScope.launchMolecule {}}
View Modelclass 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)}
Molecule● How to setup presenter● How to launch molecules
Testing
Testingval models: StateFlow = scope.launchMolecule {profilePresenter(postsFlow, likesFlow)}
Test CasesPresenter Model
Testing● Uses Turbine● Turbine is a library for testing flows
TestingtestImplementation "app.cash.molecule:molecule-testing"
Presenter@Testfun `should get profiles`() {}
Presenter@Testfun `should get profiles`() {val posts = MutableSharedFlow()val likes = MutableSharedFlow()}
Presenter@Testfun `should get profiles`() {testMolecule({})}
Presenter@Testfun `should get profiles`() {testMolecule({profilePresenter(posts, likes)})}
Presenter@Testfun `should get profiles`() {testMolecule({profilePresenter(posts, likes)}) {assertEquals(ProfileModel.Loading, awaitItem())}}
Presenter@Testfun `should get profiles`() {testMolecule({profilePresenter(posts, likes)}) {posts.emit(data)}}
Presenter@Testfun `should get profiles`() {testMolecule({profilePresenter(posts, likes)}) {posts.emit(data) assertEquals(ProfileModel.Success(data), awaitItem())
Testing● Setup tests● How to use Turbine
Molecule Internals
Internals● Setup Recomposer● Schedule recomposition● Run composition on the passed in body
Internalsfun CoroutineScope.launchMolecule(emitter: (value: T)->Unit,body: @Composable ()->T,)
Launching Moleculeval models: StateFlow = scope.launchMolecule {profilePresenter(events)}
Internalsvar flow: MutableStateFlow? = null
Internalsvar flow: MutableStateFlow? = nulllaunchMolecule(emitter = { value->outputFlow.value = value},...)
Internals● Setup Recomposer
InternalslaunchMolecule() {val recomposer = Recomposer(coroutineContext)}Scheduler for performingrecomposition
InternalslaunchMolecule() {val recomposer = Recomposer(coroutineContext)val composition = Composition(recomposer)}Used to apply composition
Internals● Setup Recomposer● Schedule recomposition
InternalslaunchMolecule() {launch {recomposer.runRecomposeAndApplyChanges()}} Suspends - Await the invalidation of any associated composers.
InternalslaunchMolecule() {Snapshot.registerGlobalWriteObserver {Snapshot.sendApplyNotifications()}}Register an observer Called when a state object is modified
InternalslaunchMolecule() {composition.setContent {emitter(body())}} Update composition
Example App
Android App Example● How to use it in multiple presenters● Launching Molecules
Screens● Displays scores ● Player info● Weather info
Multiplatform AppSharedAndroid iOSGraphQL
ArchitectureView PresenterEvents
ArchitectureView PresenterModel
Questions● How do you setup the Presenter with Dagger?
Presenterclass ScoresPresenter @Inject constructor() {}
Questions● How do you setup the composable?
Presenterclass ScoresPresenter @Inject constructor() {@Composablefun present(events): ScoresModel {}}
Presenterclass PlayersPresenter @Inject constructor() {@Composablefun present(events): PlayersModel {}}
Questions● How do you create a pattern for setting up multiple presenter?
Presenterinterface MoleculePresenter {@Composablefun present(events: Flow): Model }
Presenterclass ScoresPresenter: MoleculePresenter {@Composableoverride fun present(events): ScoresModel {}}
Questions● How do you launch molecules for the presenter?
NavigationPlayersNavigatorScores Weather
Navigator@Composablefun AppNavigator(scoresModels: StateFlow,playersModels: StateFlow,weatherModels: StateFlow)
Navigatorclass MainActivity : ComponentActivity() {override fun onCreate() {setContent {}}}
Navigatorclass MainActivity : ComponentActivity() {override fun onCreate() {setContent {AppNavigator(...)}}}
class MainActivity : ComponentActivity() {val scoresModels = scope.launchMolecule {scoresPresenter.present(events = scoresFlow)}}Launching Molecules
class MainActivity : ComponentActivity() {val scoresModels = scope.launchMolecule {scoresPresenter.present(events = scoresFlow)}val playerModels = scope.launchMolecule {playersPresenter.present(events = playersFlow)}}Launching Molecules
Navigatorclass MainActivity : ComponentActivity() {override fun onCreate() {setContent {AppNavigator(playersModels,scoresModels)}}}
ApproachPlayersMain ActivityScores Weather
ApproachPlayersMain ActivityScores Weather Screen……
Questions● How do you setup launching molecules for many presenters?
class MainActivity : ComponentActivity() {@Injectlateinit var scoresPresenter: ScoresPresenter@Injectlateinit var weatherPresenter: WeatherPresenter}Dependency Injection
Questions● How do you setup DI with single activity multiple composable?
Feedback
Feedbackval scoresFlow = remember { simulator.simulate(speed = 1000) }val scoresModel = scoresPresenter.present(events = scoresFlow)ScoresScreen(tournamentInfoModel,scoresModel)
Feedbackval scoresFlow = remember { simulator.simulate(speed = 1000) }val scoresModel = scoresPresenter.present(events = scoresFlow)ScoresScreen(scoresModel)
Resources● Using Jetpack Compose with Molecule Library● Molecule Library
Resources
Thank You!www.codingwithmohit.com@heyitsmohit