Dialog队列能避免页面中同时显示复数的窗口,App的首页是个重灾区,例如广告弹框,公告弹框,更新弹框等。

一个Dialog队列应该具备以下特性:

  1. 防重复弹出,因为很多弹框依赖接口返回,这些接口通常也需要在onResume的时候刷新,这种情况的一般解决方案是使用一个成员变量来控制。
  2. 优先级,例如我希望公告弹框优先于广告弹框
  3. 懒加载,在显示时才构建Dialog
  4. 不占用dialog原本的dismissListener,因为我可能要在弹框消失时做某些操作
  5. 生命周期感知,在onResume时才让队列工作, 并且在onDestroy中自动隐藏dialog防止窗体泄漏

实现分析

  1. 防重复:用一个 id 或 tag 来标识 dialog 的唯一性
  2. 优先级:使用优先队列
  3. 懒加载:优先队列中的元素是dialog构建函数,用来构建Dialog
  4. 不占用listener:提供一个next函数给外部主动调用,意味着构建外部构建dialog时需要手动设置dismissListener调用next函数。
  5. 生命周期感知:传入lifecycleOwner对象。

实现代码

class DialogQueue(lifecycleOwner: LifecycleOwner) {

    private val queue = Channel<Unit>(Channel.UNLIMITED)
    private val next = Channel<Unit>(onBufferOverflow = BufferOverflow.DROP_OLDEST)
    private val pq = PriorityQueue<Task>(11, Comparator { o1, o2 ->
        return@Comparator o2.priority - o1.priority
    })

    init {
        var activeDialog: Dialog? = null
        lifecycleOwner.lifecycleScope.launchWhenResumed {
            for (Unit in queue) {
                val task = pq.peek() ?: continue
                val nextFunc = fun() {
                    activeDialog = null
                    pq.remove(task)
                    next.trySend(Unit)
                }
                try {
                    activeDialog = task.dialogBuilder(nextFunc)
                    activeDialog?.show()
                    next.receive()
                } catch (e: Throwable) {
                    e.printStackTrace()
                }
            }
        }

        lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                activeDialog?.dismiss()
                activeDialog = null
            }
        })
    }

    fun offer(tag: String, priority: Int, dialogBuilder: (next: () -> Unit) -> Dialog) {
        val task = Task(tag, priority, dialogBuilder)
        if (pq.contains(task)) {
            return
        }
        pq.offer(task)
        queue.trySend(Unit)
    }

    private class Task(
        val tag: String,
        val priority: Int,
        val dialogBuilder: (next: () -> Unit) -> Dialog
    ) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false
            other as Task
            if (tag != other.tag) return false
            return true
        }

        override fun hashCode(): Int {
            return tag.hashCode()
        }
    }
}

使用方式

class SampleActivity : AppcompatActivity {

    // 传入一个lifecycleOwner对象来初始化一个Dialog队列
    private val queue = DialogQueue(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 将显示“bar” “foo”, 因为bar优先级较高
        showDialogs()

        findViewById<Button>(R.id.show_btn).setOnClickListener {
            // 将显示“foo” “bar”,因为此时队列是空,所以 “foo” 入列的一瞬间就显示了
            showDialogs()
        }
    }

    private fun showDialogs() {
        queue.offer("foo", 1) { next ->
            MaterialAlertDialogBuilder(this)
                .setMessage("Hello, foo!")
                .setOnDismissListener { next() } // 在dialog消失时调用next函数
                .create()
        }

        queue.offer("bar", 2) { next ->
            MaterialAlertDialogBuilder(this)
                .setMessage("Hello, bar!")
                .setOnDismissListener { next() } // 在dialog消失时调用next函数
                .create()
        }
    }
}

Github 完整代码

https://github.com/aitsuki/DialogQueue