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

Эта статья сопровождается видеоуроком, охватывающим многие из тех же идей здесь.

Вполне вероятно, что наиболее частый вопрос, который мне задают во время моих прямых вопросов и ответов, выглядит примерно так:

MVVM против MVP / MVC?

Когда мне задают этот вопрос, я сразу же подчеркиваю, что ни одна архитектура графического интерфейса не работает хорошо во всех ситуациях. Почему, спросите вы? Лучшая архитектура (или, по крайней мере, хороший выбор) для данного приложения сильно зависит от предъявляемых требований. Давайте вкратце задумаемся, что на самом деле означает слово требования:

  • Насколько сложен ваш пользовательский интерфейс? Простой пользовательский интерфейс обычно не требует сложной логики для его координации; тогда как сложный пользовательский интерфейс может потребовать обширной логики и детального управления для бесперебойной работы.
  • Насколько вы заботитесь о тестировании? Вообще говоря, классы, которые тесно связаны с фреймворками и ОС (особенно пользовательский интерфейс), требуют дополнительной работы для тестирования.
  • Насколько много повторного использования и абстракции вы хотите продвигать? Что делать, если вы хотите использовать серверную часть, домен и даже логику представления вашего приложения на разных платформах?
  • Вы по натуре прагматик, перфекционист, ленивый или все вышеперечисленное в разное время и в разных ситуациях?

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

ВЫ НЕ ДОЛЖНЫ СМОТРЕТЬ КЛАССЫ ПРОСМОТРА

Для моих друзей по английскому языку, которые не умеют читать по-английски: «ВЫ НЕ МОЖЕТЕ СМОТРЕТЬ ПРОСМОТР КЛАССОВ»

Помимо использования имени ViewModel (которое само по себе может сбивать с толку в зависимости от того, как вы его используете), есть одно железное правило архитектуры MVVM, которое я не видел оспариваемым, заключается в том, что вы никогда не можете ссылаться на View из ViewModel. Итак, первая область путаницы может возникнуть из-за этого слова «ссылка», которое я повторю, используя несколько разных уровней жаргона:

  • Ваши ViewModels не могут иметь никаких ссылок (переменные-члены, свойства, изменяемые / неизменяемые поля) на какие-либо представления.
  • Ваши ViewModels не могут зависеть от каких-либо представлений
  • Ваши ViewModels не могут напрямую взаимодействовать с вашими представлениями

На платформе Android причина этого правила не просто в том, что его нарушать плохо, потому что кто-то, кто, кажется, разбирается в архитектуре программного обеспечения, сказал вам, что это плохо. При использовании класса ViewModel из компонентов архитектуры (который предназначен для того, чтобы его экземпляр сохранялся дольше, чем жизненный цикл фрагмента / действия при необходимости), ссылка на представление запрашивает СЕРЬЕЗНАЯ УТЕЧКА ПАМЯТИ.

Что касается того, почему MVVM в целом не допускает таких ссылок, цель состоит в том, чтобы гипотетически упростить тестирование и запись как View, так и ViewModel. Другие могут также указать, что это способствует повторному использованию ViewModels, но именно здесь что-то не работает с этим шаблоном.

ViewLogic + ViewModel или View + ViewModelController?

Когда я сказал «сломать» в предыдущем разделе, я не имел в виду, что шаблон буквально ломается. Я имею в виду, что он разбивается на (по крайней мере) два разных подхода, которые имеют очень разные внешний вид, преимущества и последствия. Давайте рассмотрим эти два подхода, и когда вы, возможно, пожелаете предпочесть один другому.

Первый подход: уделите приоритетное внимание многоразовым моделям просмотра

Насколько я могу судить (мы попадаем в мутную воду), большинство людей, реализующих MVVM, ставят своей целью способствовать повторному использованию ViewModels, чтобы их можно было повторно использовать в течение n количества разные просмотры (соотношение "многие к одному"). Проще говоря, это повторное использование может быть достигнуто двумя способами:

  • Не ссылаясь на конкретный View. Надеюсь, на данный момент это не новость для вас.
  • зная как можно меньше деталей пользовательского интерфейса в целом.

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

