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

The Data and Business Logic Layers

You’ll work your way from the centermost abstract layers to the outer, more concrete layers.

The Domain Layer

The Domain layer contains all the models and business rules of your app.

Move the Bookmark and Document models provided in the starter project to the core module. Select Bookmark and Document files from app module and drag them to the com.raywenderlich.android.majesticreader.domain package in the core module. You’ll see a dialog:

Click on Refactor to finish the process.

The Data Layer

This layer provides abstract definitions for accessing data sources like a database or the internet. You’ll use Repository pattern in this layer.

The main purpose of the repository pattern is to abstract away the concrete implementation of data access. To achieve this, you’ll add one interface and one class for each model:

  • DataSource interface: The interface that the Framework layer must implement.
  • Repository class: Provides methods for accessing the data that delegate to DataSource.

Using the repository pattern is a good example of the Dependency Inversion Principle because:

  • A Data layer which is of a higher, more abstract level doesn’t depend on a framework, lower-level layer.
  • The repository is an abstraction of Data Access and it does not depend on details. It depends on abstraction.

Adding Repositories

Select com.raywenderlich.android.majesticreader.data in the core module. Add a new Kotlin file by right-clicking and selecting New ▸ Kotlin File/Class.

Type BookmarkDataSource in the dialog.

Click Finish. Open the new file and paste the following code after the first line:

import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document

interface BookmarkDataSource {

  suspend fun add(document: Document, bookmark: Bookmark)

  suspend fun read(document: Document): List<Bookmark>

  suspend fun remove(document: Document, bookmark: Bookmark)
}

You’ll use this interface to provide the Bookmark data access from the Framework layer.

Repeat the process and add another interface named DocumentDataSource:

import com.raywenderlich.android.majesticreader.domain.Document

interface DocumentDataSource {

  suspend fun add(document: Document)

  suspend fun readAll(): List<Document>

  suspend fun remove(document: Document)
}

Repeat the process and add the last data source named OpenDocumentDataSource:

import com.raywenderlich.android.majesticreader.domain.Document

interface OpenDocumentDataSource {

  fun setOpenDocument(document: Document)

  fun getOpenDocument(): Document
}

This data source will take care of storing and retrieving the currently opened PDF document. Next, add a new Kotlin file named BookmarkRepository to the same package and copy and paste the following code:

import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document

class BookmarkRepository(private val dataSource: BookmarkDataSource) {
  suspend fun addBookmark(document: Document, bookmark: Bookmark) =
    dataSource.add(document, bookmark)

  suspend fun getBookmarks(document: Document) = dataSource.read(document)

  suspend fun removeBookmark(document: Document, bookmark: Bookmark) =
    dataSource.remove(document, bookmark)	
} 

This is the Repository that you’ll use to add, remove and fetch stored bookmarks in the app. Note that you mark all the methods with the suspend modifier. This allows you to use coroutine-powered mechanisms in Room, for simpler threading.

Note: If you want to know more about Coroutines check out the official Kotlin documentation or our Kotlin Coroutines book!

As an exercise, try adding DocumentRepository.

[spoiler title=”DocumentRepository”]

import com.raywenderlich.android.majesticreader.domain.Document

class DocumentRepository(
    private val documentDataSource: DocumentDataSource,
    private val openDocumentDataSource: OpenDocumentDataSource) {

  suspend fun addDocument(document: Document) = documentDataSource.add(document)	

  suspend fun getDocuments() = documentDataSource.readAll()
  
  suspend fun removeDocument(document: Document) = documentDataSource.remove(document)

  fun setOpenDocument(document: Document) = openDocumentDataSource.setOpenDocument(document)	

  fun getOpenDocument() = openDocumentDataSource.getOpenDocument()
}

[/spoiler]

The Use Cases Layer

This layer converts and passes user actions, also known as use cases, to inner layers of the application.

Majestic Reader has two key functionalities:

  • Showing and managing a list of documents in a library.
  • Enabling the user to open a document and manage bookmarks in it.

From that, you can list the actions users should be able to perform:

  • Adding a bookmark to a currently open document.
  • Removing a bookmark from a currently open document.
  • Getting all bookmarks for currently open documents.
  • Adding a new document to the library.
  • Removing a document from the library.
  • Getting documents in the library.
  • Setting currently opened documents.
  • Getting currently opened documents.

Your next task is to create a class that represents each use case.

Create a new Kotlin file named AddBookmark in com.raywenderlich.android.majesticreader.interactors. Add the following code after the package statement:

import com.raywenderlich.android.majesticreader.data.BookmarkRepository
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document

class AddBookmark(private val bookmarkRepository: BookmarkRepository) {
  suspend operator fun invoke(document: Document, bookmark: Bookmark) =
      bookmarkRepository.addBookmark(document, bookmark)
}

Each use case class has only one function that invokes the use case. For convenience, you’re overloading the invoke operator. This enables you to simplify the function call on AddBookmark instance to addBookmark() instead of addBookmark.invoke().

