夜间模式下 RecyclerView 的问题 android kotlin NullPointerException

Problem with RecyclerView in Night Mode android kotlin NullPointerException

我的 Fragment 中有一个 RecyclerView,应用中有两个主题:Day、Night 和 System Default。

有一个奇怪的问题导致 NullPointerException。如果我将主题切换为夜间并退出应用程序,然后再次进入,则会出现 NullPointerException 崩溃并且应用程序将不会再次打开,直到我将其从 phone 或模拟器中删除。但是,如果我一直停留在浅色主题上,然后关闭并再次打开应用程序,那么一切都会好起来的。

片段代码:

private  var _binding: FragmentListBinding? = null
private val binding get() = _binding!!

private lateinit var rvAdapter: RvStatesAdapter
private var statesList = ArrayList<State>()
private var databaseReferenceStates: DatabaseReference? = null

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    _binding = FragmentListBinding.inflate(inflater, container, false)

    checkTheme()
    initDatabase()
    getStates()

    binding.rvStates.layoutManager = LinearLayoutManager(requireContext())

    binding.ibMenu.setOnClickListener {
        openMenu()
    }

    return binding.root
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

private fun getStates() {
    databaseReferenceStates?.addValueEventListener(object: ValueEventListener {
        override fun onDataChange(snapshot: DataSnapshot) {
            if (snapshot.exists()) {
                for (stateSnapshot in snapshot.children) {
                    val state = stateSnapshot.getValue(State::class.java)

                    statesList.add(state!!)
                }

                rvAdapter = RvStatesAdapter(statesList)
                binding.rvStates.adapter = rvAdapter
            }
        }

        override fun onCancelled(error: DatabaseError) {

        }
    })
}

private fun initDatabase() {
    FirebaseApp.initializeApp(requireContext());
    databaseReferenceStates = FirebaseDatabase.getInstance().getReference("States")
}

private fun openMenu() {
    binding.drawerLayout.openDrawer(GravityCompat.START)

    binding.navigationView.setNavigationItemSelectedListener {
        when (it.itemId) {
            R.id.about_app -> Toast.makeText(context, "item clicked", Toast.LENGTH_SHORT).show()

            R.id.change_theme -> {
                chooseThemeDialog()
            }
        }

        binding.drawerLayout.closeDrawer(GravityCompat.START)
        true
    }
}

private fun checkTheme() {
    when (ThemePreferences(requireContext()).darkMode) {
        0 -> {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            (activity as AppCompatActivity).delegate.applyDayNight()
        }

        1 -> {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            (activity as AppCompatActivity).delegate.applyDayNight()
        }

        2 -> {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
            (activity as AppCompatActivity).delegate.applyDayNight()
        }
    }
}

private fun chooseThemeDialog() {
    val builder = AlertDialog.Builder(requireContext())
    builder.setTitle("Choose Theme")

    val themes = arrayOf("Light", "Dark", "System default")

    val checkedItem = ThemePreferences(requireContext()).darkMode

    builder.setSingleChoiceItems(themes, checkedItem) {dialog, which ->
        when (which) {
            0 -> {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
                (activity as AppCompatActivity).delegate.applyDayNight()
                ThemePreferences(requireContext()).darkMode = 0
                dialog.dismiss()
            }

            1 -> {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
                (activity as AppCompatActivity).delegate.applyDayNight()
                ThemePreferences(requireContext()).darkMode = 1
                dialog.dismiss()
            }

            2 -> {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
                (activity as AppCompatActivity).delegate.applyDayNight()
                ThemePreferences(requireContext()).darkMode = 2
                dialog.dismiss()
            }
        }
    }

    val dialog = builder.create()
    dialog.show()
}

主题首选项class:

companion object {
    private const val DARK_STATUS = ""
}

private val preferences = PreferenceManager.getDefaultSharedPreferences(context)

var darkMode = preferences.getInt(DARK_STATUS, 0)
    set(value) = preferences.edit().putInt(DARK_STATUS, value).apply()

.xml 代码中的 RecyclerView:

<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvStates"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="20dp"
        android:background="@color/background"
        app:layoutManager="LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvLabelDescription"
        tools:listitem="@layout/rv_state_list" />

还有来自 RecyclerView Adapter 的代码:

inner class MyViewHolder(val binding: RvStateListBinding): RecyclerView.ViewHolder(binding.root)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    return MyViewHolder(RvStateListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    val currentItem = stateList[position]

    with(holder) {
        with(stateList[position]) {
            binding.tvState.text = this.name

            Picasso.with(itemView.context)
                .load(this.image)
                .into(binding.ibState, object: Callback {
                    override fun onSuccess() {
                        binding.progressBar.visibility = View.GONE
                    }

                    override fun onError() {

                    }
                })

            itemView.ibState.setOnClickListener {
                val action = StatesFragmentDirections.actionListFragmentToAttractionsFragment(currentItem)
                itemView.findNavController().navigate(action)
            }
        }
    }
}

override fun getItemCount(): Int {
    return stateList.size
}

你应该只使用

private lateinit var binding: FragmentListBinding

onCreateView()
    binding = FragmentListBinding.inflate(inflater, container, false)

不要在任何地方将其设置为 null

同时将任何不负责创建视图的代码从 onCreateView 移动到 onViewCreated

你的崩溃来自 onDataChange 回调 - 你正在调用 getStates(在设置 binding 之后)但是当结果返回时 onDataChange 尝试访问 binding,它再次为空。

如果我不得不猜测,当您调用 checkTheme 并且它调用 applyDayNight 时,如果 activity 已经在使用您正在应用的主题,那可能什么都不做。因此,如果您正在设置浅色主题,并且它已经在使用浅色主题,那没问题。 (如果您将系统设置为深色主题,假设您的应用程序主题是 DayNight,您可以通过查看它是否停止崩溃来测试它)

但如果需要更改为深色主题,则意味着重新创建 ActivityFragment。我不知道现在重新创建的具体内容,但至少您可能会使用新主题重新创建视图布局。这意味着布局被破坏,这意味着 onDestroyView 被调用 - 在那里,您将 binding 设置为 null

所以我假设你的 onDataChange 回调要么到达布局(和绑定)销毁和重新创建之间,要么整个 Fragment 被销毁并且回调只是调用一个 binding 变量那永远不会恢复。


最简单的解决方法就是不将 binding 设置为空。像 Emmanuel 说的那样让它成为 lateinit,每次调用 onCreateView 时它都会得到 initialised/overwritten。如果回调更新了一个旧的绑定布局,那很好,新的会在 onCreateView 中要求更新无论如何

如果需要,请确保清理在 databaseReferenceStates 上设置的事件侦听器 - 如果这就是您清除 onDestroyView 中绑定的原因,侦听器仍然有对保存该变量的片段的引用,你最终可能会在内存中保留死变量(否则你可以只进行空检查binding

我认为您的问题与主题本身无关,而与 fragment/activity 生命周期有关。如果你打开,不要保持活动和后台然后恢复应用程序它会崩溃。

我认为问题在于您泄露了带有 addValueEventListener 的片段。由于您将其实现为匿名 class,因此您无法在 onStop 上将其删除,因此您的片段会泄漏,因此会出现空指针异常,因为视图已被破坏。