Оригиналы этих постов можно почитать в тг канале НеКрутой Архитектор
Там набирается материал для будущих статей с сильным опережением

План:

? Функции обратного вызова (Callback)

Что делать в ситуации, когда мы вызываем функцию load,
а она хочет обратиться к вызывающему, то есть к нам?
Чтобы вызывающий выполнил какую‑то работу или чтобы передать ему значения спустя время или вообще несколько раз

Для решения таких задач есть всем известные (и не особо любимые), функции обратного вызова, ну или проще callback

? Реализация крайне простая:

  • Создаем интерфейс Callback, у которого есть метод, например call

  • Создаем реализацию этого интерфейса в которой выполняем нужную работу

  • Передаем в функцию load реализацию Callback

  • Внутри функции load, вызываем callback.call(), когда это необходимо

В современных языках, вместо написания интерфейсов, можно передавать сами функции или так называемые лямбда выражения

Пример на kotlin для наглядности

Пример в лоб для наглядности
interface Callback {
    fun call(result: String): Unit
}

fun load(callback: Callback) {
    callback.call("Loading...")
    // slow operation...
    callback.call("Finish")
}

fun main() {
    val callback: Callback = object : Callback {
        override fun call(result: String) = println(result)
    }
    load(callback)
}
Ну или тот же пример с использованием лямбда выражений
fun load(callback: (String) -> Unit) {
    callback("Loading...")
    // slow operation...
    callback("Finish")
}

fun main() {
    load(callback = { result -> println(result) })
}

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

? Паттерн Наблюдатель (Observer)

Как понятно из названия, с его помощью можно наблюдать за чем‑либо
Практически, это обычный Callback, только с дополнительной возможностью, отказаться от обратных вызовов

Более правильная формулировка: мы можем не просто передать callback, а подписаться на его вызовы и отписаться от них

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

Пример реализации Observer в лоб
class Loader {
    private var callback: ((String) -> Unit)? = null

    fun subscribe(callback: (String) -> Unit) {
        this.callback = callback
    }

    fun unsubscribe() {
        callback = null
    }

    fun load() {
        callback?.invoke("Loading...")
        // slow operation...
        callback?.invoke("Finish")
    }
}

fun main() {
    val loader = Loader()
    loader.subscribe(::println)
    loader.load()
    loader.unsubscribe()
}

Ну и развивая этот подход, хорошо бы иметь возможность, чтобы подписчиков могло быть много, а не один

Для этого достаточно хранить их в списке, а не просто в переменной
class Loader {
    private var callbacks = mutableListOf<(String) -> Unit>()

    fun subscribe(callback: (String) -> Unit) = callbacks.add(callback) 

    fun unsubscribe(callback: (String) -> Unit) = callbacks.remove(callback)

    private fun update(result: String) = callbacks.forEach { it(result) }

    fun load() {
        update("Loading...")
        // slow operation...
        update("Finish")
    }
}

fun main() {
    val loader = Loader()
    val printer: (String) -> Unit = ::println
    val logger: (String) -> Unit = { Log.d(it) }
    loader.subscribe(printer)
    loader.subscribe(logger)
    loader.load()
    loader.unsubscribe(printer)
    loader.unsubscribe(logger)
}

Выглядит уже хорошо, но мы бы не были архитекторами, если бы оставили это так

Использование данного подхода явно будет не в одном месте, и было бы хорошо не писать везде один и тот же код, а оформить его в отдельную сущность
Да и класс Loader выглядит перегруженным, хотя у него должен быть только один метод load

Создадим новую сущность Observable и перенесем в нее весь код связанный с подписками
Callback переименуем в Observer
И пока оставим создание Observable внутри нашего Loader

Создадим новую сущность Observable
class Observable {
    private var observers = mutableListOf<(T) -> Unit>()
    fun subscribe(observer: (T) -> Unit) = observers.add(observer) 
    fun unsubscribe(observer: (T) -> Unit) = observers.remove(observer)
    fun update(value: T) = observers.forEach { it(value) }
}

class Loader {
    val observable = Observable()

    fun load() {
        observable.update("Loading...")
        // slow operation...
        observable.update("Finish")
    }
}

fun main() {
    val loader = Loader()
    val observable = loader.observable
    val printer: (String) -> Unit = ::println
    val logger: (String) -> Unit = { Log.d(it) }
    observable.subscribe(printer)
    observable.subscribe(logger)
    loader.load()
    observable.unsubscribe(printer)
    observable.unsubscribe(logger)
}

Мы вынесли код Наблюдателя в отдельную сущность,
которую можем переиспользовать в других местах

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

Развитие именно этого паттерна привело к созданию таких вещей как реактивное программирование
В Android это RxJava, LiveData, Channel и Flow
Все они основаны на простых принципах наблюдателя ?

? MVVM (ViewModel)

Настало время разобраться, что же такое MVVM и чем он отличается от MVP

Presenter (Ведущий) — ведет View за ручку, говоря, что та должна делать или показывать
ViewModel (Представление модели) — показывает своим примером, что View должна показывать пользователю

Открытие века:

ViewModel из шаблона и ViewModel от Google (класс в android) это разные вещи
Класс ViewModel от Google, по официальной документации, место для логики
НЕ НАДО ТАК ДЕЛАТЬ

ViewModel из шаблона — это тончайшая прослойка между тупой View и умной Model
Далее говорим только о ней

