EmptyDatabaseAlert 显示两次

EmptyDatabaseAlert showing twice

我有一个 RecyclerView 片段,它的 ViewModel 执行 Room 操作 - add()。如果数据库为空,该片段应显示一个 AlertDialog,允许用户关闭或创建一个新条目。

CrimeListFragment 和相关位:

class CrimeListFragment:
    Fragment(),
    EmptyAlertFragment.Callbacks {

    interface Callbacks {
        fun onCrimeClicked(crimeId: UUID)
    }

    //==========

    private var callback: Callbacks? = null
    private lateinit var crimeRecyclerView: RecyclerView
    private val crimeListViewModel: CrimeListViewModel by lazy {
        ViewModelProviders.of(this).get(CrimeListViewModel::class.java)
    }

    //==========

    override fun onAttach(context: Context) {
        super.onAttach(context)

        callback = context as Callbacks?
    }

    override fun onCreate(savedInstanceState: Bundle?) {}

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {}

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        crimeListViewModel.crimeListLiveData.observe( //crimeListLiveData: LiveData<List<Crime>>
            viewLifecycleOwner,
            Observer { crimes ->
                crimes?.let {
                    Log.i(TAG, "Retrieved ${crimes.size} crimes.")
                    updateUI(crimes)
                }
            }
        )
    }

    override fun onDetach() {
        super.onDetach()

        callback = null
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {}

    override fun onOptionsItemSelected(item: MenuItem): Boolean {}

    override fun onCreateSelected() = createNewCrime()

    //==========

    private fun updateUI(crimes: List<Crime>) {
        if(crimes.isEmpty()) {
            Log.d(TAG, "empty crime list, show empty dialog")
            showEmptyDialog()
        }

        (crimeRecyclerView.adapter as CrimeListAdapter).submitList(crimes)
        Log.d(TAG, "list submitted")
    }

    private fun showEmptyDialog() {
        Log.d(TAG, "show empty dialog")
        EmptyAlertFragment.newInstance().apply {
            setTargetFragment(this@CrimeListFragment, REQUEST_EMPTY)
            show(this@CrimeListFragment.requireFragmentManager(), DIALOG_EMPTY)
        }
    }

    private fun createNewCrime() {
        val crime = Crime()
        crimeListViewModel.addCrime(crime)
        callback?.onCrimeClicked(crime.id)
        Log.d(TAG, "new crime added")
    }

    //==========

    companion object {}

    //==========

    private inner class CrimeHolder(view: View)
        : RecyclerView.ViewHolder(view), View.OnClickListener {}

    private inner class CrimeListAdapter
        : ListAdapter<Crime, CrimeHolder>(DiffCallback()) {}

    private inner class DiffCallback: DiffUtil.ItemCallback<Crime>() {}
}

我的EmptyAlertFragment:

class EmptyAlertFragment: DialogFragment() {

    interface Callbacks {
        fun onCreateSelected()
    }

    //==========

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity!!)

        builder.setPositiveButton("Create") {
                _, _ ->
            targetFragment?.let { fragment ->
                (fragment as Callbacks).onCreateSelected()
            }
        }
        builder.setNegativeButton("Cancel") {
                dialog, _ ->
            dialog.dismiss()
        }

        val alert = builder.create()

        alert.apply {
            setTitle("Crime list empty!")
            setMessage("Do you want to create a new crime?")
        }

        return alert
    }

    //==========

    companion object {
        fun newInstance(): EmptyAlertFragment {
            return EmptyAlertFragment()
        }
    }
}

最后是我的 MainActivity:

class MainActivity:
    AppCompatActivity(),
    CrimeListFragment.Callbacks {

    override fun onCreate(savedInstanceState: Bundle?) {}

    //==========

    override fun onCrimeClicked(crimeId: UUID) {
        val crimeFragment = CrimeDetailFragment.newInstance(crimeId)

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragment_container, crimeFragment)
            .addToBackStack("crime")
            .commit()
    }
}

基本上流程是这样的:

  1. 应用程序启动,CrimeListFragment 观察数据库,updateUI() 被调用,数据库为空,因此弹出警报,也就是显示 EmptyAlertFragment,单击创建 -> onCreateSelected()回调到 CrimeListFragment.
  2. onCreateSelected() 调用 createNewCrime() 使用 ViewModel 添加犯罪(Room, Repository 模式),onCrimeClicked() 回调到 MainActivity.
  3. MainActivity 启动 CrimeDetailFragment 显示现有的或空的(新的)犯罪供我们填写。我们填写它并单击返回,犯罪得到保存:CrimeDetailFragment - onStop() { super.onStop; crimeDetailViewModel.saveCrime(crime) }
  4. 数据库更新,CrimeListFragment 观察数据库变化,updateUI() 被调用,数据库不为空,所以警报不应该弹出,但它确实弹出。
  5. 我再次点击创建,创建第二个犯罪,点击返回,警报将不会再次显示。

换句话说,警报显示的次数太多了。

Logcat 显示:

`Retrieved 0 crimes`
`empty crime list, show empty dialog`
`show empty dialog`
`list submitted`
`*(I add a crime)*`
`new crime added`
`Retrieved 0 crimes` <--- Why? I just created a crime, Observer should notify and `updateUI()` should get called with a non-empty list
`empty crime list, show empty dialog`
`show empty dialog`
`list submitted`
`Retrieved 1 crimes.` <--- Correct behavior from here on out

为什么我的对话框弹出两次而不是一次?

这是由于 LiveData 的工作原理:它缓存并returns查询更新数据之前的最后一个值。

你的 CrimeListFragment 第一次开始 observe crimeListLiveData 时,它得到一个空列表,正确显示你的对话框。

当你去CrimeDetailFragment时,crimeListViewModel.crimeListLiveData不会被摧毁。它保留现有值 - 您的空列表。

因此当你回到你的 CrimeListFragment 时,onCreateView() 再次运行并且你再次开始观察。 LiveData 立即 returns 它拥有的缓存值,Room 异步启动对更新数据的查询。因此,在获得更新的非空列表之前,您应该首先获得一个空列表。

如果在 EmptyAlertFragment 在屏幕上并且 CrimeListFragment 在屏幕后面时旋转设备,您会看到相同的行为 - 您最终会创建您的第二个副本EmptyAlertFragment 同理。然后是第三个、第四个、第五个等等,如果你继续旋转你的设备。

根据 Material design guidelines for dialogs, dialogs are for critical information or important decisions, so perhaps the most appropriate solution for your "Create a new crime" requirement is to not use a dialog at all, instead using an empty state in your CrimeListFragment alongside a Floating Action Button。然后,您的 updateUI 方法将根据计数简单地在空状态和非空 RecyclerView 之间切换。

另一种选择是您的 CrimeListFragment 应该跟踪您是否已经在 boolean 字段中显示对话框,将该布尔值保存到 BundleonSaveInstanceState() 以确保它在旋转和过程死亡/重建中幸存下来。这样您就可以确保对于给定的 CrimeListFragment.

只显示一次对话框