How to initialize a field in viewModel with suspend method

Viewed 877

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()
3 Answers

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.

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.

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()
    }
}

Related