ViewModel хранит данные о том, что сейчас должен видеть пользователь
View смотрит на эти данные и принимает соответствующий вид, который пользователь и увидит

«Смотрит» View, как раз благодаря паттерну Наблюдатель
Доработанному на хранение последнего значения, чтобы новая View сразу получала состояние, а не ждала обновления

Когда пользователь как либо взаимодействует с View, та тупо передает это событие на ViewModel
ViewModel может отреагировать на событие пользователя ТОЛЬКО 3 действиями:

  1. Можно сразу изменить состояние, чтобы например показать лоадинг

  2. Можно пойти в систему навигации и попросить ее переключиться на другой экран

  3. Можно пойти в Model, попросить выполнить работу или вернуть данные, после чего можно выполнить действие 1 или 2

Больше ViewModel ничего не делает!
Ну точнее у вас конечно делает, но это она не правильно делает

❕Важно понять❕

ViewModel(VM) ничего не делает сама
Она всегда реагирует только на действия пользователя
Открытие или закрытие экрана, это действия пользователя, на которые она так же реагирует

Пользователь открывает экран
→ Система или View создает VM
VM в init реагирует на то, что пользователь открыл экран и начинает свою работу

Пользователь закрывает экран
View сообщает VM что экран закрывается
VM реагирует на закрытие экрана и заканчивает свою работу

? Маппинг

ViewModel получая какие‑либо данные от Model, преобразует их в данные пригодные для View
Самый простой пример: форматировать дату в человеко‑читаемую строку с нужной локализацией
Никаких валидаций, формул, фильтров, сортировок и прочего делать не нужно
По хорошему, это все ответственность Model

Сразу предвижу возражение опытных: «Но это логика presentation слоя, ее не должно быть в бизнес логике»
Ответ:
Клиенты, и Android в частности, целиком являются presentation слоем, а бизнес логика находится на сервере
Поэтому наша логика представления, это и есть наша бизнес логика
Не понимание этих смыслов и порождает огромные и уродливые ViewModel/Presenter, на которые потом все ругаются
А что клиенты без сервера?
Они отличаются только наличием дополнительного слоя реальной бизнес логики
Сейчас я забежал на территорию Фрактальной архитектуры, но на этом пока остановимся
В будущем мы будем подробно об этом говорить

Сразу предвижу возражение опытных:

«Но это логика presentation слоя, ее не должно быть в бизнес логике»

Ответ:

Клиенты, и Android в частности, целиком являются presentation слоем,
а бизнес логика находится на сервере
Поэтому наша логика представления, это и есть наша бизнес логика

Не понимание этих смыслов и порождает огромные и уродливые ViewModel/Presenter, на которые потом все ругаются

А что клиенты без сервера?

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

Подведем итог

  • View передает все действия пользователя во ViewModel

  • ViewModel реагирует на действия, получает данные от Model, оформляет их в человеко‑читаемый вид и сохраняет у себя как State

  • View с помощью Наблюдателя, получает готовые данные и отображает их

P. S.: Минутка священных войн ⚔️

Можно хранить данные в VM как набор полей или как единый State
Можно сделать у VM много методов для действий пользователя или один метод
Это не имеет значения и VM, все еще останется собой, а не превратится в MVI

MVI это про другое

Не обязательно даже класс называть ViewModel (привет гугл),
чтобы сущность оставалась представлением модели
У меня в проекте роль VM занимаетComponentиз Decompose, но это все еще остается VM по смыслу ?

? Привязка данных (Data Binding)

А сейчас будет небольшой взрыв мозга для Android разработчиков (а может и не только?)

В шаблоне MVVM часто встречается формулировка,
что View использует привязку данных к ViewModel

А я рассказывал вам про Наблюдателя вместо привязки данных

Причина заключается в том, что DataBinding это библиотека для связывания XML с Activity/Fragment
Есть еще WPF Binding
Если что MVVM зародился в WPF (Windows Presentation Foundation)

Оба про связку XML/XAML с кодом JavaKotlin/С#
А по определению MVVM привязка данных используется между View и ViewModel
В WPF за View считается XALM, а код к которому идет привязка это как раз ViewModel
Из этого следует, что в Android: View это XML, а Activity/Fragment/внезапно класс View — это ViewModel

Ну и получается, что класс ViewModel от Google, это ни что иное как Model
Теперь понятно почему они вечно такие жирные

В процессе заимствования подхода MVVM из WPF, не был учтен тот факт,
что в Android часть сущностей совпадет названиями, но не смыслами

Разработчики под андройд, начиная разбираться как это все устроено,
не были знакомы с WPF, и интерпретировали названия как есть
XML + View (ну и Activity/Fragment в догонку) = View
Потом еще Google создали класс ViewModel и вообще все пошло поехало

Но самое интересное в этой истории, что по итогу эта ошибка оказалась верным решением, пусть и не сразу понятым

Да и в Google вроде не дураки сидят

Меня все еще бомбит что, цитата:

Класс ViewModel ... инкапсулирует соответствующую бизнес‑логику
Уровень предметной области ... является
необязательным

WOW только сейчас увидел приписку в обновленной документации:
Термин «уровень домена» используется в других архитектурах программного обеспечения, например в «чистой» архитектуре,
и имеет там другое значение

С приходом Compose в мир Android, он полностью занял место View (термина) заменив собой сразу и XML, и Activity/Fragment/View

Тем самым сделав класс ViewModel действительно Представлением модели

???

Комментарии (0)