قالب وردپرس درنا توس
Home / IOS Development / Clean Architecture Tutorial for Android: Getting Started

Clean Architecture Tutorial for Android: Getting Started



Are you looking for a good architecture for complex Android projects? Have you heard of Clean Architecture but don't know what all the fuss is about?

In this tutorial you will learn the basics of Clean Architecture, including:

  • Why it is important to use architecture patterns in software [19659004] What Clean Architecture is
  • What SOLID development principles are
  • When to use Clean Architecture and SOLID principles
  • How to implement Clean Architecture on Android

Getting Started

There has always been an open debate about which architectural designs to use on Android. Since the first few days, you got the feeling that things didn't work out the way they were set up. This in turn caused many people to struggle with architecture in general.

However, it has been quite a while for you to write your applications in a pure way. Furthermore, one of the most influential people in the global programming community, Robert C. Martin, also known as Uncle Bob, has written a book, specifically on this subject.

Because the Clean architecture can be used in all applications and platforms, not just Android, it is very informative to understand the idea behind it and why it is a good solution, for most problems we find today, as programmers. With that in mind, in this tutorial, you will learn how to use the Clean architecture pattern to build a Majestic Reader app, a simple PDF reader.

To get started, download the Majestic Reader project using the Download Materials button at the top or bottom of this tutorial. Open project in Android Studio 3.2 or later by selecting Open an existing Android Studio project on the Android Studio Welcome screen:

In case something goes wrong due to sync issues, just press the Gradle sync button so everything will go well! Finally, built and operates the project. You should see that the main screen of the app is currently empty:

The app consists of two screens:

  • A list of PDF documents in your library.
  • A PDF document reader.

Currently these are dummy screens. Your job is to implement library and reader functionality using Clean Architecture.

Clean Architecture

You may be wondering: Why should I use an architecture? I'm better than that. Well, bring me.

Why Architecture Is Important

All architectures have one common goal – to manage the complexity of your application. You may not have to worry about it in a smaller project, but it will be a life-saver for larger ones.

How does Clean Architecture approach this problem?

You may have already seen this graph:

The circuits represent different levels of software in your app. There are two important things to note:

  • The center circle is the most abstract, and the outer circle is the most concrete. This is called Abstraction Principle . The abstraction principle specifies that internal circuits should contain business logic, and external circuits should contain implementation details.
  • Another principle of pure architecture is the Dependency Rule . This rule specifies that each circle can only depend on the nearest inner circle – this is what makes the architecture work.

The outer circle represents the specific mechanisms specific to the platform such as network and database access. As we move inward, each circle is more abstract and higher. The center circle is the most abstract and contains business logic, which does not depend on the platform or framework you use.

Several benefits of using an architecture when structuring the app code include:

  • Parts of the code will be disconnected and easier to reuse and test.
  • It is a method of madness. When someone else is working on your code, they can learn the app's architecture and understand it better.

SOLID Principles

Five design principles make software design more understandable, flexible and maintainable. These principles are:

  • Single responsibility : Each software component should have only one reason to change – one responsibility.
  • Open-closed: You should be able to extend the behavior of a component, without interrupting its use, or changing its extensions.
  • Liskov substitution: If you have a class of one type and any subclasses of that class, you should be able to represent the base class usage with the subclass, without breaking the app.
  • Interface Segregation: It is better to have many smaller interfaces than one large one, to prevent the class from implementing the methods it does not need.
  • Dependency Inversion: Components should depend on abstractions rather than concrete implementations. Higher level modules should not depend on lower level modules.

Clean Architecture maximizes the use of these principles.

Layers of Clean Architecture

There are different opinions about how many layers of Clean Architecture should have. Architecture does not define exact layers, but instead lays the foundation. The idea is that you adapt the number of layers to your needs.

To keep things simple, use five layers:

  • Presentation : A layer that interacts with the UI.
  • Use cases : Sometimes called interactors. Defines actions the user can trigger.
  • Domain : Contains the business logic of the app.
  • Data : Abstract definition of all data sources.
  • Framework : Implements interaction with Android SDK and provides specific implementations for the data layer.

Layers marked in green depend on the Android SDK.

Project Structure

Since Pure Architecture can be used anywhere, it is important to know how to implement it on Android.

You want to divide the project into two modules:

  • The existing app module.
  • A new core module that will contain all the code that does not depend on the Android SDK.

The following graph shows communication between the teams and how they are arranged in modules:

The app module is already there, so start by creating a core module.

Right-click on MajesticReader in Project explorer and select New ▸ Module or select File ▸ New ▸ New Module .

In the wizard, select Java Library and click "Next".

Under library name, type core .

Click the Edit button next to the Java package name field and type com.raywenderlich.android.majesticreader . Then click Done .

Ignore Java Class Name and click Finish .

