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 220277346
vide
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 207944826
vide
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 AMMainViewModel
svenjacobs
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