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
currentPage
to be set to the first page or first bookmarked page if it exists. - Gets
Document
passed toReaderFragment
. - Gets the last document that was opened from
GetOpenDocument
. - Sets the value of
document
to the one passed toReaderFragment
or falls back tolastOpenDocument
if 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.