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

Droidcon London: Demystifying Molecule

Ash Davies
October 28, 2022

Droidcon London: Demystifying Molecule

Molecule is a library for turning Composables into Flows. But how does that happen? What are the advantages and disadvantages to building and running your own Compositions, and where to even start?

In this talk we’ll cover the history of reactive design patterns with Kotlin, Coroutines, Compose, and how Molecule gives you the ability to compose your own business logic into functional units. How you might use this for unit testing, and how the role of architecture components fits into the equation.

We’ll show you why you might want to do such a thing, and where it might make sense not to. How Molecule works under the hood, and help you to understand where, when and how you should use it.

Ash Davies

October 28, 2022
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Demystifying Molecule
    Running Your Own Compositions for Fun and Profit
    Droidcon London - Oct 22’ 󰏅
    Ash Davies
    Android / Kotlin GDE - Berlin
    @askashdavies
    @[email protected]

    View Slide

  2. View Slide

  3. Compose UI
    A Brief Introduction
    @[email protected]

    View Slide

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

    View Slide

  5. @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(text = email, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(text = pass, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    LoginInputView("billjings", "12345")
    @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(text = email, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(text = pass, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    LoginInputView("billjings", "12345")
    @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(text = email, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(text = pass, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    LoginInputView("ashdavies", "12345")
    @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(text = email, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(text = pass, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    LoginInputView("ashdavies", "12345")

    View Slide

  6. @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(text = email, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(text = pass, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    val currentUsername = remember { mutableStateOf(email) }
    val currentPassword = remember { mutableStateOf(pass) }
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(text = email, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(text = pass, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: () *> Unit) {
    val currentUsername = remember { mutableStateOf(email) }
    val currentPassword = remember { mutableStateOf(pass) }
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    LabeledTextField(state = currentUsername, hidden = false, label = "Username")
    RhythmSpacer()
    LabeledTextField(state = currentPassword, hidden = true, label = "Password")
    RhythmSpacer()
    TextButton(onSubmit, "Login")
    }
    }
    @Composable
    fun LoginInputView(email: String, pass: String, onSubmit: (String, String) *> Unit) {
    val currentUsername = remember { mutableStateOf(email) }
    val currentPassword = remember { mutableStateOf(pass) }
    Column(modifier = Modifier.padding(all = 48.dp)) {
    Text(style = cursiveTextStyle, text = "Login")
    RhythmSpacer()
    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

  7. View Slide

  8. @Composable
    private fun LoginView(service: 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 = service.login(username.value, password.value)) {
    LoginResult.Success *> goTo(LoggedInScreen(username.value))
    is LoginResult.Failure *> goTo(ErrorScreen("${result.throwable.message}"))
    }
    }
    ProgressView()
    }
    } Can’t validate ANYTHING
    Without validating EVERYTHING

    View Slide

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

    View Slide

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

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

    View Slide

  12. Molecular Motivation
    Demystifying Molecule
    @[email protected]

    View Slide

  13. @Suppress("DEPRECATION")
    class CallbackLoginPresenter(val service: SessionService, 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)
    }
    }
    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 *> Unit
    }
    }
    }

    View Slide

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

    View Slide

  15. View Slide

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

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

  18. kotlin.coroutines
    Imperative Strikes Back
    ● Native Library
    ● Less Yoda code (startWith)
    ● suspend fun rules
    ● Observable -> Flow
    @[email protected]

    View Slide

  19. class CoLoginPresenter(val sessionService: SessionService, 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}"))
    }
    }
    }

    View Slide

  20. class BigCombinePresenter(
    val connectivity: ConnectivityManager,
    val session: SessionService,
    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

  21. Compose != Compose UI
    Kotti knows it’s true, and so should you
    Kotti
    @[email protected]

    View Slide

  22. Compose...?
    The Last Framework?
    ● Can consume flows
    ● Compositional state (no more
    ever-expanding combines!)
    ● Declarative job management with
    LaunchedEffect
    @[email protected]

    View Slide

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

  24. But how?
    ● How do we test the presenters?
    ● How do we fit it into our
    architecture?
    @[email protected]

    View Slide

  25. Molecule: Turning
    Compositions Into Flows
    Demystifying Molecule
    @[email protected]

    View Slide

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

    View Slide

  27. fun integersFlow() = flow {
    emit(0)
    for (i in 1*.5) {
    delay(1000)
    emit(i)
    }
    }
    @Composable
    fun integersComposable(): Int {
    val output by produceState(0) {
    for (i in 1*.5) {
    delay(1000)
    value = i
    }
    }
    return output
    }

    View Slide

  28. val recomposer = Recomposer(coroutineContext)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

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

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

    View Slide

  45. Turbine
    github.com/cashapp/turbine
    flowOf("one", "two").test {
    assertEquals("one", awaitItem())
    assertEquals("two", awaitItem())
    awaitComplete()
    }
    @[email protected]

    View Slide

  46. Pros / Cons
    Composables
    ● “Are” a StateFlow
    ● Composable state
    Flows
    ● Defined number of items
    ● Meaning is (mostly) not contextual
    @[email protected]

    View Slide

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

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

    View Slide

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

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

  51. Role of Architecture
    Demystifying Molecule
    @[email protected]

    View Slide

  52. Jetpack ViewModel

    View Slide

  53. View Lifecycle
    Role of Architecture
    @[email protected]

    View Slide

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

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


  56. https://developer.android.com/guide/topics/resources/runtime-changes#HandlingTheChange

    View Slide

  57. @Composable
    fun rememberSaveable(
    vararg inputs: Any?,
    stateSaver: Saver,
    key: String? = null,
    init: () *> MutableState
    ): MutableState = rememberSaveable(
    *inputs,
    saver = mutableStateSaver(stateSaver),
    key = key,
    init = init
    )
    https://developer.android.com/jetpack/compose/state

    View Slide

  58. slackhq.github.io/circuit/
    🚧 Circuit
    @[email protected]

    View Slide

  59. @Parcelize object PetListScreen : Screen {
    sealed interface State : Parcelable {
    @Parcelize object Loading : State
    @Parcelize object Success : State
    }
    }
    val backstack = rememberSaveableBackStack {
    push(PetListScreen)
    }
    val navigator = rememberCircuitNavigator(
    onRootPop = onBackPressedDispatcher*:onBackPressed,
    backstack = backstack,
    )
    navigator.goTo(** **. */)

    View Slide

  60. arkivanov.github.io/Decompose/child-stack/overview
    Decompose
    @[email protected]

    View Slide

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

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

    View Slide

  63. Demystifying Molecule
    github.com/ashdavies/demystifying-molecule
    Py
    CENSORED
    @[email protected]

    View Slide

  64. Ash Davies
    Android & Kotlin GDE Berlin
    @askashdavies
    Thanks!
    Kotti
    Follow for more cat pics!
    @[email protected]

    View Slide

  65. Demystifying Molecule
    Links
    ● Demystifying Molecule - DC NYC
    [droidcon.com/2022/09/29/demystifying-molecule-running-your-own-compositions-for-fun-and-profit/]
    ● 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]
    @[email protected]

    View Slide