class NoteViewModel(val repo: NoteRepo): ViewModel(){
    //Note: you may also publish data to the View via Databinding, RxJava Observables, and other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with AAC
    val noteState: MutableLiveData<Note>()
    //...
    fun handleEvent(event: NoteEvent) {
        when (event) {
            is NoteEvent.OnStart -> getNote(event.noteId)
            //...
        }
    }
    private fun getNote(noteId: String){
        noteState.value = repo.getNote(noteId)
    }
}

Хотя это очень упрощенный пример, дело в том, что единственное, что эта конкретная модель ViewModel предоставляет публично (кроме функции handleEvent), - это простой объект Note:

data class Note(val creationDate:String,
                val contents:String,
                val imageUrl: String,
                val creator: User?)

Теперь, с этим конкретным подходом, ViewModel хорошо и действительно отделена не только от конкретного View, но и от деталей и, соответственно, логики представления любого конкретного View. Если то, что я говорю, все еще кажется расплывчатым, я обещаю, что это станет ясно, когда я опишу другой подход.

Хотя мой предыдущий заголовок «ViewLogic + ViewModel…» не предназначен для использования или серьезного отношения; Я указываю на тот факт, что, имея очень разделенные и повторно используемые модели представления, мы теперь зависим от самого представления, чтобы выяснить, как именно визуализировать / привязать этот объект Note на экране. Некоторым из нас не нравится заполнять классы представлений логикой.

Здесь все становится очень запутанным и зависит от требований проекта. Я не говорю, что классы View заполняются такой логикой, как…:

private fun observeViewModel() {
    viewModel.notes.observe(
        viewLifecycleOwner,
        Observer { notes: List<Note> ->
            if (notes.isEmpty()) showEmptyState()
            else showNoteList(notes)
        }
    )
   //..
}

всегда плохо (в конце концов, логическая сложность возникает редко), но в лучшем случае это требует тестирования (тестирования чего-то, что тесно связано с ОС); в худшем случае - это неприменение того, что я считаю золотым принципом любой хорошей архитектуры: Разделение проблем.

Мое личное мнение состоит в том, что следует применять разделение интересов в очень высокой степени; но не сомневайтесь, что множество приложений дойной коровы написано людьми, которые не имеют ни малейшего представления о том, что это означает. В любом случае подход, который мы обсудим далее, хотя и имеет свои собственные побочные эффекты, снова удаляет логику представления из представления; ну, во всяком случае, большую часть.

Второй подход: скромный взгляд, модель просмотра, не имеющая отношения к контролю

Иногда отсутствие детального контроля над вашими представлениями (что является следствием приоритета повторного использования ViewModels) на самом деле отстой. Чтобы вызвать у меня еще меньший энтузиазм по поводу применения предыдущего подхода к моей повседневной работе с Android, я обнаружил, что мне часто не нужно повторно использовать ViewModel .

По иронии судьбы, «слишком много абстракции» - это обычная критика MVP по сравнению с MVVM.

С учетом вышесказанного, нельзя просто добавить ссылку обратно в ViewModel, чтобы восстановить этот мелкозернистый контроль над View. В основном это были бы утечки памяти MVP + (при условии, что вы все еще используете ViewModel из AAC).

Альтернативой является создание ваших ViewModels таким образом, чтобы они содержали почти все поведение, состояние и логику представления данного View. Представление, конечно, все равно должно быть привязано к ViewModel, но достаточно подробностей о View присутствует в ViewModel, чтобы функции View были сведены к одному вкладышу (за небольшими исключениями). В соглашении об именах Мартина Фаулера это известно как пассивный просмотр / экран. Более широко используемое название этого подхода - Шаблон скромных объектов.

Чтобы достичь этого, вы должны иметь в своей модели ViewModel наблюдаемое поле (как бы вы этого ни добились; привязка данных, Rx, LiveData и т. Д.) Для каждого элемента управления или виджета, присутствующего в представлении:

