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

Droidcon NYC: Demystifying Molecule

Ash Davies
September 01, 2022

Droidcon NYC: Demystifying Molecule

Molecule is a library for turning Composables into Flows. But how does that happen? And why would you want to do such a thing? And why *not*? In this talk, Ash and Bill will dive a bit into how Molecule does what it does, and help you understand where, when, and how you should use it.

Ash Davies
twitter.com/askashdavies

Bill Phillips
twitter.com/billjings

Ash Davies

September 01, 2022
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Demystifying Molecule Running Your Own Compositions for Fun and Profit

    Ash Davies Senior Android Developer Android / Kotlin GDE Berlin Bill Phillips Staff Android Engineer Cash App @askashdavies @billjings Droidcon NYC - Sep 22’ 󰑔 😷
  2. Who We Are Ash Davies 󰏅 󰎲 Android & Kotlin

    GDE Senior Android Developer Snapp Mobile GmbH Thought about writing a book at some point… Bill Phillips 󰑔 Uncredentialed opinionated person • Cash App, SF • Contributor to Molecule • Rx To Coroutines Concepts series • Wrote a book ages ago (Big Nerd Ranch Guide To Android Programming)
  3. None
  4. A Brief Introduction Compose UI @askashdavies @billjings

  5. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } }
  6. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } }
  7. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } } “billjings” “12345”
  8. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } } “billjings” “12345”
  9. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } } “ashdavies” “12345”
  10. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } } “ashdavies” “12345”
  11. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } }
  12. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() val currentUsername = remember { mutableStateOf(username) } val currentPassword = remember { mutableStateOf(password) } LabeledTextField(text = username, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(text = password, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } }
  13. @Composable private fun LoginInputView( username: String, password: String, onSubmit: ()->Unit,

    ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() val currentUsername = remember { mutableStateOf(username) } val currentPassword = remember { mutableStateOf(password) } LabeledTextField(state = currentUsername, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(state = currentPassword, hidden = true, label = "Password") RhythmSpacer() TextButton(onSubmit, "Login") } }
  14. @Composable private fun LoginInputView( username: String, password: String, onSubmit: (String,

    String)->Unit, ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() val currentUsername = remember { mutableStateOf(username) } val currentPassword = remember { mutableStateOf(password) } LabeledTextField(state = currentUsername, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(state = currentPassword, hidden = true, label = "Password") RhythmSpacer() TextButton({ onSubmit(currentUsername.value, currentPassword.value)}, "Login") } }
  15. @Composable private fun LoginInputView( username: String, password: String, onSubmit: (String,

    String)->Unit, ) { Column(modifier = Modifier.padding(all = 48.dp)) { Text(style = cursiveTextStyle, text = "Login") RhythmSpacer() val currentUsername = remember { mutableStateOf(username) } val currentPassword = remember { mutableStateOf(password) } LabeledTextField(state = currentUsername, hidden = false, label = "Username") RhythmSpacer() LabeledTextField(state = currentPassword, hidden = true, label = "Password") RhythmSpacer() TextButton({ onSubmit(currentUsername.value, currentPassword.value)}, "Login") } }
  16. None
  17. @Composable private fun LoginView( sessionService: SessionService, goTo: (Screen) *> Unit,

    ) { val username = remember { mutableStateOf("") } val password = remember { mutableStateOf("") } var click by remember { mutableStateOf<Int?>(null) } if (click *= null) { LoginInputView(username, password, onSubmit = { click = (click *: 0) + 1 }) } else { LaunchedEffect(click) { when (val result = sessionService.login(username.value, password.value)) { LoginResult.Success *> goTo(LoggedInScreen(username.value)) is LoginResult.Failure *> goTo(ErrorScreen(result.throwable.message *: "Failed to login")) } } ProgressView() } }
  18. @Composable private fun LoginView( sessionService: SessionService, goTo: (Screen) *> Unit,

    ) { val username = remember { mutableStateOf("") } val password = remember { mutableStateOf("") } var click by remember { mutableStateOf<Int?>(null) } if (click *= null) { LoginInputView(username, password, onSubmit = { click = (click *: 0) + 1 }) } else { LaunchedEffect(click) { when (val result = sessionService.login(username.value, password.value)) { LoginResult.Success *> goTo(LoggedInScreen(username.value)) is LoginResult.Failure *> goTo(ErrorScreen(result.throwable.message *: "Failed to login")) } } ProgressView() } }
  19. @Composable private fun LoginView( sessionService: SessionService, goTo: (Screen) *> Unit,

    ) { val username = remember { mutableStateOf("") } val password = remember { mutableStateOf("") } var click by remember { mutableStateOf<Int?>(null) } if (click *= null) { LoginInputView(username, password, onSubmit = { click = (click *: 0) + 1 }) } else { LaunchedEffect(click) { when (val result = sessionService.login(username.value, password.value)) { LoginResult.Success *> goTo(LoggedInScreen(username.value)) is LoginResult.Failure *> goTo(ErrorScreen(result.throwable.message *: "Failed to login")) } } ProgressView() } } Can’t validate ANYTHING
  20. @Composable private fun LoginView( sessionService: SessionService, goTo: (Screen) *> Unit,

    ) { val username = remember { mutableStateOf("") } val password = remember { mutableStateOf("") } var click by remember { mutableStateOf<Int?>(null) } if (click *= null) { LoginInputView(username, password, onSubmit = { click = (click *: 0) + 1 }) } else { LaunchedEffect(click) { when (val result = sessionService.login(username.value, password.value)) { LoginResult.Success *> goTo(LoggedInScreen(username.value)) is LoginResult.Failure *> goTo(ErrorScreen(result.throwable.message *: "Failed to login")) } } ProgressView() } } Can’t validate ANYTHING Without validating EVERYTHING
  21. sealed class LoginUiModel { object Loading : LoginUiModel() object Content:

    LoginUiModel() } sealed class LoginUiEvent { data class Submit(val username: String, val password: String): LoginUiEvent() }
  22. @Composable private fun LoginView(model: LoginUiModel, onEvent: (LoginUiEvent)*>Unit) { when (model)

    { is LoginUiModel.Loading *> ProgressView() is LoginUiModel.Content *> { LoginInputView(onSubmit = { login -> onEvent(LoginUiEvent.Submit(login.username, login.password) ) }) } } }
  23. class LoginPresenter( private val sessionService: SessionService, private val goTo: (Screen)*>Unit,

    ) { */ implement some business logic here }
  24. None
  25. Demystifying Molecule Molecular Motivation @askashdavies @billjings

  26. @Suppress("DEPRECATION") class CallbackLoginPresenter( private val service: SessionService, private val goTo:

    (Screen) *> Unit, ) { var onModel: (LoginUiModel) *> Unit = {} var task: AsyncTask<Submit,Void,LoginResult>? = null fun start() { onModel(Content) } fun stop() { task*.cancel(true) } fun onEvent(event: LoginUiEvent) { when (event) { is Submit *> task = LoginAsyncTask() .also { it.execute(event) } } } **. }
  27. @Suppress("DEPRECATION") class CallbackLoginPresenter( private val service: SessionService, private val goTo:

    (Screen) *> Unit, ) { **. inner class LoginAsyncTask : AsyncTask<Submit,Void,LoginResult>() { private var username: String = "" override fun doInBackground(vararg events: Submit?): LoginResult { val event = events[0]*! username = event.username return runBlocking { service.login(event.username, event.password) } } override fun onPostExecute(result: LoginResult?) { when (result) { is Success *> goTo(LoggedInScreen(username)) is Failure *> goTo(ErrorScreen(result.throwable*.message *: "")) else *> {} } } } }
  28. Reactive Programming RxJava @askashdavies @billjings RxJava in 2022? Srsly? •

    Reactive Pipelines (Push Updates) • Explicit thread handling • Inline error-handling • Lifecycle awareness
  29. None
  30. class RxLoginPresenter(val service: SessionService, val goTo: (Screen) *> Unit) {

    fun present(events: Observable<LoginUiEvent>): Observable<LoginUiModel> = events.flatMap<LoginUiModel> { event *> when (event) { is Submit *> service.loginSingle(event.username, event.password).toObservable().map { result *> when (result) { is Failure *> goTo(ErrorScreen(result.throwable*.message *: "Something went wrong")) is Success *> goTo(LoggedInScreen(event.username)) } Loading }.startWith(Loading) } }.startWith(LoginUiModel.Content) }
  31. Observable .fromIterable(resourceDraft.getResources()) .flatMap(resourceServiceApiClient*:createUploadContainer) .zipWith(Observable.fromIterable(resourceDraft.getResources()), Pair*:create) .flatMap(uploadResources()) .toList() .toObservable() .flatMapMaybe(resourceCache.getResourceCachedItem()) .defaultIfEmpty(Resource.getDefaultItem())

    .flatMap(postResource(resourceId, resourceDraft.getText(), currentUser, getIntent())) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe( resource *> repository.setResource(resourceId, resource, provisionalResourceId), resourceUploadError(resourceId, resourceDraft, provisionalResourceId) ); Well that escalated quickly...
  32. kotlin.coroutines Imperative Strikes Back @askashdavies @billjings • Native Library •

    Less Yoda code (startWith) • suspend fun rules • Observable -> Flow
  33. class CoLoginPresenter( private val sessionService: SessionService, private val goTo: (Screen)

    *> Unit, ) { fun present(events: Flow<LoginUiEvent>): Flow<LoginUiModel> = flow { emit(LoginUiModel.Content) val loginEvent = events.filterIsInstance<LoginUiEvent.Submit>().first() emit(LoginUiModel.Loading) val result = sessionService.login(loginEvent.username, loginEvent.password) when (result) { is LoginResult.Success *> goTo(LoggedInScreen(loginEvent.username)) is LoginResult.Failure *> goTo(ErrorScreen(result.throwable*.message *: "Hmm")) } } }
  34. class BigCombinePresenter( private val connectivity: ConnectivityManager, private val session: SessionService,

    private val goTo: (Screen) *> Unit, ) { suspend fun present(events: Flow<LoginUiEvent>, emit: (LoginUiModel) *> Unit) { combine(connectivity.isActive(), session.sessionStatus(), events) { isActive, status, event *> if (isActive) { emit(LoginUiModel.Content) val loginEvent = events.filterIsInstance<LoginUiEvent.Submit>().first() emit(LoginUiModel.Loading) if (status *= SessionStatus.Active) { /** **. */
  35. Compose != Compose UI Kotti knows it’s true, and so

    do you Kotti @askashdavies @billjings
  36. Compose...? The Last Framework? @askashdavies @billjings • Can consume flows

    • Compositional state (no more ever-expanding combines!) • Declarative job management with LaunchedEffect
  37. @Composable fun UiModel(events: Flow<LoginUiEvent>): LoginUiModel { var login by remember

    { mutableStateOf<Submit?>(null) } LaunchedEffect(events) { events.filterIsInstance<Submit>().collect { login = it } } return if (login *= null) { LaunchedEffect(login) { when (val result = sessionService.login(login*!.username, login*!.password)) { Success *> goTo(LoggedInScreen(login*!.username)) is Failure *> goTo(ErrorScreen(result.throwable.message *: "Failed to login")) } } Loading } else { Content } }
  38. But how? @askashdavies @billjings • How do we test it?

    • How do we fit it into our architecture?
  39. Demystifying Molecule Molecule: Turning Compositions Into Flows @askashdavies @billjings

  40. None
  41. fun integersFlow() = flow { emit(0) for (i in 1*.5)

    { delay(1000) emit(i) } }
  42. fun integersFlow() = flow { emit(0) for (i in 1*.5)

    { delay(1000) emit(i) } } @Composable fun integersComposable(): Int { var output by remember { mutableStateOf(0) } LaunchedEffect(Unit) { for (i in 1*.5) { delay(1000) output = i } } return output }
  43. None
  44. val recomposer = Recomposer(coroutineContext)

  45. val recomposer = Recomposer(coroutineContext) val composition = Composition(applier, compositionContext)

  46. val recomposer = Recomposer(coroutineContext) val composition = Composition(applier, recomposer)

  47. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer)

  48. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() }
  49. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val snapshotHandle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() }
  50. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() }
  51. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) }
  52. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() }
  53. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) }
  54. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() }
  55. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() } That’s Molecule!
  56. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() } That’s Molecule!
  57. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() } For this to recompose, two things must happen:
  58. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() } For this to recompose, two things must happen: 1. content() must be dirty (e.g. new snapshot state)
  59. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() } For this to recompose, two things must happen: 1. content() must be dirty (e.g. new snapshot state) 2. Frame clock must be ticked
  60. val recomposer = Recomposer(coroutineContext) val composition = Composition(NoOpApplier, recomposer) launch(start

    = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } val sendApplyNotification = Channel<Unit>(CONFLATED) launch { for (notification in sendApplyNotification) { Snapshot.sendApplyNotifications() } } val snapshotHandle = Snapshot.registerGlobalWriteObserver { sendApplyNotification.trySend(Unit) } composition.setContent { val output = content() callback(output) } coroutineContext.job.invokeOnCompletion { composition.dispose() snapshotHandle.dispose() } Must have a frame clock For this to recompose, two things must happen: 1. content() must be dirty (e.g. new snapshot state) 2. Frame clock must be ticked
  61. fun <T> moleculeFlow( clock: RecompositionClock, body: @Composable () *> T,

    ): Flow<T> fun <T> CoroutineScope.launchMolecule( clock: RecompositionClock, body: @Composable () *> T, ): StateFlow<T>
  62. Turbine github.com/cashapp/turbine flowOf("one", "two").test { assertEquals("one", awaitItem()) assertEquals("two", awaitItem()) awaitComplete()

    } @askashdavies @billjings
  63. Pros/cons Composables • “Are” a StateFlow • Composable state Flows

    • Defined number of items • Meaning is (mostly) not contextual
  64. @Test fun withContextClock() = runBlocking { val goTos = Channel<Screen>(UNLIMITED)

    val sessionService = FakeSessionService() val events = MutableSharedFlow<LoginUiEvent>() val username = "username" val password = "password" val clock = BroadcastFrameClock() val presenter = LoginPresenter(sessionService, goTos*:trySend) withContext(clock) { moleculeFlow(RecompositionClock.ContextClock) { presenter.UiModel(events) }.test { ... } } }
  65. @Test fun withContextClock() = runBlocking { val goTos = Channel<Screen>(UNLIMITED)

    val sessionService = FakeSessionService() val events = MutableSharedFlow<LoginUiEvent>() val username = "username" val password = "password" val clock = BroadcastFrameClock() val presenter = LoginPresenter(sessionService, goTos*:trySend) withContext(clock) { moleculeFlow(RecompositionClock.ContextClock) { presenter.UiModel(events) }.test { */ Fire up initial LaunchedEffects (if any) yield() assertEquals(LoginUiModel.Content, awaitItem()) events.emit(LoginUiEvent.Submit(username, password)) */ push event into composition yield() */ wait for recomposer to request a new frame yield() clock.sendFrame(0) val nextItem = awaitItem() assertEquals(LoginUiModel.Loading, nextItem) ... }
  66. @Test fun works() = runBlocking { val goTos = Channel<Screen>(UNLIMITED)

    val sessionService = FakeSessionService() val events = MutableSharedFlow<LoginUiEvent>() val username = "username" val password = "password" val presenter = LoginPresenter(sessionService, goTos*:trySend) moleculeFlow(RecompositionClock.Immediate) { presenter.UiModel(events) }.test { */ write a unit test! } }
  67. @Test fun works() = runBlocking { val goTos = Channel<Screen>(UNLIMITED)

    val sessionService = FakeSessionService() val events = MutableSharedFlow<LoginUiEvent>() val username = "username" val password = "password" val presenter = LoginPresenter(sessionService, goTos*:trySend) moleculeFlow(RecompositionClock.Immediate) { presenter.UiModel(events) }.test { */ Fire up initial LaunchedEffects (if any) yield() } }
  68. @Test fun works() = runBlocking { val goTos = Channel<Screen>(UNLIMITED)

    val sessionService = FakeSessionService() val events = MutableSharedFlow<LoginUiEvent>() val username = "username" val password = "password" val presenter = LoginPresenter(sessionService, goTos*:trySend) moleculeFlow(RecompositionClock.Immediate) { presenter.UiModel(events) }.test { */ Fire up initial LaunchedEffects (if any) yield() assertEquals(LoginUiModel.Content, awaitItem()) events.emit(LoginUiEvent.Submit(username, password)) assertEquals(LoginUiModel.Loading, awaitItem()) assertEquals(LoginAttempt(username, password), sessionService.loginAttempts.awaitValue()) sessionService.loginResults.trySend(LoginResult.Success) assertEquals(LoggedInScreen(username), goTos.awaitValue()) } }
  69. None
  70. Demystifying Molecule Role of Architecture @askashdavies @billjings

  71. class LoginViewModel(private val service: SessionService) : ViewModel() { var viewState

    by mutableStateOf<LoginResult?>(null) private set fun login(username: String, password: String) { viewModelScope.launch { viewState = service.login( username = username, password = password ) } } }
  72. View Lifecycle Role of Architecture @askashdavies @billjings

  73. class MoleculeViewModel(private val presenter: LoginPresenter) : ViewModel() { fun present(events:

    Flow<LoginUiEvent>): StateFlow<LoginUiModel> = viewModelScope.launchMolecule(RecompositionClock.ContextClock) { presenter.UiModel(events) } } @Composable fun LoginScreen(viewModel: MoleculeViewModel = viewModel()) { val events = remember { MutableSharedFlow<LoginUiEvent>() } val viewState: LoginUiModel? by viewModel .present(events) .collectAsState() }
  74. github.com/slackhq/circuit 🚧 Circuit @askashdavies @billjings

  75. @Parcelize object PetListScreen : Screen { sealed interface State :

    Parcelable { @Parcelize object Loading : State @Parcelize object Success : State } } val circuit: Circuit = ** **. */ val navigator = circuit.navigator( { content *> setContent { StarTheme { content() } } }, onBackPressedDispatcher*:onBackPressed, ) navigator.goTo(PetListScreen)
  76. arkivanov.github.io/Decompose/child-stack/overview Decompose @askashdavies @billjings

  77. class RootComponent(context: ComponentContext) : Root, ComponentContext { private val navigation

    = StackNavigation<Config>() override val childStack = childStack(** **. */) fun createChild(config: Config, context: ComponentContext): Child = when (config) { is Config.List *> Child.List(itemList(context)) is Config.Details *> ** **. */ } private fun itemList(context: ComponentContext): ItemList = ItemListComponent(context) { navigation.push(Config.Details(itemId = it)) } } private sealed class Config : Parcelable { @Parcelize object List : Config() @Parcelize data class Details(val itemId: Long) : Config() }
  78. Obligatory Pet Photo CENSORED Py @askashdavies @billjings

  79. Obligatory Pet Photo CENSORED Py @askashdavies @billjings

  80. Obligatory Pet Photo CENSORED Py @askashdavies @billjings

  81. “Every existing thing is born without reason, prolongs itself out

    of weakness, and dies by chance.” - Jean-Paul Sartre
  82. Demystifying Molecule github.com/ashdavies/demystifying-molecule Py @askashdavies @billjings CENSORED

  83. Thanks! Ash Davies Senior Android Developer Android / Kotlin GDE

    Berlin Bill Phillips Supreme Android Overlord Cash App @askashdavies @billjings Sam dog
  84. Demystifying Molecule Links • State of Managing State with Compose

    [code.cash.app/the-state-of-managing-state-with-compose] • Crouching Theme Hidden DI [code.cash.app/crouching-theme-hidden-di] • Do iiiiiiit. [twitter.com/billjings/status/1514772865869967370] • Feature [monkeyuser.com] @askashdavies @billjings