I have a RecyclerView in my Fragment and two themes in app: Day, Night and System Default.
There is a strange problem that causes a NullPointerException. If I switch the theme to night and exit the application, and then enter it again, then a NullPointerException crashes and the application will not open again until I delete it from the phone or emulator. However, if I stay on the light theme all the time and close and open the application again, then everything will be fine.
Code for Fragment:
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()
}
ThemePreferences 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()
RecyclerView in .xml code:
<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" />
and also code from 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
}
Your crash is coming from that onDataChange
callback - you're calling getStates
(after binding
has been set) but by the time the results come back and onDataChange
tries to access binding
, it's null again.
If I had to guess, when you call checkTheme
and it calls applyDayNight
, that probably does nothing if the activity is already using the theme you're applying. So if you're setting a light theme, and it's already using a light theme, no problem. (You could test this by seeing if it stops crashing if you set the system to a dark theme, assuming your app theme is a DayNight one)
But if it needs to change to a dark theme, that means recreating the Activity
and Fragment
. I don't know the specifics of what gets recreated now, but at the very least you're probably going to be recreating your view layout with the new theme. Which means the layout is getting destroyed, which means onDestroyView
is getting called - and in there, you're setting binding
to null
So I'm assuming your onDataChange
callback is either arriving between layout (and binding) destruction and recreation, or the whole Fragment is getting destroyed and the callback is just calling a binding
variable that's never getting restored.
Easiest fix is just to not set binding
to null. Make it lateinit
like Emmanuel says, and it'll get initialised/overwritten every time onCreateView
is called. If the callback updates an old binding layout, it's cool, the new one will ask for an update in onCreateView
anyway
Just make sure you're cleaning up the event listeners you're setting on databaseReferenceStates
if you need to - if that's the reason you're clearing the binding in onDestroyView
, the listener still has a reference to the fragment that holds that variable, and you could end up keeping dead ones in memory (otherwise you could just null-check binding
)