Eaedev
Eaedev

Reputation: 319

Kotlin coroutine list returning null value

New at development of android apps. I am using kotlin and I am triying to retrieve a list from room database at my viewmodel and make a toast at my fragment when I push a button (codes below). If I push the button once, I get an empty string, but if I push twice, I get the list. How can I do to retrive the list with just one push? Probably I am missing something from coroutines.

Viewmodel code:

var Listado = ""


    fun listaTotal(): String {
        uiScope.launch {
            getTodaListaCompra().forEach{
                Log.i("Listado Compra",Listado )
                Listado = Listado + " " + it
                Log.i("Data",data.value)
                Log.i("Pueba",it)
            }
        }
        return Listado 
    }

Fragment call:

Toast.makeText(application, tabListaCompraViewModel.listaTotal(), Toast.LENGTH_SHORT)
                .show()

Thanks in advance

Upvotes: 3

Views: 2817

Answers (2)

nulldroid
nulldroid

Reputation: 1230

You are defining an empty string at start. When listaTotal() gets called the first time, a coroutine gets launched in background to calculate the value of 'listado'. However the return of listaTotal is not waiting for the background coroutine to finish. That's why 'listado' is still empty.

Between your first and second click on the Button, the first coroutine finishes and 'listado' now is not empty anymore, so when you click the button the second time, the coroutine gets launched again but 'listado', again gets returned before that coroutine finishes, so it returns the result of the first button click.

Because you can only make a Toast on the main UI thread, you need to tell it to wait for the coroutine to finish to get the returned value. You can do it using runBlocking, like this:

fun listaTotal(): String = runBlocking {
    getTodaListaCompra().forEach{
        Log.i("Listado Compra",listado )
        listado += " " + it
        Log.i("Data",data.value)
        Log.i("Pueba",it)
    }
    listado
}

Update: To clarify, this approach blocks the main UI thread until the result is returned. You should therefore consider using LiveData (see Sergeys answer) or Flows for fetching data. The purpose of this answer is mainly to explain the behavior of your code and coroutines in general.

Upvotes: 0

Sergio
Sergio

Reputation: 30645

I would suggest using LivaData to observe data:

class MyViewModel : ViewModel() {

    val listado: LiveData<String> = MutableLiveData<String>()

    fun listaTotal() = viewModelScope.launch {
        var localListado = ""
        getTodaListaCompra().forEach{
            localListado = "$localListado $it"
        }
        (listado as MutableLiveData).value = localListado
    }

    // function marked as suspend to suspend a coroutine without blocking the Main Thread
    private suspend fun getTodaListaCompra(): List<String> {
        delay(1000) // simulate request delay
        return listOf("one", "two", "three")
    }
}

In your activity or fragment you can use next methods to instantiate ViewModel class and observe data:

private fun initViewModel() {
    val viewModel = ViewModelProvider(
            this,
            viewModelFactory { MyViewModel() }
    )[MyViewModel::class.java]

    viewModel.listado.observe(this, androidx.lifecycle.Observer { data: String ->
        Toast.makeText(application, data, Toast.LENGTH_SHORT).show()
    })

    viewModel.listaTotal()
}

inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) = object : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(aClass: Class<T>):T = f() as T
}

Also you may need to import next libraries:

api 'androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION'

Upvotes: 1

Related Questions