Clean Architecture Tutorial for Android: Getting Started
In this tutorial, you’ll learn how to use Clean Architecture on Android to build robust, flexible and maintainable applications. By Ivan Kušt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Clean Architecture Tutorial for Android: Getting Started
30 mins
- Getting Started
- Clean Architecture
- Why Architecture Is Important
- SOLID Principles
- Layers of Clean Architecture
- Project Structure
- The Data and Business Logic Layers
- The Domain Layer
- The Data Layer
- Adding Repositories
- The Use Cases Layer
- Adding The Remaining Use Cases
- Framework and UI
- The Framework Layer
- Adding Remaining Data Sources
- The Presentation Layer
- Using MVVM
- Providing Sources
- Implementing MVVM
- Reading Documents
- Rendering PDFs
- Adding the Library Functionality
- Opening and Bookmarking Documents
- Conclusion
- When to Use Clean Architecture
- Where to Go From Here?
The Data and Business Logic Layers
You’ll work your way from the centermost abstract layers to the outer, more concrete layers.
The Domain Layer
The Domain layer contains all the models and business rules of your app.
Move the Bookmark
and Document
models provided in the starter project to the core module. Select Bookmark and Document files from app module and drag them to the com.raywenderlich.android.majesticreader.domain package in the core module. You’ll see a dialog:
Click on Refactor to finish the process.
The Data Layer
This layer provides abstract definitions for accessing data sources like a database or the internet. You’ll use Repository pattern in this layer.
The main purpose of the repository pattern is to abstract away the concrete implementation of data access. To achieve this, you’ll add one interface and one class for each model:
-
DataSource
interface: The interface that the Framework layer must implement. -
Repository
class: Provides methods for accessing the data that delegate toDataSource
.
Using the repository pattern is a good example of the Dependency Inversion Principle because:
- A Data layer which is of a higher, more abstract level doesn’t depend on a framework, lower-level layer.
- The repository is an abstraction of Data Access and it does not depend on details. It depends on abstraction.
Adding Repositories
Select com.raywenderlich.android.majesticreader.data in the core module. Add a new Kotlin file by right-clicking and selecting New ▸ Kotlin File/Class.
Type BookmarkDataSource in the dialog.
Click Finish. Open the new file and paste the following code after the first line:
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document
interface BookmarkDataSource {
suspend fun add(document: Document, bookmark: Bookmark)
suspend fun read(document: Document): List<Bookmark>
suspend fun remove(document: Document, bookmark: Bookmark)
}
You’ll use this interface to provide the Bookmark
data access from the Framework layer.
Repeat the process and add another interface named DocumentDataSource
:
import com.raywenderlich.android.majesticreader.domain.Document
interface DocumentDataSource {
suspend fun add(document: Document)
suspend fun readAll(): List<Document>
suspend fun remove(document: Document)
}
Repeat the process and add the last data source named OpenDocumentDataSource
:
import com.raywenderlich.android.majesticreader.domain.Document
interface OpenDocumentDataSource {
fun setOpenDocument(document: Document)
fun getOpenDocument(): Document
}
This data source will take care of storing and retrieving the currently opened PDF document. Next, add a new Kotlin file named BookmarkRepository
to the same package and copy and paste the following code:
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document
class BookmarkRepository(private val dataSource: BookmarkDataSource) {
suspend fun addBookmark(document: Document, bookmark: Bookmark) =
dataSource.add(document, bookmark)
suspend fun getBookmarks(document: Document) = dataSource.read(document)
suspend fun removeBookmark(document: Document, bookmark: Bookmark) =
dataSource.remove(document, bookmark)
}
This is the Repository that you’ll use to add, remove and fetch stored bookmarks in the app. Note that you mark all the methods with the suspend
modifier. This allows you to use coroutine-powered mechanisms in Room, for simpler threading.
As an exercise, try adding DocumentRepository
.
[spoiler title=”DocumentRepository”]
import com.raywenderlich.android.majesticreader.domain.Document
class DocumentRepository(
private val documentDataSource: DocumentDataSource,
private val openDocumentDataSource: OpenDocumentDataSource) {
suspend fun addDocument(document: Document) = documentDataSource.add(document)
suspend fun getDocuments() = documentDataSource.readAll()
suspend fun removeDocument(document: Document) = documentDataSource.remove(document)
fun setOpenDocument(document: Document) = openDocumentDataSource.setOpenDocument(document)
fun getOpenDocument() = openDocumentDataSource.getOpenDocument()
}
[/spoiler]
The Use Cases Layer
This layer converts and passes user actions, also known as use cases, to inner layers of the application.
Majestic Reader has two key functionalities:
- Showing and managing a list of documents in a library.
- Enabling the user to open a document and manage bookmarks in it.
From that, you can list the actions users should be able to perform:
- Adding a bookmark to a currently open document.
- Removing a bookmark from a currently open document.
- Getting all bookmarks for currently open documents.
- Adding a new document to the library.
- Removing a document from the library.
- Getting documents in the library.
- Setting currently opened documents.
- Getting currently opened documents.
Your next task is to create a class that represents each use case.
Create a new Kotlin file named AddBookmark in com.raywenderlich.android.majesticreader.interactors. Add the following code after the package statement:
import com.raywenderlich.android.majesticreader.data.BookmarkRepository
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document
class AddBookmark(private val bookmarkRepository: BookmarkRepository) {
suspend operator fun invoke(document: Document, bookmark: Bookmark) =
bookmarkRepository.addBookmark(document, bookmark)
}
Each use case class has only one function that invokes the use case. For convenience, you’re overloading the invoke operator. This enables you to simplify the function call on AddBookmark
instance to addBookmark()
instead of addBookmark.invoke()
.
Adding The Remaining Use Cases
Repeat this procedure and add the classes for the remaining actions:
[/spoiler]
[/spoiler]
[/spoiler]
[/spoiler]
[/spoiler]
[/spoiler]
[/spoiler]
-
AddDocument
[spoiler title=”AddDocument”]import com.raywenderlich.android.majesticreader.data.DocumentRepository import com.raywenderlich.android.majesticreader.domain.Document class AddDocument(private val documentRepository: DocumentRepository) { suspend operator fun invoke(document: Document) = documentRepository.addDocument(document) }
[/spoiler]
-
GetBookmarks
[spoiler title=”AddDocument”]import com.raywenderlich.android.majesticreader.data.BookmarkRepository import com.raywenderlich.android.majesticreader.domain.Document class GetBookmarks(private val bookmarkRepository: BookmarkRepository) { suspend operator fun invoke(document: Document) = bookmarkRepository.getBookmarks(document) }
[/spoiler]
-
GetDocuments
[spoiler title=”GetDocuments”]import com.raywenderlich.android.majesticreader.data.DocumentRepository class GetDocuments(private val documentRepository: DocumentRepository) { suspend operator fun invoke() = documentRepository.getDocuments() }
[/spoiler]
-
GetOpenDocument
[spoiler title=”GetOpenDocument”]import com.raywenderlich.android.majesticreader.data.DocumentRepository class GetOpenDocument(private val documentRepository: DocumentRepository) { operator fun invoke() = documentRepository.getOpenDocument() }
[/spoiler]
-
RemoveBookmark
[spoiler title=”RemoveBookmark”]import com.raywenderlich.android.majesticreader.data.BookmarkRepository import com.raywenderlich.android.majesticreader.domain.Bookmark import com.raywenderlich.android.majesticreader.domain.Document class RemoveBookmark(private val bookmarksRepository: BookmarkRepository) { suspend operator fun invoke(document: Document, bookmark: Bookmark) = bookmarksRepository .removeBookmark(document, bookmark) }
[/spoiler]
-
RemoveDocument
[spoiler title=”RemoveDocument”]import com.raywenderlich.android.majesticreader.data.DocumentRepository import com.raywenderlich.android.majesticreader.domain.Document class RemoveDocument(private val documentRepository: DocumentRepository) { suspend operator fun invoke(document: Document) = documentRepository.removeDocument(document) }
[/spoiler]
-
SetOpenDocument
[spoiler title=”SetOpenDocument”]import com.raywenderlich.android.majesticreader.data.DocumentRepository import com.raywenderlich.android.majesticreader.domain.Document class SetOpenDocument(private val documentRepository: DocumentRepository) { operator fun invoke(document: Document) = documentRepository.setOpenDocument(document) }
[/spoiler]
import com.raywenderlich.android.majesticreader.data.DocumentRepository
import com.raywenderlich.android.majesticreader.domain.Document
class AddDocument(private val documentRepository: DocumentRepository) {
suspend operator fun invoke(document: Document) = documentRepository.addDocument(document)
}
import com.raywenderlich.android.majesticreader.data.BookmarkRepository
import com.raywenderlich.android.majesticreader.domain.Document
class GetBookmarks(private val bookmarkRepository: BookmarkRepository) {
suspend operator fun invoke(document: Document) = bookmarkRepository.getBookmarks(document)
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository
class GetDocuments(private val documentRepository: DocumentRepository) {
suspend operator fun invoke() = documentRepository.getDocuments()
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository
class GetOpenDocument(private val documentRepository: DocumentRepository) {
operator fun invoke() = documentRepository.getOpenDocument()
}
import com.raywenderlich.android.majesticreader.data.BookmarkRepository
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document
class RemoveBookmark(private val bookmarksRepository: BookmarkRepository) {
suspend operator fun invoke(document: Document, bookmark: Bookmark) = bookmarksRepository
.removeBookmark(document, bookmark)
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository
import com.raywenderlich.android.majesticreader.domain.Document
class RemoveDocument(private val documentRepository: DocumentRepository) {
suspend operator fun invoke(document: Document) = documentRepository.removeDocument(document)
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository
import com.raywenderlich.android.majesticreader.domain.Document
class SetOpenDocument(private val documentRepository: DocumentRepository) {
operator fun invoke(document: Document) = documentRepository.setOpenDocument(document)
}