One Off the Slack: How Do We Initialize a ViewModel From a Composable?

The indefatigable Colton Idle asked:

This is bad right?

fun SomeScreen(
    viewModel: SomeScreenViewModel = hiltViewModel(),
    someIdINeed: String = ""
) {

The viewModel.init(id) should go inside of a remember{}? Or is it better to do LaunchedEffect(Unit)?

TL;DR: yeah, that feels bad. Anything that has impact outside of the composable, and that is not part of some non-composable callback lambda (e.g., on a click event), should be managed by some sort of effect, usually.

Philip Wedemann offered:

Regarding viewModel init: What about using a custom ViewModel factory which remembers the created viewModel instance?

theapache64 suggested:

I think the problem here is not about remembering the viewModel. Its about potential recomposition causing multiple calls to init method.

You can inject the params to the viewModel through a Bundle and then access it using a SavedStateHandle in the ViewModel? if you’re using navigation compose, this is already there.

Colton then clarified a bit:

I guess theres two things I’m hinting at here.

  1. Is there a better way to inject values like this into a viewModel yet? What I think is im looking for assisted injection but thats not quite there yet?
  1. Lets pretend that init() was just called start() or something and it didn’t take any variables or anything. How would I make sure that it’s only called once? Both remember{} and launchedEffect{} seem right.

theapache64 then leaned towards using LaunchedEffect. Google’s Adam Powell had other ideas:

re. remember {} - keep in mind that until the composition is applied, nothing that executed in a composable has “happened” yet. Beware of executing side effects that leak this way.

is there a better way to represent what’s happening here other than creating an event for a viewmodel to handle in the form of an on-appeared method call?

it sounds like at a minimum there’s a scenario being created here with some sort of id parameter where that id can potentially change, and subsequent recompositions would not capture this if you enforce a once-only call. Same as initializing the contents of a remember {} from parameters without a key.

this sort of thing is why SideEffect doesn’t take keys - it expects whatever it does to implement idempotence somewhere else

IOW, if the parameter to init() actually matters and needs to be a composable function parameter, the right answer probably is to use SideEffect and fix the viewmodel to cope with multiple init() calls, where the passed-in value might change based on how the composable is recomposed.

That approach is what nitrog42 took:

usually on my code, the method init(someId) simply does idStateFlow.emit(someId) , which won’t reemit the same value so it’s kinda ok for me.

However, if the ID does not change, and the viewmodel cannot readily deal with multiple “init”-style calls, then it would be best to inject the value into the viewmodel when it is created, as Philip suggested earlier in the Slack thread.

Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!