When using Molecule, how do I ensure state persist...
# squarelibraries
r
When using Molecule, how do I ensure state persists after process death?
Copy code
var isChecked by rememberSaveable { mutableStateOf(false) } <<< does not persist after process death
In a vanilla view model without Molecule, I would: 1. Inject
SavedStateHandle
into the view model 2. Call
getStateFlow()
3. Mutate the key on
SavedStateHandle
Copy code
@HiltViewModel                                                                                                                                                                
class DashboardViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    val isChecked by savedStateHandle.getStateFlow("is_checked", false)
}
s
I'm on mobile, but for state restorations to work, you'll have to provide a
LocalSaveableStateRegistry
manually at the place where you launch molecule
thank you color 1
r
Thanks I'll take a look. If you could provide a more detailed example that'd be super helpful. 🙂
z
tbh seems a good exercise for you to learn how that system works
➕ 1
s
or just use @Zac Sweers's circuit that offers first party support for molecule
r
@Zac Sweers yep, I was actually digging through the Circuit source code yesterday.
@saket does Square / Block use Circuit for any of their apps?
s
Not quite, Cash App uses its own thing called Broadway but it isn't public
nice spin 1
r
Update on my approach and learnings here. I've realized that I don't need to use Molecule to expose state. I'm taking a "Circuit light" approach by exposing a
produceState
function from my vm.
Copy code
@HiltViewModel
class DashboardViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    @Composable
    override fun produceState(): DashboardUiState {
        var isChecked by rememberSaveable { mutableStateOf(false) }

        return DashboardUiState(
            isChecked = isChecked,
        )
    }
}
Since
produceState
is called in a Compose ui context, state persists after process death.
Copy code
@Composable
fun DashboardScreen(
    viewModel: DashboardViewModel
) {
    val uiState = viewModel.produceState()
    // ...
}
For tests, I use Molecule to convert the produceState function into a Flow.
Copy code
moleculeFlow(RecompositionMode.Immediate) {
    withCompositionLocal(
        LocalLifecycleOwner provides TestLifecycleOwner().apply {
            handleLifecycleEvent(Lifecycle.Event.ON_START)
        },
    ) {
        viewModel.produceState()
    }
}.test {
    awaitItem()
    // More assertions...
}
Credit goes to @eygraber for coming up with this approach and sharing it with me. :)
s
So this means that in a config change scenario (like wallpaper color change) you will restart this present function from scratch right? And you rely on saveable to save the state. But if you got any work being done by a LaunchedEffect it will be cancelled through this process
z
sidenote: you should use retention (circuit's or the new upstream version) to avoid the serialization round-trip on config changes
❤️ 1
out of curiosity - why not use circuit here? Tied to hilt/viewmodels? Asking as a maintainer if there's something missing here that's causing the degree of copying heh
e
I've been working on this pattern for a bit now, and I've found that an extremely thin MVI architecture layer built on Compose (UI + presentation) is all I've ever needed for small to medium projects (1-250 modules). I think I've only ever needed a ViewModel once (a misguided attempt at using paging3). Especially the past few years where Compose +
configChanges
avoids 99.9% of the cases where retention would come into play (maybe it's been the various domains I've been working in, but I haven't found wallpaper changes to be particularly common), I've found save state (particularly via
rememberSaveable
) covers almost all cases. I have been meaning to look at utilizing the retain APIs as an optimization when needed though.
I think Circuit is a great arch/library; I just find it too large for my minimalist vision.
z
Ah I was directing at OP
e
OP and I are collabing on a team on these topics 😅
👍 1
s
Completely cancelling work during a config change which you aren't handling does sound quite harsh though tbh. You'd want a coroutine scope which outlives your compose UI in those cases, how would you grab it in this sort of setup which you propose?
e
True, but the reality at this point is that config changes that cause a restart are almost non-existent in my experience. If there's critical work that isn't idempotent I'd have some other way of handling it anyways (WorkManager, an app scoped CoroutineScope, etc...) and the inputs to the work are usually saveable so they can get restored. . The upside is not having to jump through hoops for injection and testing, and that I own all of the code execution; Android is truly just an OS entry point from my perspective (which I believe is what the framework team originally intended, but that's a whole other topic 😅)
☝️ 1
r
@Zac Sweers > out of curiosity - why not use circuit here? Tied to hilt/viewmodels? Asking as a maintainer if there's something missing here that's causing the degree of copying heh I think I'm more open to using Circuit than @eygraber is 😆. It's an ongoing conversion. Definitely not tied to Hilt—we are using Metro (code example above doesn't reflect that but I can't update). Also not tied to view models.
👍 1