svenjacobs
09/01/2023, 9:52 AMViewModel when the reference of the VM is used in a lambda, like in confirmValueChange of rememberModalBottomSheetState.
For example when I use the VM reference like this
val sheetState = rememberModalBottomSheetState(
confirmValueChange = {
viewModel.someCallback()
true
}
)
the bottom sheet will never show when I call sheetState.show(). However when I use rememberUpdatedState on the VM the bottom sheet works.
val updatedViewModel by rememberUpdatedState(viewModel)
val sheetState = rememberModalBottomSheetState(
confirmValueChange = {
updatedViewModel.someCallback()
true
}
)
I confirmed that viewModel is always the same instance, even after recomposition, so this is not the problem.
The VM is provided with hiltViewModel() in a NavHost. When I change this to a “normal” VM without Hilt injection, the bottom sheet works even without rememberUpdatedState! This is all very confusing. What is even more confusing is that I can reproduce this in a small sample project but sometimes the variant without rememberUpdatedState works, sometimes when I recompile the app it doesn’t 🤷🏼 The relevant code can be found in MainScreen.
Has anyone else encountered this strange and random behaviour?svenjacobs
09/04/2023, 5:32 AMandroidx.hilt:hilt-navigation-compose, where would I best report it?vide
09/04/2023, 7:41 AMvide
09/04/2023, 7:47 AMsvenjacobs
09/04/2023, 7:47 AMvide
09/04/2023, 7:49 AMvide
09/04/2023, 7:51 AMvide
09/04/2023, 7:59 AMvide
09/04/2023, 8:00 AMval lambda = { _: SheetValue ->
viewModel.someCallback()
// updatedViewModel.someCallback()
true
}
Log.v("test", "lambda is ${lambda.hashCode()}")
11:00:08.163 test com.svenjacobs.sample V lambda is 161472184
11:00:09.041 test com.svenjacobs.sample V lambda is 128193555
11:00:13.019 test com.svenjacobs.sample V lambda is 92163513
11:00:13.504 test com.svenjacobs.sample V lambda is 220277346vide
09/04/2023, 8:02 AM@Stable to the viewmodel class, `remember`ing the lambda (or using `rememberUpdatedState`:
11:01:14.804 test com.svenjacobs.sample V lambda is 161472184
11:01:15.208 test com.svenjacobs.sample V lambda is 161472184
11:01:16.189 test com.svenjacobs.sample V lambda is 161472184
11:01:16.572 test com.svenjacobs.sample V lambda is 161472184
The reason you saw it working with rememberUpdatedState is that the lambda object will actually capture the state object (which is stable), not the viewmodel object itself.vide
09/04/2023, 8:03 AMvide
09/04/2023, 8:04 AMsvenjacobs
09/04/2023, 8:25 AMviewModel instance itself never changes? And why does it behave differently once Hilt is involved? If you replace hiltViewModel() with viewModel() and use the other non-Hilt ViewModel class of the sample project everything works 🤷🏼vide
09/04/2023, 8:31 AMsvenjacobs
09/04/2023, 8:31 AMvide
09/04/2023, 8:32 AMvide
09/04/2023, 8:33 AMsvenjacobs
09/04/2023, 8:37 AMMainHiltViewModel with MainViewModel and used viewModel() in MainActivity?svenjacobs
09/04/2023, 8:39 AMsvenjacobs
09/04/2023, 8:42 AMval lambda = { _: SheetValue ->
viewModel.someCallback()
// updatedViewModel.someCallback()
true
}
Log.v("test", "lambda is ${lambda.hashCode()}")
Where did you put this code? Because val lambda = … will of course be evaluated on recomposition but is it actually the lambda instance that is used in rememberModalBottomSheetState()?vide
09/04/2023, 8:45 AMval lambda = { _: SheetValue ->
viewModel.someCallback()
// updatedViewModel.someCallback()
true
}
Log.v("test", "lambda is ${lambda.hashCode()}")
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = lambda
)svenjacobs
09/04/2023, 8:46 AMsvenjacobs
09/04/2023, 8:49 AMrememberModalBottomSheetState you will see that no keys are used for the rememberSaveable call so the same state should be returned regardless of the confirmValueChange argument.svenjacobs
09/04/2023, 8:53 AMsheetState changes on every recomposition once viewModel is used inside the lambda. This is unexpected!
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = {
viewModel.someCallback()
true
}
)
SideEffect {
Log.d("XXX", "${sheetState.hashCode()}")
}vide
09/04/2023, 8:54 AMreturn rememberSaveable(
skipPartiallyExpanded, confirmValueChange,
the keys are definitely here? 🤔vide
09/04/2023, 8:54 AMsvenjacobs
09/04/2023, 8:55 AMinputs and key argumentsvenjacobs
09/04/2023, 8:58 AMviewModel is referenced inside the lambda although the instance is always the same. This is unexpected, right?
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = {
viewModel.someCallback()
true
}
)
SideEffect {
Log.d("XXX", "VM ${viewModel.hashCode()}")
Log.d("XXX", "STATE ${sheetState.hashCode()}")
}
D VM 67021652
D STATE 43694845
D VM 67021652
D STATE 240805241
D VM 67021652
D STATE 12137239
D VM 67021652
D STATE 207944826vide
09/04/2023, 9:00 AMvide
09/04/2023, 9:02 AMsvenjacobs
09/04/2023, 9:04 AM@Stable to the ViewModel class or using a reference of the callback function fixes this behaviour. But I have not seen the recommendation of adding @Stable to a ViewModel before?!
val callback = viewModel::someCallback
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = {
callback()
true
}
)vide
09/04/2023, 9:08 AMvide
09/04/2023, 9:09 AM./gradlew :app:buildDebug --rerun-tasks and checking compose metrics, the viewmodel is reported as runtime stable by compose metrics:
stable class MainHiltViewModel {
<runtime stability> = Stable
}
... and it works (returns the same SheetState instance)vide
09/04/2023, 9:10 AMsvenjacobs
09/04/2023, 9:10 AMvide
09/04/2023, 9:11 AMvide
09/04/2023, 9:12 AM--rerun-tasks I forgot to write.)vide
09/04/2023, 9:13 AMvide
09/04/2023, 9:19 AM--rerun-tasks (or rebuild):
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MainScreen(
stable viewModel: MainViewModel
stable modifier: Modifier? = @static Companion
)
after modifying a log line in the lambda and rebuilding:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MainScreen(
viewModel: MainViewModel
stable modifier: Modifier? = @static Companion
)svenjacobs
09/04/2023, 9:19 AMvide
09/04/2023, 9:19 AMMainViewModelsvenjacobs
09/04/2023, 9:20 AMvide
09/04/2023, 9:24 AMvide
09/04/2023, 9:26 AMrestartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MainScreen(
stable viewModel: MainViewModel
stable modifier: Modifier? = @static Companion
)
by rebuilding:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MainScreen(
viewModel: MainViewModel
stable modifier: Modifier? = @static Companion
)svenjacobs
09/04/2023, 9:29 AMvide
09/04/2023, 9:30 AMsvenjacobs
09/04/2023, 9:32 AMvide
09/04/2023, 9:48 AM--rerun-tasks).
• var0: MainViewModel
• var2: Composer
var2.startReplaceableGroup(1157296644);
ComposerKt.sourceInformation(var2, "CC(remember)P(1):Composables.kt#9igjgp");
boolean var8 = var2.changed(var0);
var9 = var2.rememberedValue();
if (var8 || var9 == Composer.Companion.getEmpty()) {
var9 = (Function1)(new Function1(var0) {
final MainViewModel $viewModel;
{
this.$viewModel = var1;
}
public final Boolean invoke(SheetValue var1) {
Intrinsics.checkNotNullParameter(var1, "<anonymous parameter 0>");
this.$viewModel.someCallback();
Log.v(LiveLiterals$MainScreenKt.INSTANCE.String$arg-0$call-v$fun-$anonymous$$val-lambda$fun-MainScreen(), LiveLiterals$MainScreenKt.INSTANCE.String$arg-1$call-v$fun-$anonymous$$val-lambda$fun-MainScreen());
Log.v(LiveLiterals$MainScreenKt.INSTANCE.String$arg-0$call-v-1$fun-$anonymous$$val-lambda$fun-MainScreen(), LiveLiterals$MainScreenKt.INSTANCE.String$arg-1$call-v-1$fun-$anonymous$$val-lambda$fun-MainScreen());
Log.v(LiveLiterals$MainScreenKt.INSTANCE.String$arg-0$call-v-2$fun-$anonymous$$val-lambda$fun-MainScreen(), LiveLiterals$MainScreenKt.INSTANCE.String$arg-1$call-v-2$fun-$anonymous$$val-lambda$fun-MainScreen());
return LiveLiterals$MainScreenKt.INSTANCE.Boolean$fun-$anonymous$$val-lambda$fun-MainScreen();
}
});
var2.updateRememberedValue(var9);
}
var2.endReplaceableGroup();vide
09/04/2023, 9:50 AMvide
09/04/2023, 9:53 AMvide
09/04/2023, 9:58 AMLog.v() call). No generated rememberedValue & updateRememberedValue.
Function1 var13 = (Function1)(new Function1(var0) {
final MainViewModel $viewModel;
{
this.$viewModel = var1;
}
public final Boolean invoke(SheetValue var1) {
Intrinsics.checkNotNullParameter(var1, "<anonymous parameter 0>");
this.$viewModel.someCallback();
Log.v(LiveLiterals$MainScreenKt.INSTANCE.String$arg-0$call-v$fun-$anonymous$$val-lambda$fun-MainScreen(), LiveLiterals$MainScreenKt.INSTANCE.String$arg-1$call-v$fun-$anonymous$$val-lambda$fun-MainScreen());
Log.v(LiveLiterals$MainScreenKt.INSTANCE.String$arg-0$call-v-1$fun-$anonymous$$val-lambda$fun-MainScreen(), LiveLiterals$MainScreenKt.INSTANCE.String$arg-1$call-v-1$fun-$anonymous$$val-lambda$fun-MainScreen());
Log.v(LiveLiterals$MainScreenKt.INSTANCE.String$arg-0$call-v-2$fun-$anonymous$$val-lambda$fun-MainScreen(), LiveLiterals$MainScreenKt.INSTANCE.String$arg-1$call-v-2$fun-$anonymous$$val-lambda$fun-MainScreen());
return LiveLiterals$MainScreenKt.INSTANCE.Boolean$fun-$anonymous$$val-lambda$fun-MainScreen();
}
});vide
09/04/2023, 10:00 AMsvenjacobs
09/05/2023, 5:56 AMvide
01/24/2024, 10:37 AM