Note: For more details on overloading operators, see the official Kotlin documentation .

Adding The Remaining Use Cases

Repeat this procedure and add the classes for the remaining actions:

[/spoiler]

[/spoiler]

[/spoiler]

[/spoiler]

[/spoiler]

[/spoiler]

[/spoiler]

  • AddDocument
    [spoiler title=”AddDocument”]
    import com.raywenderlich.android.majesticreader.data.DocumentRepository
    import com.raywenderlich.android.majesticreader.domain.Document
    
    class AddDocument(private val documentRepository: DocumentRepository) {
      suspend operator fun invoke(document: Document) = documentRepository.addDocument(document)
    }
    

    [/spoiler]

  • GetBookmarks
    [spoiler title=”AddDocument”]
    import com.raywenderlich.android.majesticreader.data.BookmarkRepository
    import com.raywenderlich.android.majesticreader.domain.Document
    
    class GetBookmarks(private val bookmarkRepository: BookmarkRepository) {
    
      suspend operator fun invoke(document: Document) = bookmarkRepository.getBookmarks(document)
    }
    

    [/spoiler]

  • GetDocuments
    [spoiler title=”GetDocuments”]
    import com.raywenderlich.android.majesticreader.data.DocumentRepository
    
    class GetDocuments(private val documentRepository: DocumentRepository) {
      suspend operator fun invoke() = documentRepository.getDocuments()
    }
    

    [/spoiler]

  • GetOpenDocument
    [spoiler title=”GetOpenDocument”]
    import com.raywenderlich.android.majesticreader.data.DocumentRepository
    
    class GetOpenDocument(private val documentRepository: DocumentRepository) {
      operator fun invoke() = documentRepository.getOpenDocument()
    }
    

    [/spoiler]

  • RemoveBookmark
    [spoiler title=”RemoveBookmark”]
    import com.raywenderlich.android.majesticreader.data.BookmarkRepository
    import com.raywenderlich.android.majesticreader.domain.Bookmark
    import com.raywenderlich.android.majesticreader.domain.Document
    
    class RemoveBookmark(private val bookmarksRepository: BookmarkRepository) {
      suspend operator fun invoke(document: Document, bookmark: Bookmark) = bookmarksRepository
          .removeBookmark(document, bookmark)
    }
    

    [/spoiler]

  • RemoveDocument
    [spoiler title=”RemoveDocument”]
    import com.raywenderlich.android.majesticreader.data.DocumentRepository
    import com.raywenderlich.android.majesticreader.domain.Document
    
    class RemoveDocument(private val documentRepository: DocumentRepository) {
      suspend operator fun invoke(document: Document) = documentRepository.removeDocument(document)
    }
    

    [/spoiler]

  • SetOpenDocument
    [spoiler title=”SetOpenDocument”]
    import com.raywenderlich.android.majesticreader.data.DocumentRepository
    import com.raywenderlich.android.majesticreader.domain.Document
    
    class SetOpenDocument(private val documentRepository: DocumentRepository) {
      operator fun invoke(document: Document) = documentRepository.setOpenDocument(document)
    }
    

    [/spoiler]

import com.raywenderlich.android.majesticreader.data.DocumentRepository
import com.raywenderlich.android.majesticreader.domain.Document

class AddDocument(private val documentRepository: DocumentRepository) {
  suspend operator fun invoke(document: Document) = documentRepository.addDocument(document)
}
import com.raywenderlich.android.majesticreader.data.BookmarkRepository
import com.raywenderlich.android.majesticreader.domain.Document

class GetBookmarks(private val bookmarkRepository: BookmarkRepository) {

  suspend operator fun invoke(document: Document) = bookmarkRepository.getBookmarks(document)
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository

class GetDocuments(private val documentRepository: DocumentRepository) {
  suspend operator fun invoke() = documentRepository.getDocuments()
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository

class GetOpenDocument(private val documentRepository: DocumentRepository) {
  operator fun invoke() = documentRepository.getOpenDocument()
}
import com.raywenderlich.android.majesticreader.data.BookmarkRepository
import com.raywenderlich.android.majesticreader.domain.Bookmark
import com.raywenderlich.android.majesticreader.domain.Document

class RemoveBookmark(private val bookmarksRepository: BookmarkRepository) {
  suspend operator fun invoke(document: Document, bookmark: Bookmark) = bookmarksRepository
      .removeBookmark(document, bookmark)
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository
import com.raywenderlich.android.majesticreader.domain.Document

class RemoveDocument(private val documentRepository: DocumentRepository) {
  suspend operator fun invoke(document: Document) = documentRepository.removeDocument(document)
}
import com.raywenderlich.android.majesticreader.data.DocumentRepository
import com.raywenderlich.android.majesticreader.domain.Document

class SetOpenDocument(private val documentRepository: DocumentRepository) {
  operator fun invoke(document: Document) = documentRepository.setOpenDocument(document)
}