夜间模式下 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,您可以通过查看它是否停止崩溃来测试它)
但如果需要更改为深色主题,则意味着重新创建 Activity
和 Fragment
。我不知道现在重新创建的具体内容,但至少您可能会使用新主题重新创建视图布局。这意味着布局被破坏,这意味着 onDestroyView
被调用 - 在那里,您将 binding
设置为 null
所以我假设你的 onDataChange
回调要么到达布局(和绑定)销毁和重新创建之间,要么整个 Fragment 被销毁并且回调只是调用一个 binding
变量那永远不会恢复。
最简单的解决方法就是不将 binding
设置为空。像 Emmanuel 说的那样让它成为 lateinit
,每次调用 onCreateView
时它都会得到 initialised/overwritten。如果回调更新了一个旧的绑定布局,那很好,新的会在 onCreateView
中要求更新无论如何
如果需要,请确保清理在 databaseReferenceStates
上设置的事件侦听器 - 如果这就是您清除 onDestroyView
中绑定的原因,侦听器仍然有对保存该变量的片段的引用,你最终可能会在内存中保留死变量(否则你可以只进行空检查binding
)
我认为您的问题与主题本身无关,而与 fragment/activity 生命周期有关。如果你打开,不要保持活动和后台然后恢复应用程序它会崩溃。
我认为问题在于您泄露了带有 addValueEventListener
的片段。由于您将其实现为匿名 class,因此您无法在 onStop
上将其删除,因此您的片段会泄漏,因此会出现空指针异常,因为视图已被破坏。
我的 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,您可以通过查看它是否停止崩溃来测试它)
但如果需要更改为深色主题,则意味着重新创建 Activity
和 Fragment
。我不知道现在重新创建的具体内容,但至少您可能会使用新主题重新创建视图布局。这意味着布局被破坏,这意味着 onDestroyView
被调用 - 在那里,您将 binding
设置为 null
所以我假设你的 onDataChange
回调要么到达布局(和绑定)销毁和重新创建之间,要么整个 Fragment 被销毁并且回调只是调用一个 binding
变量那永远不会恢复。
最简单的解决方法就是不将 binding
设置为空。像 Emmanuel 说的那样让它成为 lateinit
,每次调用 onCreateView
时它都会得到 initialised/overwritten。如果回调更新了一个旧的绑定布局,那很好,新的会在 onCreateView
中要求更新无论如何
如果需要,请确保清理在 databaseReferenceStates
上设置的事件侦听器 - 如果这就是您清除 onDestroyView
中绑定的原因,侦听器仍然有对保存该变量的片段的引用,你最终可能会在内存中保留死变量(否则你可以只进行空检查binding
)
我认为您的问题与主题本身无关,而与 fragment/activity 生命周期有关。如果你打开,不要保持活动和后台然后恢复应用程序它会崩溃。
我认为问题在于您泄露了带有 addValueEventListener
的片段。由于您将其实现为匿名 class,因此您无法在 onStop
上将其删除,因此您的片段会泄漏,因此会出现空指针异常,因为视图已被破坏。