Wait for the degree to be synchronized. Open the build.gradle in the core module and add the following after the first application line application:

  Apply plugin: & # 39; kotlin & # 39;

In the dependencies section add two more dependencies:

  implementation "org.jetbrains.kotlin: kotlin-stdlib-jdk8: $ kotlin_version"
implementation "org.jetbrains.kotlinx: kotlinx-coroutines-android: 1.2.1"

Then you refer to the core module from the app module.

Open build.gradle in the app and add the following line under the dependency block:

  implementation project (& # 39 ;: core & # 39;)

Click Now sync in the upper right corner of the message list and wait for Gradle to sync.

Next, select com.raywenderlich.adroid .majesticreader in the kernel module. Right-click and select New ▸ package

Enter domain for the name of the new package.

Repeat the process and create two more packages: data and interactors .

Delete the Java class. created by the new module wizard. Now you need to implement each of the layers independently. :]

The Data and Business Logic Layers

You will work from the outermost abstract layers to the outer, more concrete layers.

The domain team

The domain team contains all the models and business rules for your app.

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

Click Refactor to complete the process.

Data layer

This layer provides abstract definitions for accessing data sources such as a database or the Internet. You will use Storage Pattern in this layer.

The main purpose of the repository pattern is to abstract the specific implementation of data access. To achieve this, you will 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 delegated to DataSource .

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

  • A data layer that is of a higher, more abstract level does not depend on a framework, a lower level layer.
  • The repository is an abstraction of data Access and it does not depend on details. It depends on abstraction.

Add repositories

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

Type BookmarkDataSource in the dialog box.

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

BookmarkDataSource interface {

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

suspend fun read (document: document): List 

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

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

Repeat the process and add another interface called DocumentDataSource :

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

DocumentDataSource interface {

suspend fun add (document: document)

suspend fun readAll (): List 

suspend fun remove (document: document)
}

Repeat the process and add the latest data source called OpenDocumentDataSource :

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

OpenDocumentDataSource interface {

funny setOpenDocument (document: document)

funny getOpenDocument (): Document
}

This data source will handle the storage and retrieval of the currently open PDF document. Then 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 funny addBookmark (document: document, bookmark: bookmark) =
dataSource.add (document, bookmark)

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

cancel fun removeBookmark (document: document, bookmark: bookmark) =
dataSource.remove (document, bookmark)
}

This is the Repository that you want to use to add, remove, and retrieve saved bookmarks in the app. Note that you select all methods with the suspend modifier. This allows you to use coroutine-driven mechanisms in the room for easier threading.

As an exercise, try adding DocumentRepository .

[spoiler title=”DocumentRepository”]

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

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

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

suspend fun getDocuments () = documentDataSource.readAll ()

cancel fun removeDocument (document: Document) = documentDataSource.remove (document)

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

funny getOpenDocument () = openDocumentDataSource.getOpenDocument ()
}

[/spoiler]

The Use Cases Layer

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

Majestic Reader has two important functions:

  • Displays and manages a list of documents in a library.
  • Allows the user to open a document and manage bookmarks in it.

From there, you can list the actions users can take:

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

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 call (document: document, bookmark: bookmark) =
bookmarkRepository.addBookmark (document, bookmark)
}

Each usage class has only one function that invokes the use case. For your convenience, you overload the on-call operator. This allows you to simplify the function shell of the AddBookmark instance to addBookmark () instead of addBookmark.invoke () .

Adding remaining cases [19659021] Repeat this procedure and add the classes for the remaining actions:

  • AddDocument
    [spoiler title=”AddDocument”]
      import com.raywenderlich.android.majesticreader.data.DocumentRepository
    import com.raywenderlich.android.majesticreader.domain.Document
    
    class AddDocument (private val documentRepository: DocumentRepository) {
    cancel operator fun call (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 call (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 call (document: document, bookmark: bookmark) = bookmarks Depot
    . remove bookmark (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]

Framework and UI

This completes the implementation of the three inner layers of the kernel module. You are now ready to move on to the remaining layers: Framework and Presentation . Both of these layers depend on the Android SDK, so you place them in the app module.

The Framework Layer

The Framework layer contains interface implementations defined in the Data Layer.
Your next task is to provide data source interface implementations from the data layer. Start with OpenDocumentDataSource . It will store the currently open document in memory and is the easiest.

Create a new file in the app module in com.raywenderlich.android.majesticreader.framework called 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 implementation is quite straightforward.

Adding remaining data sources

You will use the remaining data sources to delegate and maintain data in the database using the Room library. The classes required for persistent Bookmarks and Document via Room are in the db subpackage.

Create a new Kotlin file named RoomBookmarkDataSource in framework . Add the following code to 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 suspended fun (Document: Document): List  = 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)
)
}

This is what the code does, step by step:

  1. Use MajesticReaderDatabase to get an example of BookmarkDao and save 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 to 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  = 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)
)
}

