When is the `init { }` block in a ViewModel called? The init block for Screen A's ViewModel doesn't ...
v
When is the
init { }
block in a ViewModel called? The init block for Screen A's ViewModel doesn't seem to be triggered when swiping back from Screen B, for instance.
y
I believe its like a constructor. So once, even if you pause and resume the activity.
If you want to do something whenever resumed. Then I assume you would use lifecycle events.
v
Such as a LaunchedEffect?
y
I meant inside the viewmodel, not the composable
Actually, that's not obvious, sorry. Let me dig.
v
Ah. More for me to research, haven't encountered them yet.
y
Yep, actually doesn't seem like there is an automatic way to do it. Sorry for bad advice. The examples implement LifecycleObserver in ViewModel and hook it up externally. Not fun.
From a composable I guess more like
Copy code
val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(Unit) {
        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {

        }
    }
With things like SwipeToDimiss the composable can be created before it's actually on screen. Or may not be called when it's in the background but still exists. So lifecycle seems more correct.
v
OK, I think I have it working like this:
ViewModel:
Copy code
init {
  refresh()
}

fun refresh() {
// fetches data from Room database, setting up LiveData objects
}
Composable function:
Copy code
Theme {
val livecycleOwner = LocalLifecycleOwner.current
        LaunchedEffect(Unit) {
            livecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
                Log.i("MainMenu comp","Lifecycle resumed, calling refresh")
                viewModel.refresh()
            }
        }
}
There's a wee bit of flicker on the emulator but otherwise working. Thank you.
Maybe not. Oh well.
Works on the emulator, not on the phone!
z
The ViewModel isn’t recreated but one is used from cache, so init won’t be called again. You’re on the right track. Only issue right now is refresh is called twice. Once in the instructor and once through the LaunchedEffect. Some will tell you to keep your ViewModels side effect free (don’t use its init block to launch any API calls)
v
If I don't fetch the data from the database on init, how will I have data? The first thing the application needs to do is find out if there any saved records.
z
You’re fetching that data from your LaunchedEffect. Also, if you’re getting a LiveData from your refresh function then there’s no reason to constantly fetch that since LiveData already does that (see the Live part)
y
A common pattern is the viewmodel provides a stateflow produced by stateIn
Composable uses collectAsStateWithLifecycle
StateIn has modes like while subscribed
v
It's always one step forward then three steps back with me and Compose I'm afraid 😞
z
Can you actually share with us what you’re doing inside refresh function?
v
Copy code
// Function gets the number of Vehicles in the database
// Then looks to see if there is an activeChargeEvent stored in the SettingsRepository. If there is, it fetches it from the ChargeEventRepository

// The composable function displays a FAB if there are vehicles and there is no charge event
// If there is a charge event, the FAB isn't shown, but the charge event details are

 fun refreshView() {
        viewModelScope.launch {
            _vehicleCount = vehicleRepository.getVehicleCount()
            Log.i("ChargeEventViewModel refresh", "_vehicleCount = $_vehicleCount")
            _selectedVehicle = settingsRepository.getSetting(SettingsKey.SELECTED_VEHICLE)?.lValue
            val activeChargeId =
                settingsRepository.getSetting(SettingsKey.CURRENT_CHARGE_EVENT)?.lValue
            Log.i("ChargeEventViewModel refresh", "activeChargeId = $activeChargeId")
            if (activeChargeId != null) {
                _activeChargeEvent = chargeEventRepository.getLiveChargeEventWithId(activeChargeId)
                Log.i("ChargeEventViewModel refresh", "_activeChargeEvent = ${_activeChargeEvent.value}")
            }
        }
    }
If I call refreshView in the ViewModel init block, AND I use a LaunchedEffect with lifecycleOwner.withStateAtLeast(Lifecycle.State.CREATED) { viewModel.refreshView() } in my Composable, THEN I get the behaviour I expect.
d
But this results in refreshView being called once, when the screen is opened for the first time. To answer your original question, the viewmodel would be scoped to the nav back stack entry usually which means that if you are coming "back" to a screen, your viewmodel init won't be called. Like Zun has suggested you might want to get rid of that side effect on init and just let your Composable tell the VM when to request the data
c
When you scwipe back from B back to A, you're not creating the VM for A again. A's VM still exists on the backstack.
So if you have A, B, C, D, E and you're looking at E. You actually have the VM for ABCD and E "running".
3806 Views