首页 > 解决方案 > 夜间模式下 RecyclerView 的问题 android kotlin NullPointerException

问题描述

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

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

片段代码:

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()
}

主题偏好类:

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
}

例外

标签: androidkotlinandroid-recyclerviewandroid-night-mode

解决方案


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

如果我不得不猜测,当您调用checkTheme并且它调用applyDayNight时,如果活动已经在使用您正在应用的主题,那可能没有任何作用。因此,如果您正在设置浅色主题,并且它已经在使用浅色主题,那没问题。(如果您将系统设置为深色主题,您可以通过查看它是否停止崩溃来测试这一点,假设您的应用主题是 DayNight 主题)

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

因此,我假设您的onDataChange回调要么在布局(和绑定)破坏和重新创建之间到达,要么整个 Fragment 被破坏并且回调只是调用一个binding永远不会恢复的变量。


最简单的解决方法就是不设置binding为空。让它lateinit像 Emmanuel 所说的那样,每次onCreateView调用它都会被初始化/覆盖。onCreateView如果回调更新旧的绑定布局,这很酷,新的无论如何都会要求更新

只要确保您正在清理您正在设置的事件侦听器(databaseReferenceStates如果需要) - 如果这是您清除绑定的原因onDestroyView,则侦听器仍然具有对包含该变量的片段的引用,您可以最终将死者保留在内存中(否则你可以只是 null-check binding


推荐阅读