Link 88
Link 88

Reputation: 563

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.

Upvotes: 1

Views: 3073

Answers (3)

Pramod Moolekandathil
Pramod Moolekandathil

Reputation: 589

The issue happens because LiveData always post the available data to the observer if any data is readily available. Afterwords it will post the updates. I think it is the expected working since this behaviour has not been fixed even-though bug raised in issue tracker. However there are many solutions suggested by developers in SO, i found this one easy to adapt and actually working just fine.

Solution

viewModel.messagesLiveData.observe(viewLifecycleOwner, {
        if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
            //Do your stuff
        }
    })  

Upvotes: 3

Link 88
Link 88

Reputation: 563

UPDATE

Found a different and still useful implementation of what Frances answered here. Take a look

Upvotes: 0

Francesc
Francesc

Reputation: 29280

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()
            }
        }
}

Upvotes: 2

Related Questions