What remains to be done is to connect all the dots and view the data.

Presentation layer

This layer contains user interface related code. This layer is in the same circle as the frame layer, so you can depend on the class.

Using MVVM

You are using the MVVM pattern in this layer because it is supported by Android Jetpack. Note that it doesn't matter what pattern you use for this team, and you can use whatever suits your needs best, whether it's MVP, MVI or something else.

For a quick introduction, here is a chart:

The MVVM pattern consists of three components:

  • View : responsible for drawing user interface
  • Model : Contains business logic and data.
  • ViewModel : Works as a bridge between data and UI.

In pure architecture, instead of relying on models, you want to interact with Interactors from the Use Case team.

This team contains user interface related code, powered by Android Jetpack! :]

Specify Sources

Before proceeding to implement the presentation layer, you need a way to provide the data sources to the data layer. You should usually do this by using dependency injection . It is the process of separating vendor functions or factories for addictions and their use. This makes your classes cleaner as they do not create dependencies on their designers.

Note : To fully utilize pure architecture, you can use an addiction injection frame such as Dagger 2 or Koin.

To keep things simple, you implement a simple way to manually obtain ViewModels dependencies.

First replace the empty class Interactors in the frame namespace with the data class containing all interactors:

  import com.raywenderlich.android.majesticreader.interactors. *

data class interactors (
drop addBookmark: AddBookmark,
fall getBookmarks: GetBookmarks,
select deleteBookmark: RemoveBookmark,
drop addDocument: AddDocument,
val getDocuments: GetDocuments,
val removeDocument: RemoveDocument,
fall getOpenDocument: GetOpenDocument,
val setOpenDocument: SetOpenDocument
)

You want to use it to access ViewModels interactors.

Open MajesticReaderApplication and replace onCreate () with the following, and make sure you add all necessary imports: [19659090] overrides 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 dependencies into MajesticViewModelFactory . It creates ViewModels in the app and provides interactor dependencies to them.

Ending everything required for addiction injection. Now back to the presentation team.

Implementing MVVM

Open LibraryViewModel.kt in com.raywenderlich.android.majesticreader.presentation.library .
ViewModel contains functions for loading the list of documents and adding a new one to the list. It acts as a connection between the user interface and the interactions, or use cases.

First replace loadDocuments () with the following:

  fun loadDocuments () {
GlobalScope.launch {
documents.postValue (interactors.getDocuments ())
}
}

This retrieves the list of documents from the library using the GetDocuments interactor, from a coroutine, which you start by calling launch () . When done, add the result to the documents LiveData.

Note: You should not rely on GlobalScope often, in your code, but for simplicity you will use it in this project.

Next, for addDocument () you will also 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, first start a coroutine, as before, then use withContext () to move the database entry to an IO-optimized thread and suspend until insertion is complete. Finally, reload the documents to update the list.

Finally, setOpenDocument () calls the current interactor:

  fun setOpenDocument (document: Document) {
interactors.setOpenDocument (document)
}

Now builds and runs the app. You can now add new documents to the library. Finally, you can bear the fruits of your labor! :]

Press the floating action button. You will see a screen to pick a document from the storage location. After you add a document, you will see it in the list.

There is another screen left – the reader screen.

Reading Documents

Open ReaderViewModel in com.raywenderlich.android.majesticreader.presentation.reader . There are a few places marked with // TODO comments that you want to add code to.

Here is an overview of ReaderViewModel with features that ReaderFragment will call 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.
  • previous page () : 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 cause a document to appear as an argument when 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 be filled with updated bookmarks, which you get from the interactions, in a coroutine. Your bookmark field should now look like this:

  val bookmarks = MediatorLiveData <List > (). Applies to {
addSource (document) {document ->
GlobalScope.launch {
postValue (interactors.getBookmarks (document))
}
}
}

-dokumentet inneholder dokumentet som er analysert fra Fragment-argumenter. bokmerker inneholder listen over bokmerker i det gjeldende dokumentet. ReaderFragment vil abonnere på det for å få listen over tilgjengelige bokmerker.

Rendering PDFs

For å gjengi PDF-dokumentsidene, bruk PdfRenderer som er tilgjengelig i Android SDK siden API-nivå 21.

currentPage har referansen til PdfRenderer.Page som du for øyeblikket viser, hvis noen. renderer har en referanse til PdfRenderer brukt for gjengivelse av dokumentet. Each time you change the document‘s internal valueyou 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 = MediatorLiveData().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 documentcurrentPage 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 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))
    } annet {
      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 bookmarksto 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)
    } annet {
      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.

Where to Go From Here?

For more practice, check out this article on MVVM and Data Binding and Uncle Bob’s article on Clean architecture.

I hope you’ve enjoyed this tutorial on Clean Architecture. And if you have any comments or questions, please join the forum discussion below!


Source link