3

How to initialize a field in view model if I need to call the suspend function to get the value?

I a have suspend function that returns value from a database.

suspend fun fetchProduct(): Product

When I create the view model I have to get product in this field

private val selectedProduct: Product 

I tried doing it this way but it doesn't work because I'm calling this method outside of the coroutines

private val selectedProduct: Product = repository.fetchProduct()
Sergio
  • 27,326
  • 8
  • 128
  • 149

3 Answers3

2

You can't initialize a field in the way you described. suspend function must be called from a coroutine or another suspend function. To launch a coroutine there are a couple of builders for that: CoroutineScope.launch, CoroutineScope.async, runBlocking. The latter is not recommended to use in production code. There are also a couple of builders - liveData, flow - which can be used to initialize the field. For your case I would recommend to use a LiveData or Flow to observe the field initialization. The sample code, which uses the liveData builder function to call a suspend function:

val selectedProduct: LiveData<Product> = liveData {
    val product = repository.fetchProduct()
    emit(product)
}

And if you want to do something in UI after this field is initialized you need to observe it. In Activity or Fragment it will look something like the following:

// Create the observer which updates the UI.
val productObserver = Observer<Product> { product ->
    // Update the UI, in this case, a TextView.
    productNameTextView.text = product.name
}

// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
viewModel.selectedProduct.observe(this, productObserver)

For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 or higher.

Sergio
  • 27,326
  • 8
  • 128
  • 149
1

Since fetchProduct() is a suspend function, you have to invoke it inside a coroutine scope.

For you case I would suggest the following options:

  1. Define selectedProduct as nullable and initialize it inside your ViewModel as null:
class AnyViewModel : ViewModel {

    private val selectedProduct: Product? = null
    

    init {
        viewModelScope.launch {
            selectedProduct = repository.fetchProduct()
        }
    }
}
  1. Define selectedProduct as a lateinit var and do the same as above;

Personally I prefer the first cause I feel I have more control over the fact that the variable is defined or not.

Arthur Bertemes
  • 976
  • 14
  • 21
  • 1
    this is a bad practice, you have no way of knowing if there's actually a value or not and you'd have to check the variable multiple times hoping for there to be something, [livedata](https://developer.android.com/topic/libraries/architecture/livedata) is the perfect solution for this – a_local_nobody Apr 01 '22 at 17:55
  • you are right. I just wanted to be more practical with OP's question, which is how to initialize a variable returned by a suspend function. – Arthur Bertemes Apr 01 '22 at 20:08
  • Why do this in `init` block? – IgorGanapolsky Aug 22 '22 at 20:53
  • You don't necessarily need to do that on `init`, it's just that, since OP asked how to initialize the `ViewModel` with a value returned from a suspend function, `init` seemed a nice place to do that. – Arthur Bertemes Aug 23 '22 at 17:19
-1

You need to run the function inside a coroutine scope to get the value.

if you're in a ViewModel() class you can safely use the viewModelScope

private lateinit var selectedProduct:Product

fun initialize(){
    viewModelScope.launch {
        selectedProduct = repository.fetchProduct()
    }
}

MikkelT
  • 633
  • 1
  • 10
  • 16
  • But then the field will not be safe – Sasha Arutyunyan Apr 01 '22 at 16:22
  • 1
    Well you could make it nullable and null-check it whenever you want to use it. There's sadly no way to it 100% safe when you're relying on an asynchronous call to initialize a field. – MikkelT Apr 01 '22 at 16:36
  • this is a bad practice, you have no way of knowing if there's actually a value or not and you'd have to check the variable multiple times hoping for there to be something, [livedata](https://developer.android.com/topic/libraries/architecture/livedata) is the perfect solution for this – a_local_nobody Apr 01 '22 at 17:55
  • I was providing a solution to OP's problem, not an architectural design pattern... – MikkelT Apr 01 '22 at 18:07
  • Always LiveData is just a value container, and does not solve OP's problem in any way. Do you even know what you are talking about? – MikkelT Apr 01 '22 at 18:08
  • @a_local_nobody how to solve this proble with livedata or flow? – Sasha Arutyunyan Apr 01 '22 at 18:52
  • `Do you even know what you are talking about?` there's no need to be rude, and yes, i do know what i'm talking about, live data isn't just an architectural design, it has very little to do with architecture at all, if you use live data you can observe onto it in the activity/fragment so that you actually know when the value has been assigned, then there's no need for you to rely on `selectedProduct` being populated by something else, you can directly be told when it does have a value – a_local_nobody Apr 01 '22 at 20:29
  • How about you provice a actual solution instead of just calling "bad practice" on someone's answer (talking about rudeness). You still have yet to provide OP with a solution using LiveData... – MikkelT Apr 01 '22 at 20:35