class UserViewModel(
    val repo: IUserRepository,
){

    //The actual data model is kept private to avoid unwanted tampering
    private val userState = MutableLiveData<User>()

    //Control Logic
    internal val authAttemptState = MutableLiveData<Unit>()
    internal val startAnimation = MutableLiveData<Unit>()

    //UI Binding
    internal val signInStatusText = MutableLiveData<String>()
    internal val authButtonText = MutableLiveData<String>()
    internal val satelliteDrawable = MutableLiveData<String>()

    private fun showErrorState() {
        signInStatusText.value = LOGIN_ERROR
        authButtonText.value = SIGN_IN
        satelliteDrawable.value = ANTENNA_EMPTY
    }
    //...
}

Впоследствии View все равно нужно будет подключить себя к ViewModel, но функции, необходимые для этого, станут тривиально простыми для написания:

class LoginView : Fragment() {

    private lateinit var viewModel: UserViewModel
    //...
    
    //Create and bind to ViewModel
    override fun onStart() {
        super.onStart()
        viewModel = ViewModelProviders.of(
        //...   
        ).get(UserViewModel::class.java)

        //start background anim
        (root_fragment_login.background as AnimationDrawable).startWithFade()

        setUpClickListeners()
        observeViewModel()

        viewModel.handleEvent(LoginEvent.OnStart)
    }

    private fun setUpClickListeners() {
      //...
    }

    private fun observeViewModel() {
        viewModel.signInStatusText.observe(
            viewLifecycleOwner,
            Observer {
                //"it" is the value of the MutableLiveData object, which is inferred to be a String automatically
                lbl_login_status_display.text = it
            }
        )

        viewModel.authButtonText.observe(
            viewLifecycleOwner,
            Observer {
                btn_auth_attempt.text = it
            }
        )

        viewModel.startAnimation.observe(
            viewLifecycleOwner,
            Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName)
                )
                (imv_antenna_animation.drawable as AnimationDrawable).start()
            }
        )

        viewModel.authAttemptState.observe(
            viewLifecycleOwner,
            Observer { startSignInFlow() }
        )

        viewModel.satelliteDrawable.observe(
            viewLifecycleOwner,
            Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(it, "drawable", activity?.packageName)
                )
            }
        )
    }

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

Как вы, вероятно, заметили, мы, вероятно, не собираемся повторно использовать эту ViewModel где-либо еще. Кроме того, наше представление стало достаточно скромным (в зависимости от ваших стандартов и предпочтений в отношении покрытия кода) и очень простым в написании.

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

Предпочтения и требования

Цель этой статьи состояла в том, чтобы рассмотреть два разных подхода, которые разработчик может использовать с точки зрения построения архитектуры графического интерфейса пользователя в стиле MVVM на платформе Android (с некоторыми перенесенными на другие платформы).

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

  • Должно ли представление наблюдать поле для каждого отдельного виджета / элемента управления, которое оно представляет, или оно должно наблюдать одно поле, которое публикует одну модель, чтобы каждый раз заново отображать все представление?
  • Может быть, мы могли бы избежать необходимости делать наши ViewModels один-к-одному, сохраняя наши представления как скромные объекты, просто добавив в смесь что-то вроде Presenter или Controller?

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

  • Поиграйте с несколькими подходами, пока не найдете тот, который вам больше нравится. Лучше всего это сделать, создав приложение (оно может быть простым) в каждом стиле и посмотрев, что кажется правильным.
  • Поймите, что помимо предпочтений, разные стили будут стремиться подчеркивать разные преимущества в обмен на разные недостатки. В конце концов, вы сможете сделать правильный выбор, основываясь на вашем понимании требований проекта, а не на слепой вере.

Если вы чему-то научились…

Учить больше

Основы программирования приложений на Kotlin и Android

Социальное

Https://www.instagram.com/wiseassbrand/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301 секс
«Http://wiseassblog.com/

Пожертвовать

Https://www.paypal.me/ryanmkay