Home > Net >  How to create a global progress dialog that can handle multiple asynchronous tasks
How to create a global progress dialog that can handle multiple asynchronous tasks

Time:08-07

I have a base fragment where I added the method below that will show a progress dialog, on some occasions multiple async methods will start at the same time and all of them will be calling the showProgressDialog method, and so I'm facing the following problems:

  • the methods that call for showProgressDialog are not connected and not fixed, some of them might get called and some might not and they don't run in a specific order, depending on many user conditions.

  • if one of the methods that called the progress dialog failed it needs to show an error dialog, but other methods will stay running until they either finish or fail.

to solve the first problem I created a counter called showNo which will get incremented when a method wants to show the progress dialog, and decremented when the method finishes and wants to hide it, this way the progress dialog will get hidden only when all running methods finish. but this also cause another problem as it very hard to track and if you the method showing progress dialog doesn't get the correct showNo the progress dialog will stay on the screen for ever.

    fun showProgressDialog(show: Boolean) {
    if (!show && dialog != null) {
        showNo--
        if (showNo <= 0) {
            dialog!!.dismiss()
            showNo = 0
        }
        return
    } else if (!show && dialog == null) {
        return
    }
    if (showNo <= 0 && context != null) {
        val binding: DialogProgressBinding =
            DialogProgressBinding.inflate(LayoutInflater.from(requireContext()), null, false)
        dialog = Dialog(requireContext())
        dialog!!.apply {
            setContentView(binding.root)
            setCancelable(false)
            window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
            dismiss()
            show()
        }
    }
    showNo  
}

this is the method I created to show the dialog, but I need a better approach that is more usable

CodePudding user response:

You probably want to create a shared ViewModel that all your Fragments can access, say through your Activity's ViewModelProvider or one for your current navigation graph (if you're using the ktx version of the viewmodel library you can create these with val model: MyViewModel by activityViewModels() or by navGraphViewModels())

That way you can have one central component that's responsible for tracking the current state of what should be displayed, and UI components (say one of your Fragments, or the containing Activity) can observe a LiveData exposing that state, and display any dialogs as required.


It's probably a good idea to create an enum that represents the possible states:

enum class DisplayState {
    NONE, PROGRESS, ERROR, PROGRESS_AND_ERROR
}

Or maybe a sealed class, so you can give the different states properties:

sealed class DisplayState {
    data class Progress(val progressAmount: Int) : DisplayState()
    data class Error(errorMessage: String) : DisplayState()
    data class ProgressAndError(val progressAmount: Int, val errorMessage: String): DisplayState()
}

Or maybe just a combo state object:

// If a thing isn't null, display it!
// Null default values make it easy to say exactly what state you want,
// e.g. DisplayState(errorMessage = "oh nos") is just an error message
data class DisplayState(
    val progressAmount: Int? = null,
    val errorMessage: String? = null
)

And now you can expose that state in a VM:

class MyViewModel : ViewModel() {
    // using the "combo" version above
    private val _displayState = mutableLiveData(DisplayState())
    // observe this to see the current state, and any updates to it
    val displayState: LiveData<DisplayState> get() = _displayState
}

and the observer in the UI layer can show or hide dialogs as necessary.


Since everything's coordinated through this one ViewModel instance, it can keep track of things like how many clients have requested a particular dialog is shown. This part's a bit tricky because it depends what you want - if two different callers request a progress dialog at different times or with different amounts of progress, how much progress do you display? Does that change if the caller with the displayed progress cancels its dialog request?

If so, you'll need to keep track of which callers have ongoing requests and what their individual state (e.g. progress) is, e.g. by putting them in a Map. (You could use a WeakHashMap to avoid holding Fragments in memory, but I wouldn't recommend it - I'll get to that in a minute.) Otherwise, a counter is probably fine? Depending on how you're doing async stuff you might need some thread-safe synchronisation.

Here's one way you could do it, using caller references:

// in the VM
val progressDialogRequesters = mutableSetOf<Any>()

fun requestProgressDialog(caller: Any) {
    progressDialogRequesters  = caller
    // you could be smarter here and e.g. only call it if the set was empty before
    updateDisplayState()
}

fun cancelProgressDialog(caller: Any) {
    progressDialogRequesters -= caller
    // again you could do this only if the set is now empty
    updateDisplayState()
}

// also similar error ones

private fun updateDisplayState() {
    // here you want to push a new state depending on the current situation
    // with -all- requests and any state they have (e.g. progress amounts).
    // Just doing really simple "if there's a request, show it" logic here
    _displayState.value = DisplayState(
        progressAmount = if (progressDialogRequesters.isEmpty()) null else 50,
        errorMessage = if (errorDialogRequesters.isEmpty()) null else "UH OH"
    )
}

If you're doing it that way, you could have Sets holding references to Fragments etc, potentially keeping them in memory. Using weak references like a WeakHashMap can avoid that - but really, what you want is a robust system where you explicitly cancel requests when you don't need them anymore, either because some task has finished, or because the Fragment or whatever is being destroyed. You don't want to rely on the system cleaning up after you, if that makes sense - it can make it harder to track down why things aren't lining up, why your counts are higher than you expected, etc.

It's a little more complicated than that - it really depends how your individual tasks are running, if they're part of a Fragment (and die with it or its viewLifecycleScope, e.g. if the device is rotated) or if they're more independent. You'll want to store the task itself as its own reference, if you can. But this is another example of why you probably want to coordinate all this through the ViewModel, rather than running it as part of the UI Layer. If the VM starts all your tasks, it can keep track of what's running, what's finished and so on. The UI layer just sends events like "start this type of task" and displays the current state. I can only be really general here though because it depends what you're doing. Hope this helps you work something out!

  • Related