https://kotlinlang.org logo
#compose
Title
# compose
l

Lilly

07/29/2022, 6:18 AM
I get "Skipped frames < 100" for some semi-heavy viewmodel operations, at least I think the reason is a viewmodel operation. I'm not sure, it also might be a light non-suspending function so I'm wondering if I should wrap every non-suspending viewmodel function into a coroutine?
Copy code
fun someViewModelCall() {
    viewModelScope.launch(Dispatchers.Default) { // light stuff }
}
EDIT: Alternate question would be, if it is common that skipped frames happen ocasionally?
a

Arkadii Ivanov

07/29/2022, 6:22 AM
I would first use the profiler in the AS to find out the actual cause of lags.
l

Lilly

07/29/2022, 6:24 AM
Alternate question would be, if it is common that skipped frames happen ocassionally?
a

Arkadii Ivanov

07/29/2022, 6:34 AM
Skipped frames may happen. You can try running your app in release mode, this should have better performance. But I would still use the profiler, maybe there is room for improvements. Also mind https://issuetracker.google.com/issues/210040434
c

Colton Idle

07/29/2022, 7:29 AM
Yeah. Running a compose app in release mode is hugely beneficial. i think this podcast had a bunch of info on why debug builds with compose are slow. https://adbackstage.libsyn.com/episode-183-baseline-profiles but yeah. if in doubt. put it into release mode really quick and see if you get better perf. I use this snippet in my projects to make release builds with my debug key when i deploy from my ide.
Copy code
signingConfig =
    if (properties.containsKey("android.injected.invoked.from.ide")) {
      signingConfigs.getByName("debug")
    } else {
      signingConfigs.getByName("release")
    }
l

Lilly

07/29/2022, 7:50 AM
Thanks for sharing Colton
a

Adam Powell

07/29/2022, 1:04 PM
This is more of an #android question
Jumping over to another thread when you don't need to will often cause the operation to take longer, so you'll want to profile to confirm suspicions before jumping to the conclusion. Android does some thread prioritization across big and little cpu cores on some devices and the main thread is often given priority in the name of responsiveness
In terms of code patterns, the snippet in the OP can get risky because you've now introduced a fire and forget operation by performing a
viewModelScope.launch
in the implementation details of a ViewModel method. You're now responsible for making sure you don't have concurrent operations disrupting one another and for managing cancellation in the event you no longer want the operation to proceed before the ViewModel is cleared
These are the very issues that structured concurrency is designed to prevent
l

Lilly

07/29/2022, 4:29 PM
Thanks for ther insights Adam ❤️
@Adam Powell
In terms of code patterns, the snippet in the OP can get risky because you've now introduced a fire and forget operation by performing a
viewModelScope.launch
in the implementation details of a ViewModel method.
Can you please elaborate on this please. I mean using
viewModelScope.launch
is the only way to start a coroutine and I thought viewModel is the right place to start coroutines?
Or is it common in compose to start coroutines from composable?
Copy code
fun someViewModelCall() {
    viewModelScope.launch(Dispatchers.Default) { // light stuff }
}

becomes

suspend fun someViewModelCall() {
    // light or heavy operation
}
a

Arkadii Ivanov

07/29/2022, 4:42 PM
I think this means that, without launch operations run synchronously on the main thread and can't overlap. But with launch they can, which can introduce concurrency issues. You have to check that the underlying code is actually able to run concurrently, and always keep this in mind.
l

Lilly

07/29/2022, 4:48 PM
Ah ok so this means I should carefully decide if I wrap my viewModel operation into a coroutine or not. So for example if the operation is light, avoid launch, if the operation is a heavy backend operation use launch.
a

Arkadii Ivanov

07/29/2022, 4:49 PM
I would say yes, at least this is how I understand Adam.
a

Adam Powell

07/29/2022, 5:10 PM
Where you launch from depends on how long you want the operation to last. A ViewModel's viewModelScope stays active while an app is in the background, while the associated UI is on the back stack, etc.
so it's often a broader scope than you might want
and yes, making your viewmodel calls `suspend fun`s gets you back to a point where the viewmodel is able to know if the caller still cares or not
c

Colton Idle

07/29/2022, 5:11 PM
It's so convenient though 🙈
a

Adam Powell

07/29/2022, 5:11 PM
yes and that's why I regret us ever publishing it as an api 😛
declaring
val myViewModelScope = CoroutineScope(myFavoriteDispatcher + whateverElse + Job())
and then doing
myViewModelScope.cancel()
in
onCleared
is trivially easy but it makes you stop and think about the behaviors at play, and it doesn't create a gravity well around, "well,
viewModelScope
is already here so that must be what I want, right?"
anyway these are my own opinions on this and at least as of now they aren't universally shared across the android/devrel teams (yet 😛 )
c

Colton Idle

07/29/2022, 5:15 PM
Good points all around. I'm really running into this now because I'm using Firestore for live query updates. And oh God. We didn't know what we were doing and now every screen is updating at once.
a

Adam Powell

07/29/2022, 5:15 PM
but if you have a
suspend fun
on your viewmodel then the viewmodel can do things like
suspend fun doThing(...) = viewModelScope.async { doRealWorkForThing(...) }.await()
which means the operation can still keep going, you can store the returned
Deferred
or
Job
in the viewmodel to dedupe the operation if multiple calls come in, but you still have the signal available on whether a caller is still listening
it's roughly equivalent in pattern to what the
Flow.stateIn
operator does by accepting a scope to collect in on behalf of multiple collectors that might come or go at any time
and it's also why
val someFlow = blah.stuff().operators().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue)
is so useful in viewmodels too
the
WhileSubscribed
defines that, "does anyone still care?" aspect that can shut the upstream operations down when the answer is, "no"
by contrast when you just do
fun doThing(...) { viewModelScope.launch { ... } }
then you've thrown away any possible, "is anyone still listening for the results of what I'm doing here?" signal
l

Lilly

07/30/2022, 11:18 PM
Thanks Adam
@Adam Powell Actually I'm not using Jetpacks viewModels. I'm using a presenter class, which gives me the option to recreate it when screen (sucscriber) is reentering. In this case it would not be a problem because scope is destoryed on leaving screen. Is Jetpacks viewModel a singleton or is it recreated on screen entering?
Out of curiosity...how would you call a suspend function from compose? I'm aware of 2 options:
Copy code
1:
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
    scope.launch { viewModel/presenter.callSuspendFun() }
    onDispose {}
}
2:
LaunchedEffect() {
    launch { viewModel/presenter.callSuspendFun() }
}
a

