Hi all, I'm trying to wrap my head around how coro...
# android
d
Hi all, I'm trying to wrap my head around how coroutine/flow works. I have the following in my viewmodel
Copy code
private val _status = MutableLiveData<Status>()
val status: LiveData<Status>
    get = _status

fun getStatus(id: Int) {
    viewModelScope.launch {
        repository.getStatus(id)    // This return Flow<Status>
            .collect { _status = it}
    }
}
And I use data binding on my layout
Copy code
<TextView
    android:text="@{viewModel.status}" />
The UI doesn't get updated to the latest value as the status come through from repository, the Main thread is being held up. If I switch the coroutine to use Dispatcher.IO, I got
"java.lang.IllegalStateException: Cannot invoke setValue on a background thread"
as the status need to be set on Main thread. I've tried using BroadcastChannel
Copy code
private val statusChannel = BroadcastChannel<Status>(Channel.CONFLATED)
val status = statusChannel.asFlow().asLiveData()

fun getStatus(id: Int) {
    viewModelScope.launch(<http://Dispatcher.IO|Dispatcher.IO>) {
        repository.getStatus(id)    // This return Flow<Status>
            .collect { statusChannel.offer(it) }
    }
}
But the idea of converting the channel back to flow feels odd. What's the best way to approach this?
k
You can use _status.postValue(it) instead of _status.value = it
👍 1
d
Oh I didn't realized
.postValue
can be used in non-UI thread. That's all I need, thanks!
k
Or you can switch context inside collect lambda using
withContext(Dispatchers.Main)
since collect is a suspend function and directly set the value instead of postValue
d
Yup that works as well! I do have a question regarding withContext, is there any overhead of switching context multiple times? Or it's negligible?
i
postValue()
will always skip a frame (it uses a technique that waits for vsync), while switching to
Dispatchers.Main
will not necessarily do so (it isn't tied to vsync), making the later generally the better choice
👍 2
d
I'm convinced, will use withContext instead 🙂
i
Of course, you should really avoid this problem entirely - what you actually want is the use a MutableStateFlow of the current ID, then use
flatMapLatest
to transform that into your
getStatus
Flow, then use
asLiveData()
to convert that into a LiveData for your UI layer
As that will cancel the previous
getStatus
collect automatically when the ID changes
(in your current code, if you call
getStatus
twice, the previous collect will continue)
d
Haven't come across MutatableStateFlow yet, will definitely do some reading about it. It's my first kotlin project, still learning the framework 😄
Thanks for the suggestion!
i
Thinking about it, your
BroadcastChannel
+
asFlow
approach for the ID is probably better than a
MutableStateFlow
since
MutableStateFlow
requires an initial state (which you could simulate with a
filterNotNull
if you wanted)
d
Hmm I'm actually giving it a stab at the
MutableStateFlow
Copy code
private val _status = MutableStateFlow<Status>()
val status: LiveData<Status>
    get = _status.asLiveData()

fun getStatus(id: Int) {
    viewModelScope.launch {
        repository.getStatus(id)    // This return Flow<Status>
            .collect { _status = it}
    }
}
And the repository layer would use
.flatMapLatest
to emit the latest flow?
Is
BroadcastChannel
generally the approach to this problem? Feels a little odd to convert it to
Flow
again then
LiveData
Yup you're correct on the initial stated required for
MutableStateFlow
it won't build with the above code as
MutableStateFlow<Status>()
is expecting a parameter
i
I think you misunderstand. I meant something like
Copy code
private val idChannel = BroadcastChannel<Int>(Channel.CONFLATED)
val status = idChannel.asFlow().flatMapLatest { id ->
  repository.getStatus(id)
}.asLiveData()

fun setId(id: Int) {
  idChannel.offer(id)
}
👍 1
d
Ah yup, that makes perfect sense!
i
If your
id
is an argument to your Fragment that doesn't change, you could make this even easier by using the Saved State module for ViewModel: https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate
Copy code
class SavedStateViewModel(state: SavedStateHandle) : ViewModel() {
  val status = state.getLiveData("id").asFlow().flatMapLatest { id ->
    repository.getStatus(id)
  }.asLiveData()
}
This takes advantage of the fact that the
SavedStateHandle
is automatically populated by the arguments of the Fragment when you use Fragment 1.2.0 or higher, meaning you don't ever need to set the ID - it is automatically pulled from the arguments when the ViewModel is created
d
I've come across
SavedStateHandle
but didn't use it as I started off the project using safe args plugin. They seems to work differently. Is
SavedStateHandle
the way forward to pass arguments?
i
They're both the same Bundle under the hood, so as long as the String you pass into
getLiveData()
is the same
android:name
of your argument in your graph, they'll read the same value
You might consider starring the existing feature request for Safe Args + `SavedStateHandle`: https://issuetracker.google.com/issues/136967621
👍 1
d
That's definitely something I'm looking for thanks!
a
i really like the way they are making things simpler these days
i ran away from native android but I'm back now