- Saved searches
- Use saved searches to filter your results more quickly
- tonivecina/kotlin-clean-architecture
- Name already in use
- Sign In Required
- Launching GitHub Desktop
- Launching GitHub Desktop
- Launching Xcode
- Launching Visual Studio Code
- Latest commit
- Git stats
- Files
- README.md
- About
- Clean Architecture с Kotlin
- Зачем нужен чистый подход?
- Какие бывают слои?
- Что такое Domain-слой?
- UseCases
- Репозитории
- Что такое Data-слой?
- Application Flow
Saved searches
Use saved searches to filter your results more quickly
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.
The clean architecture for Kotlin projects with Android Studio.
tonivecina/kotlin-clean-architecture
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Sign In Required
Please sign in to use Codespaces.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
Git stats
Files
Failed to load latest commit information.
README.md
Android clean architecture
This guide will show how to build a Android project with a clean architecture with Kotlin. It’s very important we have a project with packages and subpackages ordered by services, implementations and android functionalities.
The packages and subpackages must be ordered alphabetically for a fast localization (Automatic function of Android Studio). The valid packages will show hereunder.
Activities package must contains every activity of project divided in sub-packages. Each package will have Activity class and other class with services and implementations. If the activity has fragments, this fragments will be contained in sub-packages inside of this package.
Each interface used by Activity or Fragment must be separated by class (as service) and file.
This packages must be named with Activity name or Fragment name without key ‘Activity‘ or ‘Fragment‘. For example, package of MainActivity will be main or LoginFragment will be login.
If you wish manage application class of your project, Configuration file is your file and it will be contained in this package. If your project contains a local database, create the clase here, for example: AppDataBase.
All entities must be contained here and must be categorized like Database, Dynamic or Local.
- Dynamic for generic entities, for example entities for an Api.
- Database entities.
- Local for entities with memory storage, for example: Credentials.
The global patterns that it can be used at any class are contained here like Boolean, String, etc. File of class must be object name with Pattern; StringPattern, BooleanPattern.
The services package must contains subpackages and the class files to execute services like Api connections, Location.
Please, you must not implement services like Location, Bus, Tracking. in Activities. The best is create a global service with manage control, the Activities will use this services.
More info of class structures in classes section.
The views classes must be ordered in subpackages and the name file ends with view type. For example, a editText for forms would can called FormEditText inside of EditTexts package.
This is a simple full structure of project ordered in packages, subpackages and files:
The Activity and Fragment cycles are segmented with different elements:
View element is the Activity or Fragment class. All UI layer is contained here. It’s important that this class doesn’t implement interfaces because this package will have Listener elements to do it.
In this class the Process element will be initialized to manage all elements in this pacakge except the view. In addition, It only must contain Getters and Setters for UI.
This is a simple Activity example:
class MainActivity : AppCompatActivity() < private var recyclerView: RecyclerView? = null private var addButton: Button? = null private val processor = MainProcessor(this) private val noteAdapter = MainNoteAdapter() override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) recyclerView = findViewById(R.id.activity_main_recyclerView) as RecyclerView recyclerView?.adapter = noteAdapter recyclerView?.layoutManager = LinearLayoutManager(this) recyclerView?.itemAnimator = DefaultItemAnimator() val onClickListener = processor.onClickListener addButton = findViewById(R.id.activity_main_button_add) as Button addButton?.setOnClickListener(onClickListener) processor.onCreate() >override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) < super.onActivityResult(requestCode, resultCode, data) when (requestCode) < NewNoteActivity.REQUEST_CODE -> < processor.onNewNoteActivityResult(resultCode, data) >> > fun addNote(note: Note) < runOnUiThread < noteAdapter.notes.add(0, note) noteAdapter.notifyDataSetChanged() >> fun setNotes(notes: List) < runOnUiThread < noteAdapter.notes.clear() noteAdapter.notes.addAll(notes) noteAdapter.notifyDataSetChanged() >> >
The Process class will manage all elements in this package. This class is the communication channel with the view. Furthermore all elements in this package except the view will be initialized here.
This is a Process example with Listener and Interactor:
class MainProcessor(val view: MainActivity): MainListeners.ActionListener < val routes: MainRoutes by lazy < MainRoutes(view) >val onClickListener: MainOnClickListener by lazy < MainOnClickListener(this) >val notesInteractor: MainInteractoreNotes by lazy < MainInteractoreNotes() >fun onCreate() < getNotes() >private fun getNotes() < notesInteractor.getAll < notes: List-> view.setNotes(notes) > > fun onNewNoteActivityResult(resultCode: Int, data: Intent?) < when (resultCode) < Activity.RESULT_CANCELED -> < DLog.info("User cancelled the action.") >Activity.RESULT_OK -> < val note: Note? = data?.getSerializableExtra(NewNoteActivity.BUNDLE_NOTE) as? Note if (note == null) < DLog.warning("Note not found") return >view.addNote(note) > > > //region ActionListener override fun onAddNoteClick() < routes.intentForNoteAddActivity() >//endregion >
All communication between processor and interactors must be bidirectional through listeners. All listeners needs by interactors will be declared in Listener (abstract class).
abstract class MainListeners < interface ActionListener < fun onAddNoteClick() >>
The Interactors can communicate directly with entities to manage data or services.
class MainInteractorNotes < private val noteDao = Configuration.database.noteDao fun getAll(finished: (notes: List) -> Unit) < AsyncTask.execute < val notes = noteDao.getAll() Collections.reverse(notes) finished(notes) >> >
Implementations like OnClickListener be able to be considered as special interactor.
class MainOnClickListener(val listener: MainListeners.ActionListener): View.OnClickListener < override fun onClick(v: android.view.View?) < val when (id) < R.id.activity_main_button_add ->listener.onAddNoteClick() > > >
val onClickListener = processor.onClickListener addButton = findViewById(R.id.activity_main_button_add) as Button addButton?.setOnClickListener(onClickListener)
All navigations and connections with other activites or fragments are defined here. You apply intents here. Look at this example:
class MainRoutes(val view: MainActivity) < fun intentForNoteAddActivity() < val intent = Intent(view, NewNoteActivity::class.java) view.startActivityForResult(intent, NewNoteActivity.REQUEST_CODE, null) >>
This architecture paradigm is:
The entities are localized in two categories, Local for memory resources and Dynamic for API resources (for example).
A local entity could be Credentials. Look at this example code:
This example is for simple dynamic entity:
This example with Room service:
@Entity(tableName = "notes") class Note: Serializable < @PrimaryKey(autoGenerate = true) var id: Int = 0 @ColumnInfo(name = "title") var title: String? = null @ColumnInfo(name = "description") var description: String? = null @ColumnInfo(name = "createdAt") var createdAt: Long? = null constructor(): super() constructor(title: String, description: String, createdAt: Date) < this.title = title this.description = description val milliseconds = createdAt.time this.createdAt = milliseconds >@Ignore fun createdAtDate(): Date? < if (createdAt == null) < return null >val dateUpdated = createdAt!! return Date(dateUpdated) > >
The folder contains custom properties for primitives elements like booleans validators, formatters.
For example, If we want a validator of email format:
If your project needs location services, network services. Please, not duplicate code. We create shared services for views that need it.
This example is a location service:
For a good design is necessary that we have custom views like EditText, TextViews, etc. This custom classes must be localized here.
This is a simple example for an EditText of login view:
//region GettersReg . //end //region SettersReg . ///end
- The classes must not contains more than 250 lines.
- Not use nomenclature for parameters or methods with more than 80 characters.
About
The clean architecture for Kotlin projects with Android Studio.
Clean Architecture с Kotlin
Мощная базовая архитектура — важный показатель для масштабируемости приложения. Внесение таких изменений, как замена 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,
private val repositories: NewsRepository): BaseFlowableUseCase(transformer)
override fun createFlowable(data: Map?): Flowable return repositories.getNews()
>
fun getNews(): Flowable val data = HashMap()
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 return cache.getNews()
>
override fun getRemoteNews(): Flowable return remote.getNews()
>
override fun getNews(): Flowable 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) : BaseViewModel()
companion object private val TAG = "viewmodel"
>
var mNews = MutableLiveData>()
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(var responseType: Status, var data: RequestData? = null, var error: Error? = null)
enum class Status
Класс data wrapper используется в LiveData в качестве вспомогательного класса, который уведомляет представление о состоянии запроса (запуск, результат и т.д.).
Каким образом соединяются все слои?
Каждый слой обладает собственной сущностью (entities), специфичной для данного пакета. Mapper преобразовывает сущность одного слоя в другую. Все слои обладают разными сущностями, благодаря чему каждый из них полностью независим, и лишь необходимые данные могут быть переданы последующему слою.
Application Flow
Базовая архитектура определяет единство приложения, и да, каждому приложению соответствует своя архитектура, НО почему бы не выбрать масштабируемую, надежную и тестируемую архитектуру заранее, чтобы избежать дальнейших трудностей.