Adam Powell

08/02/2022, 2:54 PM
androidx viewmodel delegates the lifetime up to a
ViewModelStoreOwner
and it performs some handoff of currently live viewmodels across android activity recreations. They aren't singletons but they outlive individual
Activity
instances across configuration changes
You don't need the inner
launch
inside the
LaunchedEffect
block any more than you need to write things like
Copy code
myScope.launch {
  launch { callSuspendFun() }
}
I would be extra suspicious of (1) since it's doing more work than necessary to accomplish the same thing as (2) (presuming you eliminated the inner launch from 2)
l

Lilly

08/03/2022, 6:10 PM
@Adam Powell Yeah I already noted that. But why does LaunchedEffect be called two times sometimes although the key parameter didnt changed:
Copy code
LaunchedEffect(connectionState) {
        Log.w("ParameterScreen", "LaunchedEffect, conState $connectionState.")
        if (connectionState == Protocol.State.READY) {
            Log.e("ParameterScreen", "collecting.")
            parameterPresenter.loadState()
        }
    }
It does not happen often and so I have no logs for the moment. But
constate
is 2 times the same and still
loadState()
is called. Or is this behavior intended?
a

Adam Powell

08/03/2022, 7:03 PM
I would guess that you have more than one instance of this composable observing the same connection state, because it shouldn't be called twice for the same instance. You could verify with something like this:
Copy code
val token = remember { Any() }
LaunchedEffect(connectionState) {
  Log.w("ParameterScreen", "LaunchedEffect, conState $connectionState, token $token")
if you get different identity hashes printed for
token
there when you observe the otherwise same log message printed more than once, then you have different instances of the composable
l

Lilly

08/03/2022, 8:48 PM
@Adam Powell token is the same, here are the logs
Copy code
W/ParameterScreen: LaunchedEffect, conState READY, hash: 229053043, token: java.lang.Object@e640d2b.
E/ParameterScreen: collecting.
E/BasePresenter: isLoading true
E/ParameterPresenter: fetch web-keys.

+ 1 second later
W/ParameterScreen: LaunchedEffect, conState READY, hash: 229053043, token: java.lang.Object@e640d2b.
E/ParameterScreen: collecting.
E/BasePresenter: isLoading true
E/ParameterPresenter: fetch web-keys.

And a bit later the coroutine is canceled:
W/System.err: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@55683c8
To enusre that
connectionState
is in both cases the same I also logged its hashCode, represented as
hash
in the logs. I'm using compose version 1.2.0. Any ideas? Just for completeness...this didn't happen once with DisposableEffect
a

Adam Powell

08/03/2022, 9:17 PM
if you can post a minimal repro of this on the issue tracker I'd appreciate it
the described behavior is unexpected and I have not seen this before
11 Views