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

\$\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 {
  // ...
}
\$\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)
\$\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)
    }
}
\$\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\$
    – Tenfour04
    Commented Oct 21, 2022 at 18:22

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.