Мощная базовая архитектура — важный показатель для масштабируемости приложения. Внесение таких изменений, как замена API на обновленную и оптимизированную структуру API, требует переписать практически все приложение полностью.
Причина заключается в том, что код тесно связан с модулем данных ответа. Использование Чистой архитектуры (Clean architecture) помогает решить эту проблему. Это лучшее решение для крупных приложений с большим количеством функций и SOLID-принципами. Она была предложена Робертом С. Мартином (известным как Дядя Боб) в блоге “Чистый код” в 2012 году.
Зачем нужен чистый подход?
- Разделение кода на разные слои с назначенными обязанностями облегчает дальнейшую модификацию
- Высокий уровень абстракции
- Слабая связанность между частями кода
- Легкость тестирования кода
“Чистый код всегда выглядит так, будто написан с заботой.” — Майкл Фэзерс
Какие бывают слои?
Domain-слой: Запускает независимую от других уровней бизнес-логику. Представляет собой чистый пакет kotlin без android-зависимостей.
Data-слой: Отправляет необходимые для приложения данные в domain-слой, реализуя предоставляемый доменом интерфейс.
Presentation-слой: Включает в себя как domain-, так и data-слои, а также является специфическим для android и выполняет UI-логику.
Что такое Domain-слой?
Базовый слой, соединяющий presentation-слой с data-слоем, в котором выполняется бизнес-логика приложения.
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-слой должен быть организован таким образом, чтобы данные могли быть использованы любым приложением без модификации логики представления.
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 и т. д. В этом слое соединяются все части архитектуры.
Папка 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