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.

4.7 (94) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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.

Note: Fore more info on PdfRenderer check the official documentation .

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.

Note: For more details on LiveData transformations, see the official documentation .

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.

  1. Initializes currentPage to be set to the first page or first bookmarked page if it exists.
  2. Gets Document passed to ReaderFragment.
  3. Gets the last document that was opened from GetOpenDocument.
  4. Sets the value of document to the one passed to ReaderFragment or falls back to lastOpenDocument if nothing was passed.
  5. 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.