androidkotlinmvvmfragment-oncreateviewlifecycleowner

Kotlin ViewModel onchange gets called multiple times when back from Fragment (using Lifecycle implementation)


I am working with the MVVM architecture.

The code

When I click a button, the method orderAction is triggered. It just posts an enum (further logic will be added).

ViewModel

class DashboardUserViewModel(application: Application) : SessionViewModel(application) {

    enum class Action {
        QRCODE,
        ORDER,
        TOILETTE
    }

    val action: LiveData<Action>
        get() = mutableAction
    private val mutableAction = MutableLiveData<Action>()

    init {
    }

    fun orderAction() {
        viewModelScope.launch(Dispatchers.IO) {
            // Some queries before the postValue
            mutableAction.postValue(Action.QRCODE)    
        }
    }
}

The fragment observes the LiveData obj and calls a method that opens a new fragment. I'm using the navigator here, but I don't think that the details about it are useful in this context. Notice that I'm using viewLifecycleOwner

Fragment

class DashboardFragment : Fragment() {

    lateinit var binding: FragmentDashboardBinding
    private val viewModel: DashboardUserViewModel by lazy {
        ViewModelProvider(this).get(DashboardUserViewModel::class.java)
    }

    private val observer = Observer<DashboardUserViewModel.Action> {
        // Tried but I would like to have a more elegant solution
        //if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED)
            it?.let {
                when (it) {
                    DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
                    DashboardUserViewModel.Action.ORDER -> TODO()
                    DashboardUserViewModel.Action.TOILETTE -> TODO()
                }
            }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentDashboardBinding.inflate(inflater, container, false)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this

        viewModel.action.observe(viewLifecycleOwner, observer)

        // Tried but still having the issue
        //viewModel.action.reObserve(viewLifecycleOwner, observer)

        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        // Tried but still having the issue
        //viewModel.action.removeObserver(observer)
    }

    private fun navigateToQRScanner() {
        log("START QR SCANNER")
        findNavController().navigate(LoginFragmentDirections.actionLoginToPrivacy())
    }
}

The problem

When I close the opened fragment (using findNavController().navigateUp()), the Observe.onChanged of DashboardFragment is immediately called and the fragment is opened again.

I have already checked this question and tried all the proposed solutions in the mentioned link (as you can see in the commented code). Only this solution worked, but it's not very elegant and forces me to do that check every time.

I would like to try a more solid and optimal solution.

Keep in mind that in that thread there was no Lifecycle implementation.


Solution

  • That's how LiveData works, it's a value holder, it holds the last value.

    If you need to have your objects consumed, so that the action only triggers once, consider wrapping your object in a Consumable, like this

    class ConsumableValue<T>(private val data: T) {
    
        private val consumed = AtomicBoolean(false)
    
        fun consume(block: ConsumableValue<T>.(T) -> Unit) {
            if (!consumed.getAndSet(true)) {
                block(data)
            }
        }
    }
    

    then you define you LiveData as

    val action: LiveData<ConsumableValue<Action>>
        get() = mutableAction
    private val mutableAction = MutableLiveData<ConsumableValue<Action>>()
    

    then in your observer, you'd do

    private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
            it?.consume { action ->
                when (action) {
                    DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
                    DashboardUserViewModel.Action.ORDER -> TODO()
                    DashboardUserViewModel.Action.TOILETTE -> TODO()
                }
            }
    }