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’ 󰑔 😷

    View Slide

  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)

    View Slide

  3. View Slide

  4. A Brief Introduction
    Compose UI
    @askashdavies
    @billjings

    View Slide

  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")
    }
    }

    View Slide

  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")
    }
    }

    View Slide

  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”

    View Slide

  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”

    View Slide

  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”

    View Slide

  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”

    View Slide

  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")
    }
    }

    View Slide

  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")
    }
    }

    View Slide

  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")
    }
    }

    View Slide

  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")
    }
    }

    View Slide

  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")
    }
    }

    View Slide

  16. View Slide

  17. @Composable
    private fun LoginView(
    sessionService: SessionService,
    goTo: (Screen) *> Unit,
    ) {
    val username = remember { mutableStateOf("") }
    val password = remember { mutableStateOf("") }
    var click by remember { mutableStateOf(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()
    }
    }

    View Slide

  18. @Composable
    private fun LoginView(
    sessionService: SessionService,
    goTo: (Screen) *> Unit,
    ) {
    val username = remember { mutableStateOf("") }
    val password = remember { mutableStateOf("") }
    var click by remember { mutableStateOf(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()
    }
    }

    View Slide

  19. @Composable
    private fun LoginView(
    sessionService: SessionService,
    goTo: (Screen) *> Unit,
    ) {
    val username = remember { mutableStateOf("") }
    val password = remember { mutableStateOf("") }
    var click by remember { mutableStateOf(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

    View Slide

  20. @Composable
    private fun LoginView(
    sessionService: SessionService,
    goTo: (Screen) *> Unit,
    ) {
    val username = remember { mutableStateOf("") }
    val password = remember { mutableStateOf("") }
    var click by remember { mutableStateOf(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

    View Slide

  21. sealed class LoginUiModel {
    object Loading : LoginUiModel()
    object Content: LoginUiModel()
    }
    sealed class LoginUiEvent {
    data class Submit(val username: String, val password: String): LoginUiEvent()
    }

    View Slide

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

    View Slide

  23. class LoginPresenter(
    private val sessionService: SessionService,
    private val goTo: (Screen)*>Unit,
    ) {
    */ implement some business logic here
    }

    View Slide

  24. View Slide

  25. Demystifying Molecule
    Molecular Motivation
    @askashdavies
    @billjings

    View Slide

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

    View Slide

  27. @Suppress("DEPRECATION")
    class CallbackLoginPresenter(
    private val service: SessionService,
    private val goTo: (Screen) *> Unit,
    ) {
    **.
    inner class LoginAsyncTask : AsyncTask() {
    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 *> {}
    }
    }
    }
    }

    View Slide

  28. Reactive Programming
    RxJava
    @askashdavies
    @billjings
    RxJava in 2022? Srsly?
    ● Reactive Pipelines (Push Updates)
    ● Explicit thread handling
    ● Inline error-handling
    ● Lifecycle awareness

    View Slide

  29. View Slide

  30. class RxLoginPresenter(val service: SessionService, val goTo: (Screen) *> Unit) {
    fun present(events: Observable): Observable = events.flatMap { 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)
    }

    View Slide

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

    View Slide

  32. kotlin.coroutines
    Imperative Strikes Back
    @askashdavies
    @billjings
    ● Native Library
    ● Less Yoda code (startWith)
    ● suspend fun rules
    ● Observable -> Flow

    View Slide

  33. class CoLoginPresenter(
    private val sessionService: SessionService,
    private val goTo: (Screen) *> Unit,
    ) {
    fun present(events: Flow): Flow = flow {
    emit(LoginUiModel.Content)
    val loginEvent = events.filterIsInstance().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"))
    }
    }
    }

    View Slide

  34. class BigCombinePresenter(
    private val connectivity: ConnectivityManager,
    private val session: SessionService,
    private val goTo: (Screen) *> Unit,
    ) {
    suspend fun present(events: Flow, emit: (LoginUiModel) *> Unit) {
    combine(connectivity.isActive(), session.sessionStatus(), events) { isActive, status, event *>
    if (isActive) {
    emit(LoginUiModel.Content)
    val loginEvent = events.filterIsInstance().first()
    emit(LoginUiModel.Loading)
    if (status *= SessionStatus.Active) {
    /** **. */

    View Slide

  35. Compose != Compose UI
    Kotti knows it’s true, and so do you
    Kotti
    @askashdavies
    @billjings

    View Slide

  36. Compose...?
    The Last Framework?
    @askashdavies
    @billjings
    ● Can consume flows
    ● Compositional state (no more
    ever-expanding combines!)
    ● Declarative job management with
    LaunchedEffect

    View Slide

  37. @Composable
    fun UiModel(events: Flow): LoginUiModel {
    var login by remember { mutableStateOf(null) }
    LaunchedEffect(events) {
    events.filterIsInstance().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
    }
    }

    View Slide

  38. But how?
    @askashdavies
    @billjings
    ● How do we test it?
    ● How do we fit it into our
    architecture?

    View Slide

  39. Demystifying Molecule
    Molecule: Turning
    Compositions Into Flows
    @askashdavies
    @billjings

    View Slide

  40. View Slide

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

    View Slide

  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
    }

    View Slide

  43. View Slide

  44. val recomposer = Recomposer(coroutineContext)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  54. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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()
    }

    View Slide

  55. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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!

    View Slide

  56. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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!

    View Slide

  57. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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:

    View Slide

  58. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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)

    View Slide

  59. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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

    View Slide

  60. val recomposer = Recomposer(coroutineContext)
    val composition = Composition(NoOpApplier, recomposer)
    launch(start = UNDISPATCHED) {
    recomposer.runRecomposeAndApplyChanges()
    }
    val sendApplyNotification = Channel(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

    View Slide

  61. fun moleculeFlow(
    clock: RecompositionClock,
    body: @Composable () *> T,
    ): Flow
    fun CoroutineScope.launchMolecule(
    clock: RecompositionClock,
    body: @Composable () *> T,
    ): StateFlow

    View Slide

  62. Turbine
    github.com/cashapp/turbine
    flowOf("one", "two").test {
    assertEquals("one", awaitItem())
    assertEquals("two", awaitItem())
    awaitComplete()
    }
    @askashdavies
    @billjings

    View Slide

  63. Pros/cons
    Composables
    ● “Are” a StateFlow
    ● Composable state
    Flows
    ● Defined number of items
    ● Meaning is (mostly) not contextual

    View Slide

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

    View Slide

  65. @Test
    fun withContextClock() = runBlocking {
    val goTos = Channel(UNLIMITED)
    val sessionService = FakeSessionService()
    val events = MutableSharedFlow()
    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)
    ...
    }

    View Slide

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

    View Slide

  67. @Test
    fun works() = runBlocking {
    val goTos = Channel(UNLIMITED)
    val sessionService = FakeSessionService()
    val events = MutableSharedFlow()
    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()
    }
    }

    View Slide

  68. @Test
    fun works() = runBlocking {
    val goTos = Channel(UNLIMITED)
    val sessionService = FakeSessionService()
    val events = MutableSharedFlow()
    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())
    }
    }

    View Slide

  69. View Slide

  70. Demystifying Molecule
    Role of Architecture
    @askashdavies
    @billjings

    View Slide

  71. class LoginViewModel(private val service: SessionService) : ViewModel() {
    var viewState by mutableStateOf(null)
    private set
    fun login(username: String, password: String) {
    viewModelScope.launch {
    viewState = service.login(
    username = username,
    password = password
    )
    }
    }
    }

    View Slide

  72. View Lifecycle
    Role of Architecture
    @askashdavies
    @billjings

    View Slide

  73. class MoleculeViewModel(private val presenter: LoginPresenter) : ViewModel() {
    fun present(events: Flow): StateFlow =
    viewModelScope.launchMolecule(RecompositionClock.ContextClock) {
    presenter.UiModel(events)
    }
    }
    @Composable
    fun LoginScreen(viewModel: MoleculeViewModel = viewModel()) {
    val events = remember { MutableSharedFlow() }
    val viewState: LoginUiModel? by viewModel
    .present(events)
    .collectAsState()
    }

    View Slide

  74. github.com/slackhq/circuit
    🚧 Circuit
    @askashdavies
    @billjings

    View Slide

  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)

    View Slide

  76. arkivanov.github.io/Decompose/child-stack/overview
    Decompose
    @askashdavies
    @billjings

    View Slide

  77. class RootComponent(context: ComponentContext) : Root, ComponentContext {
    private val navigation = StackNavigation()
    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()
    }

    View Slide

  78. Obligatory Pet Photo
    CENSORED
    Py
    @askashdavies
    @billjings

    View Slide

  79. Obligatory Pet Photo
    CENSORED
    Py
    @askashdavies
    @billjings

    View Slide

  80. Obligatory Pet Photo
    CENSORED
    Py
    @askashdavies
    @billjings

    View Slide

  81. “Every existing thing is born without
    reason, prolongs itself out of weakness,
    and dies by chance.”
    - Jean-Paul Sartre

    View Slide

  82. Demystifying Molecule
    github.com/ashdavies/demystifying-molecule
    Py
    @askashdavies
    @billjings
    CENSORED

    View Slide

  83. Thanks!
    Ash Davies
    Senior Android Developer
    Android / Kotlin GDE Berlin
    Bill Phillips
    Supreme Android Overlord
    Cash App
    @askashdavies
    @billjings
    Sam dog

    View Slide

  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

    View Slide