Is there a way to use `rememberSaveable` with `StateFlow<T>.collectAsState()` ? I'd like to ke...
t
Is there a way to use
rememberSaveable
with
StateFlow<T>.collectAsState()
? I'd like to keep the state save/restore machinery in the composition if possible.
a
If you have a
StateFlow
of the data already, why save it with
rememberSaveable
?
t
So I don't have to wrap the
StateFlow
in a
ViewModel
to save/restore across configuration changes
👀 1
Copy code
class StateMachine<State : Any, Event : Any>(
    scope: CoroutineScope,
    initialState: () -> State,
    transition: State.(Event) -> State,
) {
    private val events = MutableSharedFlow<Event>()

    private val mutableState = MutableStateFlow<State>(initialState())

    val state = mutableState.asStateFlow()

    init {
        scope.launch {
            events
                .combine(state) { event, state -> state.transition(event) }
                .collect { state -> mutableState.value = state }
        }
    }

    suspend fun send(event: Event) {
        events.emit(event)
    }
}

sealed class MyState {
    object Idle : MyState()
    object Loading : MyState()
    object Error : MyState()
}

sealed class MyEvent {
    object Click : MyEvent()
    object Success : MyEvent()
    object Failure : MyEvent()
}

val transitionFun: State.(Event) -> State = { event ->
    when (this) {
        is Idle, is Error -> when (event) {
            is Click -> Loading
            else -> state
        }
        is Loading -> when (event) {
            is Success -> Idle
            is Failure -> Error
            else -> state
        }
    }
}

@Composable
fun MyScreen() {
    val scope = rememberCoroutineScope()

    val stateMachine = remember {
        StateMachine(
            scope = scope,
            initialState = { MyState.Idle },
            transition = transitionFun
        )
    }

    val state = stateMachine.state.collectAsState()

    MyContent(
        state,
        onClick = {
            scope.launch {
                // `transition` could launch a coroutine to perform requests
                // and emit success/failure events.
                stateMachine.send(Click)
            }
        }
    )
}
...as an example
m
to survive configuration changes, you can use
remember()
though
1
i
remember()
in your
@Composable
does not help with configuration changes - your entire activity and all composables are destroyed with the default handling of configuration changes
1
n
This has always been a pain point in Android development, and one that Compose is not trying to solve as far as I can see, despite having rememberSaveable which is very low level IMO. This is fine - we have ViewModel and SSH - but I got to say I don't understand why I see many comments from [G] devs discouraging the use of ViewModel in this channel.
m
Sorry, I was short on sleep yesterday, and mis-remembered -- thanks for the correction!
d
For new apps, the adoptions of JetpackCompose and Kotlin MultiPlatform are going along with each other. It makes sense to architect an app with a ViewModel which is not platform-specific.
a
kmp is a nice bonus but it's not the motivating reason for design direction in this area.
on a full timeline that includes compose-first development, apps should disable activity recreation for all of the common config change events. Compose already handles all of those cases of declarative reconfiguration by its nature. At that point, yes, just use
remember {}
because there's no live activity recreation to handle, only state restoration from a cold process launch, and
rememberSaveable
handles the latter case.
🎉 1
l
@Adam Powell Is there a short term plan to have the config change handling declaration be the default through AGP facilities?
a
apps adding compose to existing activities that still need to recreate is where ViewModel still comes in handy, and generally you can use that at a fairly high level of hoisting, parting out bits from a ViewModel as plain snapshot or flow-backed state objects as opposed to things that extend the ViewModel class themselves.
@louiscad no, not yet. Maybe an update to the studio template for new compose activities but not anything at the agp layer
1
if the manifest merger were a bit more decoupled it'd be more feasible to use ksp or something to create a
@ComposableActivity
marker for
@Composable
functions to generate the whole activity subclass and its manifest entry outside of agp itself
l
That could also be done via a manifest configuring DSL, no?
a
in the gradle dsl? if one existed for configuring manifests, sure, but I'd prefer to avoid the requirement to declare the same thing in two places
l
Gradle or not. Why would it be declaring the same thing in two places? The only way to declare entry points for a given module using that manifest DSL would be through that manifest DSL in what I have in mind.
a
it would separate the entry point itself (the
@Composable fun
) in a compilation unit intended to execute at runtime from declaring its nature as an entry point in a dsl in a compilation unit intended to run at build time to generate the manifest. Annotation processing carries different expectations as to when it can affect the code/app and can represent that split in the same file.
anyway we're rather far afield now from the topic of ViewModels that started this thread 🙂
l
Can we start a sub-thread or something maybe? 😅
a
at any rate, ViewModel is complicated in order to solve a specific problem, and a temporary one at that if you consider an all-compose UI to be the desired end state for an activity. A lot of attempts to do what it does tend to move that problem to odd places that can break expectations in unexpected ways.
rememberSaveable
performs serialization where live object references are lost. There's a deliberate lossy compression that happens there; live operations in progress that might be associated there are dropped. It's much harder to get into trouble with a stray lambda capture that escapes the scope of a composable that created it.
this is a great fit for things like scroll positions, navigational state, etc.
n
To close the subthread I had started, the idea of not recreating the activity is a very good replacement. Is this documented somewhere though? I'd like to know which are the "common config change events" that compose is able to handle.
a
you can pretty safely let android studio autocomplete everything it knows about for the
android:configChanges
manifest entry; compose can handle all of them. The small list here will also work for the things that are probably motivating the question: https://developer.android.com/guide/topics/resources/runtime-changes#HandlingTheChange - compose obsoletes the big scary warning about doing this 🙂
🙏 1
the full list as of today is this:
Copy code
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
🎉 1
l
How does compose know that a given resource should change for the configuration change that happened? Or is it invalidating all the resources?
t
Allow me to move the goalpost: is ViewModel preferred to save UI state across process death, or is there a way to pipe my State into a
rememberSaveable
in order to save/restore across process death?
1
Or maybe a custom Saver is what I need to use
rememberSaveable
on the StateMachine itself?
👍 1
i
ViewModel has never saved state across process death - only across configuration changes. ViewModels do give you the ability to use a
SavedStateHandle
, but that's not going to give you anything over
rememberSaveable
t
Right, I should have clarified that I would use
SavedStateHandle
. I am still trying to understand how to use
rememberSaveable
with
Flow.collectAsState
, however.
z
@Adam Powell Sorry for digging up the old thread 😅 but is this
configChanges
list for manifest still up to date or maybe there is even some better way to do it nowadays?
a
Probably close. The page that shows up on d.android.com for "android providing resources" should have them, or you can just autocomplete all available options in android studio
🙏 2
🙏 1
l
It'd be so nice to have that done automatically by AGP based on something much less verbose and hard to review.