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 3 of 5 of this article. Click here to view the first page.

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.

Note: for more information on Room, check our Room tutorial .

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:

  1. Use MajesticReaderDatabase to get an instance of BookmarkDao and store it in local field.
  2. 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.
Note: For more information about MVVM, check out official documentation and our tutorial!

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.

Note: To fully leverage Clean Architecture you can use a dependency injection framework like Dagger 2 or Koin.

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.

Note: For more details on ViewModel factories, check the official documentation .

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.

Note: You shouldn’t rely on 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.