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?
Framework and UI
This concludes the implementation of the three inner layers in the core module. You’re now ready to move on to remaining layers: Framework and Presentation. Both of those layers depend on Android SDK, so you’ll place them in the app
module.
The Framework Layer
The Framework layer holds implementations of interfaces defined in the Data layer.
Your next task is to provide implementations of Data source interfaces from the Data layer. Start with OpenDocumentDataSource
. It will store the currently open document in memory and is the simplest one.
Create a new file in app module in com.raywenderlich.android.majesticreader.framework named InMemoryOpenDocumentDataSource
. Paste the following after the first line:
import com.raywenderlich.android.majesticreader.data.OpenDocumentDataSource
import com.raywenderlich.android.majesticreader.domain.Document
class InMemoryOpenDocumentDataSource : OpenDocumentDataSource {
private var openDocument: Document = Document.EMPTY
override fun setOpenDocument(document: Document) {
openDocument = document
}
override fun getOpenDocument() = openDocument
}
This is an implementation of OpenDocumentDataSource
from the Data layer. Currently, the open document is stored in memory, so the implementation is pretty straightforward.
Adding Remaining Data Sources
You will use the remaining data sources to delegate and persist data in the database, using the Room library. The classes required for persisting Bookmarks
and Document
via Room are in the db subpackage.
Create a new Kotlin file named RoomBookmarkDataSource in framework. Add the following code in the new file:
import android.content.Context
import com.raywenderlich.android.majesticreader.data.BookmarkDataSource
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document
import com.raywenderlich.android.majesticreader.framework.db.BookmarkEntity
import com.raywenderlich.android.majesticreader.framework.db.MajesticReaderDatabase
class RoomBookmarkDataSource(context: Context) : BookmarkDataSource {
// 1
private val bookmarkDao = MajesticReaderDatabase.getInstance(context).bookmarkDao()
// 2
override suspend fun add(document: Document, bookmark: Bookmark) =
bookmarkDao.addBookmark(BookmarkEntity(
documentUri = document.url,
page = bookmark.page
))
override suspend fun read(document: Document): List<Bookmark> = bookmarkDao
.getBookmarks(document.url).map { Bookmark(it.id, it.page) }
override suspend fun remove(document: Document, bookmark: Bookmark) =
bookmarkDao.removeBookmark(
BookmarkEntity(id = bookmark.id, documentUri = document.url, page = bookmark.page)
)
}
Here’s what the code is doing, step by step:
- Use
MajesticReaderDatabase
to get an instance ofBookmarkDao
and store it in local field. - Call add, read and remove methods from Room implementation.
Create a new Kotlin file named RoomDocumentDataSource in framework
. Add the following code in the new file:
import android.content.Context
import com.raywenderlich.android.majesticreader.data.DocumentDataSource
import com.raywenderlich.android.majesticreader.domain.Document
import com.raywenderlich.android.majesticreader.framework.db.DocumentEntity
import com.raywenderlich.android.majesticreader.framework.db.MajesticReaderDatabase
class RoomDocumentDataSource(val context: Context) : DocumentDataSource {
private val documentDao = MajesticReaderDatabase.getInstance(context).documentDao()
override suspend fun add(document: Document) {
val details = FileUtil.getDocumentDetails(context, document.url)
documentDao.addDocument(
DocumentEntity(document.url, details.name, details.size, details.thumbnail)
)
}
override suspend fun readAll(): List<Document> = documentDao.getDocuments().map {
Document(
it.uri,
it.title,
it.size,
it.thumbnailUri
)
}
override suspend fun remove(document: Document) = documentDao.removeDocument(
DocumentEntity(document.url, document.name, document.size, document.thumbnail)
)
}
Now, what’s left to do is to connect all the dots, and display the data.
The Presentation Layer
This layer contains the User Interface-related code. This layer is in the same circle as the framework layer, so you can depend on its classes.
Using MVVM
You’ll be using the MVVM pattern in this layer because it’s supported by Android Jetpack. Note that it doesn’t matter which pattern you use for this layer and you are free to use what suits your needs best, be it MVP, MVI or something else.
For a quick introduction, here’s a diagram:
MVVM pattern consists of three components:
- View: responsible for drawing the UI to the user
- Model: Contains business logic and data.
- ViewModel: Acts as a bridge between data and UI.
In Clean Architecture, instead of relying on Models, you’ll communicate with Interactors from the Use Case layer.
This layer contains the user interface related code, powered by Android Jetpack! :]
Providing Sources
Before moving on to implementing the presentation layer, you need a way to provide the Data sources to the data layer. You should usually do this using dependency injection. It is the process of separating provider functions or factories for dependencies, and their usage. This makes your classes cleaner, as they don’t create dependencies in their constructors.
To keep things simple you’ll manually implement an easy way to provide dependencies to your ViewModels.
First, replace the empty Interactors
class in the framework namespace with the data class that holds all interactors:
import com.raywenderlich.android.majesticreader.interactors.*
data class Interactors(
val addBookmark: AddBookmark,
val getBookmarks: GetBookmarks,
val deleteBookmark: RemoveBookmark,
val addDocument: AddDocument,
val getDocuments: GetDocuments,
val removeDocument: RemoveDocument,
val getOpenDocument: GetOpenDocument,
val setOpenDocument: SetOpenDocument
)
You’ll use it to access interactors from ViewModels.
Open MajesticReaderApplication
and replace onCreate()
with the following, making sure you add all the necessary imports:
override fun onCreate() {
super.onCreate()
val bookmarkRepository = BookmarkRepository(RoomBookmarkDataSource(this))
val documentRepository = DocumentRepository(
RoomDocumentDataSource(this),
InMemoryOpenDocumentDataSource()
)
MajesticViewModelFactory.inject(
this,
Interactors(
AddBookmark(bookmarkRepository),
GetBookmarks(bookmarkRepository),
RemoveBookmark(bookmarkRepository),
AddDocument(documentRepository),
GetDocuments(documentRepository),
RemoveDocument(documentRepository),
GetOpenDocument(documentRepository),
SetOpenDocument(documentRepository)
)
)
}
This injects all the dependencies into MajesticViewModelFactory
. It creates ViewModels in the app and passes interactor dependencies to them.
That concludes everything required for dependency injection. Now back to the Presentation layer.
Implementing MVVM
Open LibraryViewModel.kt in com.raywenderlich.android.majesticreader.presentation.library.
The ViewModel contains functions for loading the list of documents and adding a new one to the list. It serves as a connection between the UI and the interactors, or use cases.
First, replace loadDocuments()
with the following:
fun loadDocuments() {
GlobalScope.launch {
documents.postValue(interactors.getDocuments())
}
}
This fetches the list of documents from the library using the GetDocuments
interactor, from within a coroutine, which you start by calling launch()
. Once done, you post the result to the documents
LiveData.
GlobalScope
often, in your code, but for the sake of simplicity, you will use it in this project.Next, for addDocument()
, you want to additionally call loadDocuments()
after adding a new Document:
fun addDocument(uri: Uri) {
GlobalScope.launch {
withContext(Dispatchers.IO) {
interactors.addDocument(Document(uri.toString(), "", 0, ""))
}
loadDocuments()
}
}
To add a new document, you first launch a coroutine, as before, then use withContext()
, to move the database insert to an IO-optimized thread, and suspending until insertion completes. In the end, you load the documents again, to update the list.
Finally, setOpenDocument()
calls the appropriate interactor:
fun setOpenDocument(document: Document) {
interactors.setOpenDocument(document)
}
Now build and run
the app. You can now add new documents to the library. At last, you can bear the fruits of your labor! :]
Tap the floating action button. You’ll see a screen for picking a document from your storage. After you add a document, you’ll see it on the list.
There is one more screen left — the reader screen.