Architecture

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

Причина заключается в том, что код тесно связан с модулем данных ответа. Использование Чистой архитектуры (Clean architecture) помогает решить эту проблему. Это лучшее решение для крупных приложений с большим количеством функций и SOLID-принципами. Она была предложена Робертом С. Мартином (известным как Дядя Боб) в блоге “Чистый код” в 2012 году.

Зачем нужен чистый подход?

  1. Разделение кода на разные слои с назначенными обязанностями облегчает дальнейшую модификацию
  2. Высокий уровень абстракции
  3. Слабая связанность между частями кода
  4. Легкость тестирования кода

“Чистый код всегда выглядит так, будто написан с заботой.” — Майкл Фэзерс

Какие бывают слои?

Dependency Flow

Domain-слой: Запускает независимую от других уровней бизнес-логику. Представляет собой чистый пакет kotlin без android-зависимостей.

Data-слой: Отправляет необходимые для приложения данные в domain-слой, реализуя предоставляемый доменом интерфейс.

Presentation-слой: Включает в себя как domain-, так и data-слои, а также является специфическим для android и выполняет UI-логику.

Что такое Domain-слой?

Базовый слой, соединяющий presentation-слой с data-слоем, в котором выполняется бизнес-логика приложения.

Структура domain-слоя приложения

UseCases

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

class GetNewsUseCase(private val transformer: FlowableRxTransformer<NewsSourcesEntity>,
private val repositories: NewsRepository): BaseFlowableUseCase<NewsSourcesEntity>(transformer){

override fun createFlowable(data: Map<String, Any>?): Flowable<NewsSourcesEntity> {
return repositories.getNews()
}

fun getNews(): Flowable<NewsSourcesEntity>{
val data = HashMap<String, String>()
return single(data)
}
}

Прецедент возвращает Flowable, который можно модифицировать в соответствии с требуемым наблюдателем. Есть два параметра. Трансформер или ObservableTransformer, контролирующий выбор потока для выполнения логики, и репозиторий, который представляет собой интерфейс дляdata-слоя. Для передачи данных в data-слой используется HashMap.

Репозитории

Определяют функциональности в соответствии с требованиями прецедента, которые реализуются data-слоем.

Что такое Data-слой?

Этот слой предоставляет необходимые для приложения данные. Data-слой должен быть организован таким образом, чтобы данные могли быть использованы любым приложением без модификации логики представления.

Структура data-слоя приложения

API реализуют удаленную сеть. Он может включать любую сетевую библиотеку, такую как retrofit, volley и т. д. Аналогичным образом, DB реализует локальную базу данных.

class NewsRepositoryImpl(private val remote: NewsRemoteImpl,
private val cache: NewsCacheImpl) : NewsRepository {

override fun getLocalNews(): Flowable<NewsSourcesEntity> {
return cache.getNews()
}

override fun getRemoteNews(): Flowable<NewsSourcesEntity> {
return remote.getNews()
}

override fun getNews(): Flowable<NewsSourcesEntity> {
val updateNewsFlowable = remote.getNews()
return cache.getNews()
.mergeWith(updateNewsFlowable.doOnNext{
remoteNews -> cache.saveArticles(remoteNews)
})
}
}

В репозитории реализуются локальные, удаленные и любые другие источники данных. В примере выше класс NewsRepositoryImpl.kt реализует предоставляемый domain-слоем интерфейс. Он выступает в качестве единой точки доступа для data-слоя.

Что такое presentation-слой?

Presentation-слой реализует пользовательский интерфейс приложения. Этот слой выполняет только инструкции без какой-либо логики. Он внутренне реализует такие архитектуры, как MVC, MVP, MVVM, MVI и т. д. В этом слое соединяются все части архитектуры.

Структура presentation-слоя приложения

Папка DI обеспечивает внедрение всех зависимостей при запуске приложения, таких как сети, View Models, Прецеденты и т.д. DI в android реализуется с помощью dagger, kodein, koin или шаблона service locator. Все зависит от типа приложения. Я выбрал koin, поскольку его легче понять и реализовать, чем dagger.

Зачем использовать ViewModels?

В соответствии с документацией android, ViewModel:

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

class NewsViewModel(private val getNewsUseCase: GetNewsUseCase,
private val mapper: Mapper<NewsSourcesEntity, NewsSources>) : BaseViewModel() {

companion object {
private val TAG = "viewmodel"
}

var mNews = MutableLiveData<Data<NewsSources>>()

fun fetchNews() {
val disposable = getNewsUseCase.getNews()
.flatMap { mapper.Flowable(it) }
.subscribe({ response ->
Log.d(TAG, "On Next Called")
mNews.value = Data(responseType = Status.SUCCESSFUL, data = response)
}, { error ->
Log.d(TAG, "On Error Called")
mNews.value = Data(responseType = Status.ERROR, error = Error(error.message))
}, {
Log.d(TAG, "On Complete Called")
})

addDisposable(disposable)
}

fun getNewsLiveData() = mNews
}

Таким образом, ViewModel сохраняет данные при изменении конфигурации. Presenter в MVP привязан к представлению с интерфейсом, что усложняет тестирование, в то время как в ViewModel отсутствует интерфейс из-за архитектурно-ориентированных компонентов.

Базовый View Model использует CompositeDisposable для добавления всех observables и удаляет их на стадии жизненного цикла @OnCleared.

data class Data<RequestData>(var responseType: Status, var data: RequestData? = null, var error: Error? = null)

enum class Status { SUCCESSFUL, ERROR, LOADING }

Класс data wrapper используется в LiveData в качестве вспомогательного класса, который уведомляет представление о состоянии запроса (запуск, результат и т.д.).

Каким образом соединяются все слои?

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

Application Flow


Заключение:

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


Перевод статьи Rakshit jain: Kotlin Clean Architecture