Архитектура И Дизайн Android-Приложений (Мой Опыт)

Хабр, привет! Сегодня я хочу поговорить об архитектуре, которой я придерживаюсь в своих приложениях для Android. За основу я беру Чистую архитектуру, а в качестве инструментов использую компоненты Android Architecture (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines. Статья сопровождается кодом вымышленного примера, который доступен по адресу GitHub .



Отказ от ответственности

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

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



Проблема: зачем нам архитектура?

Большинство проектов, в которых я участвовал, имеют одну и ту же проблему: логика приложения размещается внутри среды Android, что приводит к большому объему кода внутри Fragment и Activity. Таким образом, код обрастает совершенно ненужными зависимостями, модульное тестирование становится практически невозможным, как и повторное использование.

Со временем фрагменты становятся Богообъектами, даже небольшие изменения приводят к ошибкам, поддержание проекта становится дорогостоящим и эмоционально истощающим.

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

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

И такая архитектура вообще не имеет смысла.

А поскольку скрипты просто вызывают нужные репозитории, то зачастую логика ложится на ViewModel или, что еще хуже, оседает во фрагментах и активностях.

Все это потом превращается в кашу, которую невозможно протестировать автоматически.



Цель архитектуры и дизайна

Цель архитектуры — отделить нашу бизнес-логику от деталей.

Под деталями я подразумеваю, например, внешние API (когда мы разрабатываем клиент для REST-сервиса), среду Android (UI, сервисы) и т. д. За основу я использую Чистую архитектуру, но со своими допущениями в реализации.

Цель проектирования — связать воедино UI, API, Бизнес-логику, модели так, чтобы все это поддавалось автоматическому тестированию, было слабо связанным и легко расширяемым.

В своем дизайне я использую компоненты архитектуры Android. Для меня архитектура должна удовлетворять следующим критериям:

  1. Пользовательский интерфейс максимально прост и имеет всего три функции:
  2. Предоставить данные пользователю.

    Данные поступают готовыми к отображению.

    Это основная функция пользовательского интерфейса.

    Есть виджеты, анимации, фрагменты и т.д.

  3. Реагируйте на события.

    ViewModel и LiveData здесь очень помогают.

  4. Отправка команд от пользователя.

    Для этого я использую свою простую командную структуру.

    Подробнее об этом позже.

  5. Бизнес-логика зависит только от абстракций.

    Это позволяет нам менять реализацию произвольных компонентов.



Решение

Принципиальная схема архитектуры представлена на рисунке ниже:

Архитектура и дизайн Android-приложений (мой опыт)

Мы движемся снизу вверх по слоям, и слой ниже ничего не знает о слое выше.

А верхний слой относится только к слою, который находится на один уровень ниже.

Те.

Уровень API не может ссылаться на домен.

Уровень предметной области содержит бизнес-сущности со своей собственной логикой.

Обычно здесь есть сущности, существующие без приложения.

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

Уровень логики приложения содержит сценарии для работы самого приложения.

Именно здесь определяются все связи приложения, строится его суть.

Уровень API, Android — это всего лишь конкретная реализация нашего приложения в среде Android. В идеале этот слой можно изменить на что угодно.

Более того, когда я приступаю к разработке приложения, я начинаю с самого нижнего уровня — домена.

Затем появляется второй слой сценариев.

На 2-м слое все зависимости от внешних частей реализуются через интерфейсы.

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

Вы можете сразу приступить к написанию тестов.

Это не подход TDD, но близкий к нему.

И только в самом конце появляется сам Android, API с реальными данными и т.д. Теперь более подробная схема проектирования Android-приложения.



Архитектура и дизайн Android-приложений (мой опыт)

Итак, логический уровень является ключевым, это приложение.

Только логический уровень может ссылаться на домен и взаимодействовать с ним.

Кроме того, уровень логики содержит интерфейсы, которые позволяют логике взаимодействовать с деталями приложения (api, android и т. д.).

Это так называемый принцип инверсии зависимостей, который позволяет логике не зависеть от деталей, а наоборот. Слой логики содержит варианты использования приложений (Use Cases), которые оперируют различными данными, взаимодействуют с доменом, репозиториями и т. д. В разработке я люблю мыслить сценариями.

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

Кто-то вводит дополнительную концепцию интерактора, который может объединять несколько вариантов использования и добавлять дополнительную логику.

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

Если вы посмотрите на диаграммы UML, вы увидите отношения включения и расширения.

Общая схема применения следующая:

Архитектура и дизайн Android-приложений (мой опыт)

  1. Создается среда Android (активности, фрагменты и т. д.).

  2. Создается ViewModel (одна или несколько).

  3. ViewModel создает необходимые сценарии, которые можно запускать из этой ViewModel. Скрипты лучше внедрять с помощью DI.
  4. Пользователь выполняет действие.

  5. Каждый компонент пользовательского интерфейса связан с командой, которую он может выполнить.

  6. Скрипт запускается с необходимыми параметрами, например, Login.execute(логин,пароль).

  7. Скрипт также использует DI для получения необходимых репозиториев и провайдеров.

    Скрипт делает запрос на получение необходимых данных (асинхронных API-запросов может быть несколько, да что угодно).

    Репозиторий выполняет запросы и возвращает данные в скрипт. Более того, у репозитория есть свои модели данных, которые он использует для своей внутренней работы, например репозиторий для REST будет содержать модели со всякими JSON-конвертерами.

    Но перед доставкой результата в скрипт репозиторий всегда преобразует данные в модели данных скрипта.

    Таким образом, логика ничего не знает о внутренней структуре репозитория и не зависит от нее.

    Получив все необходимые данные, скрипт может создать необходимые объекты из домена.

    Выполните некоторую логику в домене.

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

    Скрипт скрывает уровень предметной области, он предоставляет данные, которые сразу понятны уровню представления.

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

  8. Скрипт вернул команде данные или ошибку.

    Теперь вы можете обновить состояние ViewModel, что, в свою очередь, обновит пользовательский интерфейс.

    Обычно я делаю это с помощью LiveData (шаги 9 и 10).

Те.

Ключевую роль для нас играет логика и ее модели данных.

Мы увидели двойную трансформацию: первая — трансформация репозитория в модель данных скрипта, а вторая — трансформация, когда скрипт отправляет данные в среду в результате своей работы.

Обычно результат выполнения сценария передается в viewModel для отображения в пользовательском интерфейсе.

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

В своих проектах я использую собственную реализацию команд; они не являются частью архитектурных компонентов или чего-либо еще.

В целом их реализация проста; для более глубокого понимания идеи можно посмотреть реализацию команд в reactiveui.net для С#.

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

Основная задача команды — запустить скрипт, передав ему входные параметры, и после выполнения вернуть результат команды (данные или сообщение об ошибке).

Обычно все команды выполняются асинхронно.

Более того, команда инкапсулирует метод фонового расчета.

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

В качестве бонуса команда может сообщать о своем статусе: запущена она в данный момент или нет и может ли она быть выполнена в принципе.

Команды легко решают некоторые проблемы, такие как проблема двойного вызова (когда пользователь нажимает кнопку несколько раз во время выполнения операции) или проблемы с видимостью и отменой.



Пример

Реализовать функцию: вход в приложение по логину и паролю.

Окно должно содержать поля для ввода логина и пароля, а также кнопку «Войти».

Логика работы следующая:

  1. Кнопка «Войти» должна быть неактивной, если логин и пароль содержат менее 4 символов.

  2. Кнопка «Войти» во время процедуры входа должна быть неактивна.

  3. Во время процедуры входа в систему должен отображаться индикатор (загрузчик).

  4. Если вход успешен, должно появиться приветственное сообщение.

  5. Если логин и/или пароль неверны, над полем ввода логина должно появиться сообщение об ошибке.

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

Эту проблему можно решить по-разному, например, поместив все в MainActivity. Но я всегда слежу за тем, чтобы соблюдались два моих главных правила:
  1. Бизнес-логика не зависит от деталей.

  2. Пользовательский интерфейс максимально прост. Он занимается только своей задачей (представляет данные, которые ему передаются, а также транслирует команды от пользователя).

Вот как выглядит приложение:

Архитектура и дизайн Android-приложений (мой опыт)

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

Вместе с данным постом часто просматривают: