Шаблон MVVM и startActivity

Недавно я решил более внимательно рассмотреть новые компоненты архитектуры Android, выпущенные Google, особенно используя класс, ориентированный на жизненный цикл ViewModel, на архитектуру MVVM и LiveData.

Пока я имею дело с одним действием или с одним фрагментом, все в порядке.

Однако я не могу найти подходящее решение для обработки переключения активности. Скажем, ради краткого примера, что у Activity A есть кнопка для запуска Activity B.

Где будет обрабатываться функция startActivity()?

Следуя шаблону MVVM, логика clickListener должна находиться в ViewModel. Тем не менее, мы хотим избежать ссылок на деятельность в ней. Таким образом, передача контекста в ViewModel не является вариантом.

Я сузил несколько вариантов, которые кажутся "ОК", но не смог найти правильный ответ "здесь, как это сделать".

Вариант 1. Перечислите в ViewModel перечисление значений для возможной маршрутизации (ACTIVITY_B, ACTIVITY_C). Соедините это с LiveData. Эта активность будет наблюдать за этой LiveData, и когда ViewModel решит, что ACTIVITY_C должен быть запущен, это будет просто postValue (ACTIVITY_C). Затем активность может вызвать функцию startActivity().

Вариант 2: обычный шаблон интерфейса. Тот же принцип, что и вариант 1, но Activity будет реализовывать интерфейс. Я чувствую себя немного более с этим, хотя.

Вариант 3: вариант обмена сообщениями, например, Отто или аналогичный. ViewModel отправляет трансляцию, активность выбирает ее и запускает то, что ей нужно. Только проблема с этим решением заключается в том, что по умолчанию вы должны поместить регистр/регистрацию этой широковещательной передачи внутри ViewModel. Так что не помогает.

Вариант 4: наличие большого класса маршрутизации, где-то, как одиночный или похожий, который можно вызвать для отправки соответствующей маршрутизации в любую активность. В конце концов через интерфейс? Таким образом, каждое действие (или BaseActivity) будет реализовывать

IRouting { void requestLaunchActivity(ACTIVITY_B); }

Этот метод меня немного беспокоит, когда ваше приложение начинает иметь много фрагментов/действий (потому что класс Routing станет более гуманным)

Так что это. Это мой вопрос. Как вы, парни, справляетесь с этим? Вы идете с вариантом, о котором я не думал? Какой вариант вы считаете наиболее актуальным и почему? Каков рекомендуемый подход Google?

PS: Ссылки, которые меня не достали 1 - Android ViewModel вызов Способы действий 2 - Как запустить активность из простого класса непроактивности java?

Ответ 1

NSimon, это замечательно, что вы начинаете использовать AAC.

Я написал issue в aac's-github до этого.

Есть несколько способов сделать это.

В одном решении будет использоваться

WeakReference в NavigationController, который содержит контекст Activity. Это распространенный шаблон, используемый для обработки содержимого, связанного с контекстом, внутри ViewModel.

Я сильно отклоняю это по нескольким причинам. Во-первых: это обычно означает, что вы должны хранить ссылку на свой навигационный контроллер, который исправляет утечку контекста, но не решает архитектуру вообще.

Лучший способ (в моем oppinion) использовать LiveData, который является жизненным циклом, и может делать все необходимые материалы.

Пример:

class YourVm : ViewModel() { 

    val uiEventLiveData = SingleLiveData<Pair<YourModel, Int>>()
    fun onClick(item: YourModel) {
        uiEventLiveData.value = item to 3 // can be predefined values
    }
}

После этого вы можете прослушивать внутри своего просмотра изменения.

class YourFragmentOrActivity { 
     //assign your vm whatever
     override fun onActivityCreated(savedInstanceState: Bundle?) { 
        var context = this
        yourVm.uiEventLiveData.observe(this, Observer {
            when (it?.second) {
                1 -> { context.startActivity( ... ) }
                2 -> { .. } 
            }

        })
    }
}

Позаботьтесь о том, чтобы ive использовала модифицированную MutableLiveData, потому что иначе она всегда будет выдавать последний результат для новых наблюдателей, что приводит к плохому поведению. Например, если вы измените активность и вернитесь, она закончится в цикле.

class SingleLiveData<T> : MutableLiveData<T>() {

    private val mPending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<T>) {

        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        }

        // Observe the internal MutableLiveData
        super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(t: T?) {
        mPending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }

    companion object {
        private val TAG = "SingleLiveData"
    }
}

Почему эта попытка лучше, чем использование WeakReferences, интерфейсов или любого другого решения?

Поскольку это событие разделяет логику пользовательского интерфейса с бизнес-логикой. Также возможно иметь несколько наблюдателей. Он заботится о жизненном цикле. Он ничего не течет.

Вы также можете решить эту проблему, используя RxJava вместо LiveData, используя PublishSubject. (addToтребует RxKotlin)

Позаботьтесь о том, чтобы не утечка подписки, выпуская ее в onStop().

class YourVm : ViewModel() { 
   var subject : PublishSubject<YourItem>  = PublishSubject.create();
}

class YourFragmentOrActivityOrWhatever {
    var composite = CompositeDisposable() 
    onStart() { 
         YourVm.subject 
             .subscribe( { Log.d("...", "Event emitted $it") }, { error("Error occured $it") }) 
               .addTo(compositeDisposable)         
       }   
       onStop() {
         compositeDisposable.clear()
       }
    }

Также следите за тем, чтобы ViewModel привязана к Activity OR or Fragment. Вы не можете поделиться ViewModel между несколькими действиями, поскольку это сломает "Livecycle-Awareness".

Если вам нужно, что ваши данные сохраняются с помощью базы данных, например room или обмениваться данными с помощью посылок.

Ответ 2

Вы должны вызывать startActivity из активности, а не из viewmodel. Если вы хотите открыть его из viewmodel, вам нужно создать liveata в viewmodel с некоторым параметром навигации и наблюдать за liveata внутри действия

Ответ 3

Вы можете расширить вашу ViewModel из AndroidViewModel, которая имеет ссылку на приложение, и начать действие, используя этот контекст.