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?
Reading Documents
Open ReaderViewModel in com.raywenderlich.android.majesticreader.presentation.reader. There are a few places marked with // TODO comments that you’ll add code to.
Here’s an outline of the ReaderViewModel with functions that ReaderFragment will call on user actions:
-
openDocument(): Opens the PDF document. -
openBookmark(): Navigates to the given bookmark in the document. -
openPage(): Opens a given page in the document. -
nextPage(): Navigates to the next page. -
previousPage(): Navigates to the previous page. -
toggleBookmark(): Adds or removes the current page from document bookmarks. -
toggleInLibrary(): Adds or removes the open document from the library.
ReaderFragment will get a Document to display as an argument when it’s created.
Look for the first // TODO comment in ReaderViewModel. Add the following code in its place:
addSource(document) { document ->
GlobalScope.launch {
postValue(interactors.getBookmarks(document))
}
}
This will change the value of bookmarks each time you change the document. It will fill with up to date bookmarks, which you get from the interactors, within a coroutine. Your bookmarks field should now look like the following:
val bookmarks = MediatorLiveData<List<Bookmark>>().apply {
addSource(document) { document ->
GlobalScope.launch {
postValue(interactors.getBookmarks(document))
}
}
}
The document holds the document parsed from Fragment arguments. bookmarks holds the list of bookmarks in the current document. ReaderFragment will subscribe to it to get the list of available bookmarks.
Rendering PDFs
To render the PDF document pages, use the PdfRenderer, which is available in Android SDK since API level 21.
currentPage holds the reference to PdfRenderer.Page that you currently display, if any. renderer holds a reference to the PdfRenderer used for rendering the document. Each time you change the document‘s internal value, you create a new instance of PdfRenderer for the document and store in the renderer.
hasPreviousPage and hasNextPage rely on currentPage. They use LiveData transformations. hasPreviousPage returns true if the index of currentPage is larger than zero. hasNextPage returns true if the index of currentPage is less than the page count minus one – if the user hasn’t reached the end. This data then dictates how the UI should appear and behave, in the ReaderFragment.
Adding the Library Functionality
isCurrentPageBookmarked() returns true if a bookmark for the currently shown page exists. Find isInLibrary(). It should return true if the open document is already in the library. Replace it with:
private suspend fun isInLibrary(document: Document) =
interactors.getDocuments().any { it.url == document.url }
This will use GetDocuments to get a list of all documents in the library and check if it contains one that matches the currently open document. Since this is a suspend function, change the isInLibrary LiveData code to the following:
val isInLibrary: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>().apply {
addSource(document) { document -> GlobalScope.launch { postValue(isInLibrary(document)) } }
}
In the end, the LiveData relations are really simple. isBookmarked relies on isCurrentPageBookmarked() – it will be true if there is a bookmark for the current page. Every time document, currentPage or bookmarks change, isBookmarked will receive an update and change, as well.
Look for the next // TODO comment in loadArguments().
Put the following code in its place:
// 1
currentPage.apply {
addSource(renderer) { renderer ->
GlobalScope.launch {
val document = document.value
if (document != null) {
val bookmarks = interactors.getBookmarks(document).lastOrNull()?.page ?: 0
postValue(renderer.openPage(bookmarks))
}
}
}
}
// 2
val documentFromArguments = arguments.get(DOCUMENT_ARG) as Document? ?: Document.EMPTY
// 3
val lastOpenDocument = interactors.getOpenDocument()
// 4
document.value = when {
documentFromArguments != Document.EMPTY -> documentFromArguments
documentFromArguments == Document.EMPTY && lastOpenDocument != Document.EMPTY -> lastOpenDocument
else -> Document.EMPTY
}
// 5
document.value?.let { interactors.setOpenDocument(it) }
Here’s what the above code is doing, step by step.
- Initializes
currentPageto be set to the first page or first bookmarked page if it exists. - Gets
Documentpassed toReaderFragment. - Gets the last document that was opened from
GetOpenDocument. - Sets the value of
documentto the one passed toReaderFragmentor falls back tolastOpenDocumentif nothing was passed. - Sets the new open document by calling
SetOpenDocument.
Opening and Bookmarking Documents
Next, you’ll implement openDocument(). Replace it with the following code:
fun openDocument(uri: Uri) {
document.value = Document(uri.toString(), "", 0, "")
document.value?.let { interactors.setOpenDocument(it) }
}
This creates a new Document that represents the document that was just open and passes it to SetOpenDocument.
Next, implement toggleBookmark(). Replace it with the following:
fun toggleBookmark() {
val currentPage = currentPage.value?.index ?: return
val document = document.value ?: return
val bookmark = bookmarks.value?.firstOrNull { it.page == currentPage }
GlobalScope.launch {
if (bookmark == null) {
interactors.addBookmark(document, Bookmark(page = currentPage))
} else {
interactors.deleteBookmark(document, bookmark)
}
bookmarks.postValue(interactors.getBookmarks(document))
}
}
In this function, you either delete or add a bookmark, depending on if it’s already in your database, and then you update the bookmarks, to refresh the UI.
Finally, implement toggleInLibrary(). Replace it with the following:
fun toggleInLibrary() {
val document = document.value ?: return
GlobalScope.launch {
if (isInLibrary.value == true) {
interactors.removeDocument(document)
} else {
interactors.addDocument(document)
}
isInLibrary.postValue(isInLibrary(document))
}
}
Now build and run the app. Now you can open the document from your library by tapping it! :]
Conclusion
That’s it! You have a working PDF reader, and you’ve mastered Clean Architecture on Android! Congratulations!
Here’s a graph that gives an overview of Clean Architecture in combination with MVVM:
The three most important things to remember are:
- The communication between layers: Only outer layers can depend on inner layers.
- The number of layers is arbitrary: Customize it to your needs.
- Things become more abstract in inner circles.
Pros of using Clean Architecture:
- Code is more decoupled and testable.
- You can replace the framework and presentation layers and port your app to a different platform.
- It’s easier to maintain the project and add new features.
Cons of using Clean Architecture:
- You’ll have to write more code, but it pays off.
- You have to learn and understand Clean Architecture to work on the project.
When to Use Clean Architecture
It’s important to note that Clean architecture isn’t a silver bullet solution, but can be general, for any platform. You should decide, based on the project if it suits your needs. For example, if your project is big and complex, has a lot of business logic – then the Clean architecture brings clear benefits. On the other hand, for smaller and simpler projects those benefits might not be worth it – you’ll just end up writing more code and adding some complexity with all the layers, investing more time along the way.

