Maybe this is a general Kotlin coroutine question....
# compose
r
Maybe this is a general Kotlin coroutine question. Is every code block passed to
viewModelScope.launch { }
run in the same thread? I'm wondering how to avoid loading data twice in my ViewModel. I could have a property there,
isLoading
, but I'm not sure how much I need to worry about thread safety.
f
viewmodelScope.launch
runs on the Main dispatcher, but the suspend block could be changing dispatchers and run the code on a different thread. You can't know that and should not care either.
launch
returns a
Job
, you could hold a reference to that to know if you've already launched the coroutine
r
Ah, okay. Thank you. So if use the
Job
, that transfers my doubts to the scope that calls the ViewModel. Is the Composable
fun
always running in the same thread? So I can just check
if (job != null) { ... }
?
f
it would be better if you explained what you are trying to achieve as your question is too abstract and lacking of context to get a proper answer
r
I have a screen that shows a bunch of info about a car. It starts with a navigation arg,
vehicleID: String
. From there I need to load some data from a server. The ID and data are properties of the view model. The data is null, until it's loaded from the network. If I just do a simple
if (data == null) load()
, then
load()
gets called too many times.
Does that make sense?
f
yes, this is what
Effects
are for. You can use a
LaunchedEffect
to trigger the load in your viewmodel. This will run once the composable enters the composition for the first time. See https://developer.android.com/jetpack/compose/lifecycle for details
👍 1
if you use the navigation component you can also automatically get the arguments in your viewmodel by using SavedStateHandle
r
Thanks. I didn't know about SavedStateHandle either. I'll read this. https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate
i
If you only want to run it once, why not just run it in the
init
block?
Copy code
class VehicleViewModel(savedStateHandle: SavedStateHandle) {
  init {
    // Use the same string as you have in your route argument i.e., vehicle/{vehicleID}
    val vehicleId = savedStateHandle.get("vehicleID")
    viewModelScope.launch {
      // Load your data
    }
  }
}
r
I was creating the ViewModel above my NavHost's
composable(route) { }
and setting the vehicleID from the nav arg, inside the braces. Now I see that if I call
viewModel()
inside the braces, I get the nav args in the state as you show. But... I want to navigate to another page and keep this ViewModel. The first page is like "vehicle details" and the next page is drilling down further into more details.
i
It sounds like what you actually want is a repository that two completely separate ViewModels talk to
That translates IDs -> items
Preferably in a
Flow
based observable pattern and that reads from a source that properly handles process death and recreation
r
Okay, I guess I'm trying to put too much "model" in my ViewModel. Does the navigation component have any sense of one destination being "inside" another? Maybe if I have route paths like "foo/vehicleDetails?id={id}" and "foo/vehicleDetails/more?id={id}" ? So that it wouldn't kill and recreate the ViewModel object when navigating from "vehicleDetails" to "vehicleDetails/more"?
I guess I should think of ViewModels as lightweight adapters between my composables and the real model/repository.
i
Correct, a ViewModel is really just a layer between your domain objects+repository and UI that transforms those domain objects into the minimal set of information needed to fill in your UI
(the most simple translation being an identity translation - just passing it through the layer)