Kotlin ViewModel onchange вызывается несколько раз при возвращении из фрагмента (с использованием реализации жизненного цикла)

Я работаю с архитектурой MVVM.

Код

Когда я нажимаю кнопку, запускается метод orderAction. Он просто публикует перечисление (будет добавлена ​​дальнейшая логика).

Модель представления

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

Фрагмент наблюдает за объектом LiveData и вызывает метод, открывающий новый фрагмент. Здесь я использую навигатор, но не думаю, что подробности о нем полезны в данном контексте. Обратите внимание, что я использую viewLifecycleOwner.

Фрагмент

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

Проблема

Когда я закрываю открытый фрагмент (используя findNavController().navigateUp()), немедленно вызывается Observe.onChanged из DashboardFragment, и фрагмент снова открывается.

Я уже проверил этот вопрос и попытался все предлагаемые решения в упомянутой ссылке (как вы можно увидеть в закомментированном коде). Сработало только это решение, но оно не очень элегантно и заставляет меня каждый раз выполнять эту проверку.

Хотелось бы попробовать более солидное и оптимальное решение.

Имейте в виду, что в этом потоке не было реализации жизненного цикла.


person Link 88    schedule 19.05.2020    source источник


Ответы (2)


Вот как работает LiveData, это держатель значения, он содержит последнее значение.

Если вам нужно, чтобы ваши объекты потреблялись, чтобы действие запускалось только один раз, подумайте о том, чтобы обернуть ваш объект в Consumable, например

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

затем вы определяете свои LiveData как

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

то в вашем наблюдателе вы бы сделали

private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
        it?.consume { action ->
            when (action) {
                DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
                DashboardUserViewModel.Action.ORDER -> TODO()
                DashboardUserViewModel.Action.TOILETTE -> TODO()
            }
        }
}
person Francesc    schedule 19.05.2020
comment
Оно работало завораживающе! Это решение элегантное, оптимальное, многоразовое и занудное. Это то, что я искал. Благодарю вас! - person Link 88; 20.05.2020

ОБНОВИТЬ

Нашла другую и по-прежнему полезную реализацию того, на что ответила Фрэнсис здесь. Взглянем

person Link 88    schedule 20.05.2020
comment
Это то, что использует Google, поэтому я бы использовал это (оно несколько неофициально известно как SingleLiveData, которое не запускается каждый раз, потому что liveData полезен только как держатель значения для пользовательского интерфейса и ничего еще, несмотря на то, что многие вещи поддерживают его без причины :) - person Martin Marconcini; 20.05.2020
comment
Да, я начал с решения Francesc, а затем перешел на решение Google. Но я оставлю решение Francesc в качестве официального ответа, так как он подтолкнул меня к этому подходу Google. - person Link 88; 20.05.2020