简体   繁体   English

如何创建可以处理多个异步任务的全局进度对话框

[英]How to create a global progress dialog that can handle multiple asynchronous tasks

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:我有一个基本片段,我在其中添加了下面将显示进度对话框的方法,在某些情况下,多个异步方法将同时启动,并且它们都将调用 showProgressDialog 方法,因此我面临以下问题:

  • 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.调用 showProgressDialog 的方法未连接且未修复,其中一些可能会被调用,而另一些可能不会被调用,并且它们不会按特定顺序运行,具体取决于许多用户条件。

  • 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.为了解决第一个问题,我创建了一个名为 showNo 的计数器,当方法想要显示进度对话框时它会增加,当方法完成并想要隐藏它时会减少,这样只有在所有正在运行的方法时才会隐藏进度对话框结束。 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.但这也会导致另一个问题,因为它很难跟踪,如果您显示进度对话框的方法没有得到正确的 showNo,则进度对话框将永远留在屏幕上。

    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这是我为显示对话框而创建的方法,但我需要一种更好用的方法

You probably want to create a shared ViewModel that all your Fragment s 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() )您可能希望创建一个所有Fragment都可以访问的共享ViewModel ,例如通过您的 Activity 的ViewModelProvider或用于当前导航图的一个(如果您使用的是ktx版本的viewmodel库,您可以使用val model: MyViewModel by activityViewModels()创建这些val model: MyViewModel by activityViewModels()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 Fragment s, or the containing Activity ) can observe a LiveData exposing that state, and display any dialogs as required.这样,您可以拥有一个中央组件,负责跟踪当前 state 应显示的内容,并且 UI 组件(例如您的Fragment之一或包含Activity )可以observe一个LiveData公开该 state,并将任何对话框显示为必需的。


It's probably a good idea to create an enum that represents the possible states:创建一个代表可能状态的enum可能是个好主意:

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

Or maybe a sealed class, so you can give the different states properties:或者可能是密封的 class,因此您可以提供不同的状态属性:

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:或者可能只是一个组合 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:现在您可以在 VM 中公开 state:

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. UI 层中的观察者可以根据需要显示或隐藏对话框。


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.由于一切都通过这个ViewModel实例进行协调,因此它可以跟踪诸如有多少客户请求显示特定对话框之类的事情。 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 (eg progress) is, eg by putting them in a Map .如果是这样,您需要跟踪哪些调用者有正在进行的请求以及他们各自的 state(例如进度)是什么,例如将它们放入Map中。 (You could use a WeakHashMap to avoid holding Fragment s in memory, but I wouldn't recommend it - I'll get to that in a minute.) Otherwise, a counter is probably fine? (您可以使用 Wea WeakHashMap来避免在 memory 中保存Fragment ,但我不推荐它 - 我会在一分钟内完成。)否则,计数器可能没问题? 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 Set s holding references to Fragment s etc, potentially keeping them in memory.如果你这样做,你可以让Set持有对Fragment等的引用,可能会将它们保存在 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.使用WeakHashMap类的弱引用可以避免这种情况 - 但实际上,您想要的是一个健壮的系统,当您不再需要请求时,您可以显式取消请求,因为某些任务已经完成,或者因为Fragment或任何正在被破坏的东西. 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 , eg if the device is rotated) or if they're more independent.它比这更复杂一些 - 它实际上取决于您的各个任务是如何运行的,它们是否是Fragment的一部分(并与它或它的viewLifecycleScope ,例如,如果设备旋转)或者它们是否更独立。 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.但这是另一个示例,说明您可能希望通过ViewModel协调所有这些,而不是将其作为 UI 层的一部分运行。 If the VM starts all your tasks, it can keep track of what's running, what's finished and so on.如果 VM 启动所有任务,它可以跟踪正在运行的内容、完成的内容等等。 The UI layer just sends events like "start this type of task" and displays the current state. UI 层只发送诸如“启动此类任务”之类的事件并显示当前的 state。 I can only be really general here though because it depends what you're doing.不过,我在这里只能很笼统,因为这取决于您在做什么。 Hope this helps you work something out!希望这可以帮助您解决问题!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM