- PVSM.RU - https://www.pvsm.ru -
Сегодня я хочу рассказать об архитектуре, которой я следую в своих Android приложениях. За основу я беру Clean Architecture, а в качестве инструментов использую Android Architecture Components (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines. К статье прилагается код вымышленного примера, который доступен на GitHub [1].
Я хочу поделиться своим опытом разработки, я ни в коем случае не претендую на то, что мое решение является единственно верным и лишенным недостатков. Архитектура приложения – это своего рода модель, которую мы выбираем для решения той или иной задачи, и для выбранной модели важна её адекватность применения к конкретной задаче.
Большинство проектов, в которых мне доводилось участвовать, имеют одну и ту же проблему: внутрь андроид окружения помещается логика приложения, что приводит к большому объему кода внутри Fragment и Activity. Таким образом код обрастает зависимостями, которые совсем не нужны, модульное тестирование становится практически невозможным, так же, как и повторное использование. Фрагменты со временем становятся God-объектами, даже мелкие изменения приводят к ошибкам, поддерживать проект становится дорого и эмоционально затратно.
Есть проекты, которые вообще не имеют никакой архитектуры (тут все понятно, к ним вопросов нет), есть проекты с претензией на архитектуру, но там все равно появляются точно такие же проблемы. Сейчас модно использовать Clean Architecture в Android. Часто видел, что Clean Architecture ограничивается созданием репозиториев и сценариев, которые вызывают эти репозитории и больше ничего не делают. Того хуже: такие сценарии возвращают модели из вызываемых репозиториев. И в такой архитектуре смысла нет вообще. И т.к. сценарии просто вызывают нужные репозитории, то часто логика ложится на ViewModel или, еще хуже, оседает во фрагментах и активностях. Все это потом превращается в кашу, не поддающуюся автоматическому тестированию.
Цель архитектуры – отделить нашу бизнес-логику от деталей. Под деталями я понимаю, например, внешние API (когда мы разрабатываем клиент для REST сервиса), Android – окружение (UI, сервисы) и т.д. В основе я использую Clean architecture, но со своими допущениями в реализации.
Цель дизайна – связать вместе UI, API, Бизнес-логику, модели так, чтобы все это поддавалось автоматическому тестированию, было слабо связанным, легко расширялось. В дизайне я использую Android Architecture Components.
Для меня архитектура должна удовлетворять следующим критериям:
Принципиальная схема архитектуры представлена на рисунке ниже:
Мы движемся снизу вверх по слоям, и слой, который находится ниже, ничего не знает о слое сверху. А верхний слой ссылается только на слой, который находится на один уровень ниже. Т.е. слой API не может ссылаться на домен.
Слой домен содержит бизнес сущности со своей логикой. Обычно здесь находятся сущности, которые существуют и без приложения. Например, для банка здесь могут находиться сущности кредитов со сложной логикой расчета процентов и т.д.
Слой логики приложения содержит сценарии работы самого приложения. Именно здесь определяются все связи приложения, выстраивается его суть.
Слой api, android – это лишь конкретная реализация нашего приложения в Android – среде. В идеале этот слой можно менять на что угодно.
Причем, когда я приступаю к разработке приложения, я начинаю с самого нижнего слоя — домена. Потом появляется второй слой сценариев. На 2-ом слое все зависимости от внешних деталей реализуются через интерфейсы. Вы абстрагированы от деталей, можно сконцентрироваться только на логике приложения. Тут же уже можно начинать писать тесты. Это не TDD подход, но близко к этому. И только в самом конце появляется сам Android, API с реальными данными и т.д.
Теперь более развернутая схема дизайна Android-приложения.
Итак, слой логики является ключевым, он и есть приложение. Только слой логики может ссылаться на домен и взаимодействовать с ним. Также слой логики содержит интерфейсы, которые позволяют логике взаимодействовать с деталями приложения (api, android и т.д.). Это так называемый принцип инверсии зависимости, который позволяет логике не зависеть от деталей, а наоборот. Слой логики содержит в себе сценарии использования приложения (Use Cases), которые оперируют разными данными, взаимодействуют с доменом, репозиториями и т.д. В разработке мне нравится мыслить сценариями. На каждое действие пользователя или событие от системы запускается некоторый сценарий, который имеет входные и выходные параметры, а также всего лишь один метод – запустить сценарий.
Кто-то вводит дополнительное понятие интерактора, который может объединять несколько сценариев использования и накручивать дополнительную логику. Но я этого не делаю, я считаю, что каждый сценарий может расширять или включать любой другой сценарий, для этого не нужен интерактор. Если посмотреть на схемы UML, то там можно увидеть связи включения и расширения.
Общая схема работы приложения следующая:
Т.е. ключевую роль у нас занимает логика и ее модели данных. Мы увидели двойное преобразование: первое – это преобразование репозитория в модель данных сценария и второе – преобразование, когда сценарий отдает данные в окружение, как результат своей работы. Обычно результат работы сценария отдается во viewModel для отображения в UI. Сценарий должен отдать такие данные, с которыми viewModel и UI ничего больше не делает.
Команды
UI запускает выполнение сценария с помощью команды. В моих проектах я использую собственную реализацию команд, они не являются частью архитектурных компонент или еще чего-либо. В общем, их реализация несложная, в качестве более глубокого знакомства с идеей можете посмотреть реализацию команд в reactiveui.net [2] для C#. Я, к сожалению, не могу выложить свой рабочий код, только упрощенную реализацию для примера.
Основная задача команды — это запускать некоторый сценарий, передав в него входные параметры, а после выполнения вернуть результат команды (данные или сообщение об ошибке). Обычно все команды выполняются асинхронно. Причем команда инкапсулирует метод background-вычислений. Я использую корутины, но их легко заменить на RX, и сделать это придется всего лишь в абстрактной связке command+use case. В качестве бонуса команда может сообщать свое состояние: выполняется ли она сейчас или нет и может ли она выполниться в принципе. Команды легко решают некоторые проблемы, например, проблему двойного вызова (когда пользователь кликнул несколько раз на кнопку, пока операция выполняется) или проблемы видимости и отмены выполнения.
Реализовать фичу: вход в приложение с помощью логина и пароля.
Окно должно содержать поля ввода логина и пароля и кнопку “Вход”. Логика работы следующая:
Эту задачу можно решить разными способами, например, поместить все в MainActivity.
Но я всегда слежу за выполнением моих двух главных правил:
Так выглядит приложение:
MainActivity выглядит следующим образом:
class MainActivity : AppCompatActivity() {
private val vm: MainViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bindLoginView()
bindProgressBar()
observeAuthorization()
observeRefreshView()
}
private fun bindProgressBar() {
progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
}
private fun bindLoginView() {
loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)
loginButton.bindCommand(this, vm.loginCommand) {
LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
}
}
private fun observeAuthorization() {
vm.authorizationSuccessLive.observe(this, Observer {
showAuthorizeSuccessMsg(it?.data)
})
vm.authorizationErrorLive.observe(this, Observer {
showAuthorizeErrorMsg()
})
}
private fun observeRefreshView() {
vm.refreshLoginViewLive.observe(this, Observer {
hideAuthorizeErrorMsg()
})
}
private fun showAuthorizeErrorMsg() {
loginErrorMsg.isInvisible = false
}
private fun hideAuthorizeErrorMsg() {
loginErrorMsg.isInvisible = true
}
private fun showAuthorizeSuccessMsg(name : String?) {
val msg = getString( R.string.success_login, name)
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
}
}
Активити достаточно прост, правило UI выполняется. Я написал несколько простых расширений, типа bindVisibleWithCommandIsExecuting, чтобы связывать команды с элементами UI и не дублировать код.
Код этого примера с комментариями доступен на GitHub [1], если интересно, можете скачать и ознакомиться.
На этом все, спасибо за внимание!
Автор: Алексей Рожков
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android-development/352678
Ссылки в тексте:
[1] GitHub: https://github.com/ar2code/AndroidArchitectureSample
[2] reactiveui.net: https://reactiveui.net/docs/handbook/commands/
[3] Источник: https://habr.com/ru/post/500128/?utm_source=habrahabr&utm_medium=rss&utm_campaign=500128
Нажмите здесь для печати.