dev.farmer
dev.farmer

Reputation: 2779

Android Jetpack Navigation Fragment show again and again

I am developing an android app using Jetpack library:

Actually, I am familiar with MVP pattern.

I am trying to study MVVP pattern (Databinding and Jetpack ViewModel)

I have 2 fragments (A and B).

import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs

@AndroidEntryPoint
class AFragment {

    private val viewModel: AViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.viewModel = viewModel

    with(binding) {
        button.setOnClickListener {
            [email protected]()
        }
    }

    viewModel.result.observe(viewLifecycleOwner) { result ->
        findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
    }
  }
}

And here is AViewModel:

@HiltViewModel
class AViewModel @Inject constructor(): ViewModel() {

    private val _result: MutableLiveData<Int> = MutableLiveData()
    val result: LiveData<Int>
        get() = _result

    fun doAction() {
        _result.postValue(SOME_ACTION_RESULT)
    }
}

It shows BFragment correctly. But If I touch Back Button on BFragment, it still shows BFragment. Actually, It went to back AFragment, but it comes again to BFragment.

When I touch Back Button on BFragment,

  1. AFragment is started again (I checked onViewCreated() is called again)
  2. Below observe code is called again:
viewModel.result.observe(viewLifecycleOwner) { result ->
    findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
}

Why this code is called again?

And do I write code correctly?

What is the best practice?


Now, I found a solution.

In AFragment:

viewModel.result.observe(viewLifecycleOwner) { result ->
    if (result != null) {
        findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
        viewModel.resetResult()
    }
}

and In AViewModel:

fun resetResult() {
    _result.postValue(null)
}

With this code, it works fine.

Yes... But I don't like this code...

It's... so weird...

I don't know what is the best practice...

Upvotes: 1

Views: 152

Answers (1)

kiroglue
kiroglue

Reputation: 376

the problem is related with livedata and fragment lifecycle.

AFragment and AViewModel lives when you move to FragmentB, but view in AFragment detach and attach when you move and come back. It means onViewCreated() called every time when you touch Back button on BFragment. As a result, AFragment start to observe AViewModel which has already valid data with its _result livedata.

You should separate uidata and events in livedatas. Easiest solution is SingleEventLiveData implementation and use it.

  open class Event<out T>(private val content: T) {

  var hasBeenHandled = false
    private set // Allow external read but not write

  /**
   * Returns the content and prevents its use again.
  */
  fun getContentIfNotHandled(): T? {
    return if (hasBeenHandled) {
      null
    } else {
      hasBeenHandled = true
      content
    }
  }

in viewmodel:

private val _result: MutableLiveData<Event<Int>> = MutableLiveData()
val result: LiveData<Event<Int>>
    get() = _result

fun doAction() {
    _result.postValue(Event(5))
}

how to observe:

viewModel.result.observe(viewLifecycleOwner) { result ->
result.getContentIfNotHandled()?.let {
    findNavController().navigate(R.id.action_fragment_a_to_fragment_b)
}

}

sources: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

How to create LiveData which emits a single event and notifies only last subscribed observer?

Upvotes: 1

Related Questions