Хабр, привет! Сегодня я хочу поговорить об архитектуре, которой я придерживаюсь в своих приложениях для Android. За основу я беру Чистую архитектуру, а в качестве инструментов использую компоненты Android Architecture (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines. Статья сопровождается кодом вымышленного примера, который доступен по адресу GitHub .
Отказ от ответственности
Я хочу поделиться своим опытом разработки; Я ни в коем случае не утверждаю, что мое решение единственно правильное и не имеет недостатков.Архитектура приложения — это своего рода модель, которую мы выбираем для решения той или иной задачи, и для выбранной модели важна адекватность ее применения конкретной задаче.
Проблема: зачем нам архитектура?
Большинство проектов, в которых я участвовал, имеют одну и ту же проблему: логика приложения размещается внутри среды Android, что приводит к большому объему кода внутри Fragment и Activity. Таким образом, код обрастает совершенно ненужными зависимостями, модульное тестирование становится практически невозможным, как и повторное использование.Со временем фрагменты становятся Богообъектами, даже небольшие изменения приводят к ошибкам, поддержание проекта становится дорогостоящим и эмоционально истощающим.
Есть проекты, у которых вообще нет архитектуры (здесь все понятно, к ним нет вопросов), есть проекты, которые претендуют на архитектуру, но там все равно возникают точно такие же проблемы.
Сейчас модно использовать Clean Architecture в Android. Я часто видел, что чистая архитектура ограничивается созданием репозиториев и сценариев, которые вызывают эти репозитории и больше ничего не делают. Еще хуже: такие скрипты возвращают модели из вызванных репозиториев.
И такая архитектура вообще не имеет смысла.
А поскольку скрипты просто вызывают нужные репозитории, то зачастую логика ложится на ViewModel или, что еще хуже, оседает во фрагментах и активностях.
Все это потом превращается в кашу, которую невозможно протестировать автоматически.
Цель архитектуры и дизайна
Цель архитектуры — отделить нашу бизнес-логику от деталей.Под деталями я подразумеваю, например, внешние API (когда мы разрабатываем клиент для REST-сервиса), среду Android (UI, сервисы) и т. д. За основу я использую Чистую архитектуру, но со своими допущениями в реализации.
Цель проектирования — связать воедино UI, API, Бизнес-логику, модели так, чтобы все это поддавалось автоматическому тестированию, было слабо связанным и легко расширяемым.
В своем дизайне я использую компоненты архитектуры Android. Для меня архитектура должна удовлетворять следующим критериям:
- Пользовательский интерфейс максимально прост и имеет всего три функции:
- Предоставить данные пользователю.
Данные поступают готовыми к отображению.
Это основная функция пользовательского интерфейса.
Есть виджеты, анимации, фрагменты и т.д.
- Реагируйте на события.
ViewModel и LiveData здесь очень помогают.
- Отправка команд от пользователя.
Для этого я использую свою простую командную структуру.
Подробнее об этом позже.
- Бизнес-логика зависит только от абстракций.
Это позволяет нам менять реализацию произвольных компонентов.
Решение
Принципиальная схема архитектуры представлена на рисунке ниже:Мы движемся снизу вверх по слоям, и слой ниже ничего не знает о слое выше.
А верхний слой относится только к слою, который находится на один уровень ниже.
Те.
Уровень API не может ссылаться на домен.
Уровень предметной области содержит бизнес-сущности со своей собственной логикой.
Обычно здесь есть сущности, существующие без приложения.
Например, для банка могут существовать кредитные организации со сложной логикой расчета процентов и т.п.
Уровень логики приложения содержит сценарии для работы самого приложения.
Именно здесь определяются все связи приложения, строится его суть.
Уровень API, Android — это всего лишь конкретная реализация нашего приложения в среде Android. В идеале этот слой можно изменить на что угодно.
Более того, когда я приступаю к разработке приложения, я начинаю с самого нижнего уровня — домена.
Затем появляется второй слой сценариев.
На 2-м слое все зависимости от внешних частей реализуются через интерфейсы.
Вы абстрагируетесь от деталей, можете сконцентрироваться только на логике приложения.
Вы можете сразу приступить к написанию тестов.
Это не подход TDD, но близкий к нему.
И только в самом конце появляется сам Android, API с реальными данными и т.д. Теперь более подробная схема проектирования Android-приложения.
Итак, логический уровень является ключевым, это приложение.
Только логический уровень может ссылаться на домен и взаимодействовать с ним.
Кроме того, уровень логики содержит интерфейсы, которые позволяют логике взаимодействовать с деталями приложения (api, android и т. д.).
Это так называемый принцип инверсии зависимостей, который позволяет логике не зависеть от деталей, а наоборот. Слой логики содержит варианты использования приложений (Use Cases), которые оперируют различными данными, взаимодействуют с доменом, репозиториями и т. д. В разработке я люблю мыслить сценариями.
На каждое действие пользователя или событие из системы запускается определенный скрипт, имеющий входные и выходные параметры, а также только один метод – запуск скрипта.
Кто-то вводит дополнительную концепцию интерактора, который может объединять несколько вариантов использования и добавлять дополнительную логику.
Но я этого не делаю, я считаю, что каждый скрипт может расширять или включать любой другой скрипт, для него не нужен интерактор.
Если вы посмотрите на диаграммы UML, вы увидите отношения включения и расширения.
Общая схема применения следующая:
- Создается среда Android (активности, фрагменты и т. д.).
- Создается ViewModel (одна или несколько).
- ViewModel создает необходимые сценарии, которые можно запускать из этой ViewModel. Скрипты лучше внедрять с помощью DI.
- Пользователь выполняет действие.
- Каждый компонент пользовательского интерфейса связан с командой, которую он может выполнить.
- Скрипт запускается с необходимыми параметрами, например, Login.execute(логин,пароль).
- Скрипт также использует DI для получения необходимых репозиториев и провайдеров.
Скрипт делает запрос на получение необходимых данных (асинхронных API-запросов может быть несколько, да что угодно).
Репозиторий выполняет запросы и возвращает данные в скрипт. Более того, у репозитория есть свои модели данных, которые он использует для своей внутренней работы, например репозиторий для REST будет содержать модели со всякими JSON-конвертерами.
Но перед доставкой результата в скрипт репозиторий всегда преобразует данные в модели данных скрипта.
Таким образом, логика ничего не знает о внутренней структуре репозитория и не зависит от нее.
Получив все необходимые данные, скрипт может создать необходимые объекты из домена.
Выполните некоторую логику в домене.
Когда скрипт завершит работу, он обязательно преобразует свой ответ в следующую модель представления.
Скрипт скрывает уровень предметной области, он предоставляет данные, которые сразу понятны уровню представления.
Вариант использования также может содержать сценарии обслуживания, например обработку ошибок.
- Скрипт вернул команде данные или ошибку.
Теперь вы можете обновить состояние ViewModel, что, в свою очередь, обновит пользовательский интерфейс.
Обычно я делаю это с помощью LiveData (шаги 9 и 10).
Ключевую роль для нас играет логика и ее модели данных.
Мы увидели двойную трансформацию: первая — трансформация репозитория в модель данных скрипта, а вторая — трансформация, когда скрипт отправляет данные в среду в результате своей работы.
Обычно результат выполнения сценария передается в viewModel для отображения в пользовательском интерфейсе.
Сценарий должен предоставлять данные, с которыми viewModel и пользовательский интерфейс больше ничего не делают. Команды Пользовательский интерфейс запускает выполнение сценария с помощью команды.
В своих проектах я использую собственную реализацию команд; они не являются частью архитектурных компонентов или чего-либо еще.
В целом их реализация проста; для более глубокого понимания идеи можно посмотреть реализацию команд в reactiveui.net для С#.
К сожалению, я не могу опубликовать свой рабочий код, просто упрощенную реализацию в качестве примера.
Основная задача команды — запустить скрипт, передав ему входные параметры, и после выполнения вернуть результат команды (данные или сообщение об ошибке).
Обычно все команды выполняются асинхронно.
Более того, команда инкапсулирует метод фонового расчета.
Я использую сопрограммы, но их можно легко заменить на RX, и это нужно будет делать только в абстрактной комбинации команда+вариант использования.
В качестве бонуса команда может сообщать о своем статусе: запущена она в данный момент или нет и может ли она быть выполнена в принципе.
Команды легко решают некоторые проблемы, такие как проблема двойного вызова (когда пользователь нажимает кнопку несколько раз во время выполнения операции) или проблемы с видимостью и отменой.
Пример
Реализовать функцию: вход в приложение по логину и паролю.Окно должно содержать поля для ввода логина и пароля, а также кнопку «Войти».
Логика работы следующая:
- Кнопка «Войти» должна быть неактивной, если логин и пароль содержат менее 4 символов.
- Кнопка «Войти» во время процедуры входа должна быть неактивна.
- Во время процедуры входа в систему должен отображаться индикатор (загрузчик).
- Если вход успешен, должно появиться приветственное сообщение.
- Если логин и/или пароль неверны, над полем ввода логина должно появиться сообщение об ошибке.
- Если на экране отображается сообщение об ошибке, то любой символ, введенный в поля логина или пароля, уберет это сообщение до следующей попытки.
- Бизнес-логика не зависит от деталей.
- Пользовательский интерфейс максимально прост. Он занимается только своей задачей (представляет данные, которые ему передаются, а также транслирует команды от пользователя).
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() } }
Я написал несколько простых расширений, таких какbindVisibleWithCommandIsExecuting, для привязки команд к элементам пользовательского интерфейса без дублирования кода.
Код этого примера с комментариями доступен по адресу GitHub , если интересно, можете скачать и ознакомиться.
На этом все, спасибо за внимание! Теги: #разработка Android #разработка Android #чистая архитектура #компоненты архитектуры Android
-
Копирует Ли Vista Mac Os X?
19 Oct, 24 -
Генри Лайон Олди: Как Писать Видеоигры
19 Oct, 24