2

I have an app structured with one MainActivity and several fragments which are communicated with by some ImageButtons in this activity. In one fragment (UnreviewedFragment), I have a RecyclerView whose Adapter can be changed to display different information. This functions perfectly well when first booting into this fragment.

However, when I enter the EditFragment using the following code in MainActivity.kt:

addButton.setOnClickListener {
 supportFragmentManager.commit {
 replace<EditItemFragment>(R.id.fragmentContainerView, "edit")
 }
 reviewedButton.isEnabled = true
 unreviewedButton.isEnabled = true
}

And then return to either view with the following code:

unreviewedButton.setOnClickListener {
 Log.d("MainActivity", "Unreviewed Button pressed! Is it enabled: ${unreviewedButton.isEnabled}")
 if (supportFragmentManager.findFragmentByTag("edit") != null) {
 supportFragmentManager.commit {
 replace<UnreviewedFragment>(R.id.fragmentContainerView, "restaurants")
 }
 }
 supportFragmentManager.executePendingTransactions()
 restaurantsFragment.setModeUnreviewed()
 unreviewedButton.isEnabled = false
 reviewedButton.isEnabled = true
}
reviewedButton.setOnClickListener {
 Log.d("MainActivity", "Reviewed Button pressed! Is it enabled: ${reviewedButton.isEnabled}")
 if (supportFragmentManager.findFragmentByTag("edit") != null) {
 supportFragmentManager.commit {
 replace<UnreviewedFragment>(R.id.fragmentContainerView, "restaurants")
 }
 }
 supportFragmentManager.executePendingTransactions()
 restaurantsFragment.setModeReviewed()
 unreviewedButton.isEnabled = true
 reviewedButton.isEnabled = false
}

The binding used in the setModeReviewed and setModeUnreviewed methods in the UnreviewedFragment suddenly become null. My logging statements and some debugging indicate that after the replace call, the bindings still have values in onViewCreated, but they seem to lose these values and become nulls when the transaction is finished and restaurantsFragment.setModeReviewed() is called in the onClickListener of the button.

My fragment's onCreateView function looks like this:

override fun onCreateView(
 inflater: LayoutInflater, container: ViewGroup?,
 savedInstanceState: Bundle?
 ): View? {
 Log.d("UnreviewedFragment", "onCreateView")
 // set view bindings
 // TODO try resetting with simple findviewbyid to see if binding null exception leaves
 _binding = FragmentUnreviewedBinding.inflate(layoutInflater)
 Log.d("UnreviewedFragment", "binding is: $_binding")
 Log.d("UnreviewedFragment", "Inflated! Viewmodel: $viewModel")
 reviewedAdapter = ReviewedItemAdapter()
 unReviewedAdapter = UnreviewedItemAdapter()
 binding.unreviewedRecyclerView.adapter = unReviewedAdapter
 binding.unreviewedRecyclerView.layoutManager = LinearLayoutManager(activity?.applicationContext!!)
 viewModel.restaurantList.observe(viewLifecycleOwner, Observer { restaurants ->
 restaurants?.let { unReviewedAdapter.setRestaurants(it) }
 restaurants?.let { reviewedAdapter.setRestaurants(it) }
 })
 val view = binding.root
 return view
 }

And the code to exit the EditItemFragment looks like this:

override fun onCreateView(
 inflater: LayoutInflater, container: ViewGroup?,
 savedInstanceState: Bundle?
 ): View? {
 // Inflate the layout for this fragment
 _binding = FragmentEditItemBinding.inflate(layoutInflater)
 // set up listeners
 binding.submitButton.setOnClickListener {
 viewModel.insert(Restaurant(
 binding.restaurantNameEdit.text.toString(),
 binding.restaurantAddressEdit.text.toString(),
 binding.restaurantWebsiteEdit.text.toString(),
 "dragon_hotpot_outside", // TODO fix this later
 binding.priceBar.rating.toInt(),
 null,
 Date(),
 null
 ))
 }
 val view: View = binding.root
 return view
 }
Taslim Oseni
6,34910 gold badges51 silver badges74 bronze badges
asked Oct 22, 2025 at 11:32

1 Answer 1

1

This is a classic stale fragment instance + view-binding lifecycle combo.

Here's what's happening:

The line `replace<UnreviewedFragment>(..., "restaurants")` creates a new instance of UnreviewedFragment.kt and destroys the old one. When that old instance is destroyed, its `onDestroyView()` function is called, and that sets its `binding` to null. When you call restaurantsFragment.setModeReviewed(), it most likely calls the old instance, which already has its binding variable set to null.

One quick way to fix this problem is to use a shared viewModel. Something like this:

class RestaurantsUiViewModel : ViewModel() {
 enum class Mode { Reviewed, Unreviewed }
 private val _mode = MutableStateFlow(Mode.Unreviewed)
 val mode: StateFlow<Mode> = _mode.asStateFlow()
 fun setMode(m: Mode) { _mode.value = m }
}

Then, in your activity, you can have this:

private val uiVm: RestaurantsUiViewModel by viewModels()
unreviewedButton.setOnClickListener {
 showUnreviewedFragmentIfNeeded()
 uiVm.setMode(RestaurantsUiViewModel.Mode.Unreviewed)
}
reviewedButton.setOnClickListener {
 showUnreviewedFragmentIfNeeded()
 uiVm.setMode(RestaurantsUiViewModel.Mode.Reviewed)
}
private fun showUnreviewedFragmentIfNeeded() {
 val existing = supportFragmentManager.findFragmentByTag("restaurants")
 if (existing == null || existing !is UnreviewedFragment) {
 supportFragmentManager.commit {
 setReorderingAllowed(true)
 replace(R.id.fragmentContainerView, UnreviewedFragment(), "restaurants")
 }
 supportFragmentManager.executePendingTransactions()
 }
}

Finally, your UnreviewedFragment.kt can look like this:

private var _binding: FragmentUnreviewedBinding? = null
private val binding get() = _binding!!
private val uiVm: RestaurantsUiViewModel by activityViewModels()
override fun onCreateView(
 inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
 _binding = FragmentUnreviewedBinding.inflate(inflater, container, false)
 binding.unreviewedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
 binding.unreviewedRecyclerView.adapter = unReviewedAdapter
 return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 viewLifecycleOwner.lifecycleScope.launch {
 viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
 uiVm.mode.collect { mode ->
 when (mode) {
 RestaurantsUiViewModel.Mode.Reviewed -> {
 binding.unreviewedRecyclerView.adapter = reviewedAdapter
 }
 RestaurantsUiViewModel.Mode.Unreviewed -> {
 binding.unreviewedRecyclerView.adapter = unReviewedAdapter
 }
 }
 }
 }
 }
}
override fun onDestroyView() {
 super.onDestroyView()
 _binding = null
}

With this approach, you are guaranteed that the binding can only be touched if (and only if) the view exists.

answered Oct 22, 2025 at 12:35
Sign up to request clarification or add additional context in comments.

This solved it! Thank you so much. Out of curiosity, what is the purpose of MutableStateFlow here?
MutableStateFlow is a Kotlin flow type that can hold a single value that can change. If you're already familiar with livedata, it's basically the same thing, except that it's for coroutines.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.