0
\$\begingroup\$

I have been writting a lot of code that looks like this

class LoginViewModel : ViewModel() {
 
 // A fragment observes this and shows/hides a progressbar
 val loader = MutableLiveData<Boolean>() 
 
 fun fetchSomeData() = viewModelScope.launch {
 loader.postValue(true)
 // ...
 loader.postValue(false)
 }
}

is there a way to do it in a more clean way? if I have to do 100 requests then there will be 200 lines of loader.postValue

pacmaninbw
26.2k13 gold badges47 silver badges113 bronze badges
asked Aug 31, 2022 at 19:59
\$\endgroup\$
1
  • 3
    \$\begingroup\$ Note that generally on Code Review Stack Exchange, we prefer code with more context rather than less. I think you have gotten a downvote because of the // ... part in your code. (You can leave it as-is for now as your question has already been answered). As an example of a question with more context, see codereview.stackexchange.com/q/279291/31562 \$\endgroup\$ Commented Sep 2, 2022 at 14:33

3 Answers 3

5
\$\begingroup\$

If I interpret it correctly, you can basically wrap everything in a single method, and pass it a lambda:

fun launchWithPost(code: suspend () -> Unit) {
 viewModelScope.launch {
 loader.postValue(true)
 code.invoke()
 loader.postValue(false)
 }
}
fun fetchSomeData() = launchWithPost {
 // ...
}
answered Aug 31, 2022 at 20:08
\$\endgroup\$
2
  • \$\begingroup\$ This looks good, but it wont be reusable in another screen, which will have different ViewModel \$\endgroup\$ Commented Sep 1, 2022 at 19:17
  • 2
    \$\begingroup\$ @GeorgeShalvashvili You could make it an extention method: CoroutineScope.launchWithPost, or have both screens implement the same interface which specifies that viewModelScope and loader should both be available, and then have it as an extension method on that interface. \$\endgroup\$ Commented Sep 2, 2022 at 13:27
3
\$\begingroup\$

Here is what I came up with:

fun Job.withLoader(state: MutableStateFlow<Boolean>) {
 state.tryEmit(true)
 invokeOnCompletion { state.tryEmit(false) }
}
fun Job.withLoader(state: MutableLiveData<Boolean>) {
 state.postValue(true)
 invokeOnCompletion { state.postValue(false) }
}

The usage:

fun authenticate(name: String, password: String) = viewModelScope.launch() {
 authRepo.authenticate(
 username = name,
 password = password,
 )
}.withLoader(loading)
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
answered Aug 31, 2022 at 19:59
\$\endgroup\$
3
\$\begingroup\$

I would leave handling the MutableLiveData<Boolean> and launching a coroutine separate. That way it's not bound to ViewModel at all. The best extension to add it to is the MutableLiveData value itself. Also you are not doing any kind of error handling. Is it correct not to set the value back to false on error?

fun fetchSomeData() = viewModelScope.launch {
 loader.run {
 //do your magic
 }
}
suspend fun MutableLiveData<Boolean>.run(code: suspend () -> Unit) {
 postValue(true)
 try {
 code.invoke()
 } finally {
 postValue(false)
 }
}
answered Sep 28, 2022 at 17:02
\$\endgroup\$
1
  • \$\begingroup\$ Good call setting false in a finally block. I'd suggest a different name than run because it shadows the name of standard library top-level function, impacting readability. Another possible improvement is to replace postValue() with withContext(Dispatchers.Main.immediate){ value = ... } to avoid unnecessary deferring of the UI change by a frame. \$\endgroup\$ Commented Oct 21, 2022 at 18:22

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.