How can I get data from ViewModel to Activity or Fragment in clean and simple way?

Viewed 90

I have a question... sometimes, I need to get data from ViewModel directly. For example, Let's say there's a isChecked() method in ViewModel. And I want to use it in the if condition.

if(viewModel.isChecked()){
    // TODO:
}

So, what I am doing right now is:

fun isChecked(): Boolean = runBlocking {
    val result = dbRepo.getData()
    val response = apiRepo.check(result)
    return response.isSuccessful
}

It uses runBlocking. So, it runs on MainThread. I don't think it's a good way because it can freeze the screen. But yes, if the condition needs to run, it needs to wait it until it gets the data from DB and Network.

Another way that I can think of is using LiveData. However, I can't use it in the condition. So, I needs to move the condition in the observer block. But sometimes, this can't be done because there can be something before the condition. And it doesn't seem to look direct but writing code here and there and finally get that data.

So, Is there any simpler way than this?

3 Answers

Your best bet if you have something slow or blocking like that is to rethink how you are using the data entirely. Instead of trying to return it, use LiveData or callbacks to handle the response asynchronously without causing your UI to hang or become laggy. In these cases you really only have three options:

  1. Use a callback to handle when the response is received
  2. Use observable data like LiveData to handle when the response is received
  3. Change the method to a suspend function and call it from a coroutine

Forcing a method to wait to return on the main thread without using one of these is going to cause the app to hang.

Callback to get state

It's hard to say definitely what the best solution for you is without more details about how you are using isChecked(), but one pattern that could work would be to use a callback to handle what you were formerly putting in the if statement, like this (in the ViewModel):

fun getCheckedState(callback: (Boolean)->Unit) {
    viewModelScope.launch {
        // do long-running task to get checked state,
        // using an appropriate dispatcher if needed
        val result = dbRepo.getData()
        val response = apiRepo.check(result)

        // pass "response.isSuccessful" to the callback, to be
        // used as "isChecked" below
        callback(response.isSuccessful)
    }
}

You would call that from the activity or fragment like this:

viewModel.getCheckedState { isChecked ->
  if( isChecked ) {
    // do something
  }
  else {
    // do something else
  }
}

// CAUTION: Do NOT try to use variables you set inside 
// the callback out here!

A word of caution - the code inside the callback you pass to getCheckedState does not run right away. Do not try to use things you set inside there outside the callback scope or you fall into this common issue

Simpler Callback

Alternately, if you only want to run some code when isChecked is true, you could simplify the callback like this

fun runIfChecked(callback: ()->Unit) {
    viewModelScope.launch {
        // do long-running task to get checked state,
        // using an appropriate dispatcher if needed
        val result = dbRepo.getData()
        val response = apiRepo.check(result)

        // only call the callback when it's true
        if( response.isSuccessful ) {
            callback()
        }
    }
}

and call it with

viewModel.runIfChecked {
  // do something
}

// Again, don't try to use things from the callback out here!

Use lifecyclescope.launch(Dispatcher.IO) instead of runblocking

Try this code on your ViewModel class:

suspend fun isChecked(): Boolean {
    val response: Response? = null
    viewModelScope.launch(Dispatchers.IO) {
        val result = dbRepo.getData()
        response = apiRepo.check(result)
    }.join()
    return response?.isSuccessful
}

From Activity:

// Suppose you have a button
findViewById<Button>(R.id.btn).setOnClickListener({
    CoroutineScope(Dispatchers.Main).launch {
        if (viewModel.isChecked()) {
            Log.d("CT", "Do your others staff")
        }
    }
})

Hope it work file. If no let me comment

Related