An overview of the

I first learned about MvpClean out of curiosity while searching architecture component blogs. My first impression of MvpClean was that it was annoying to create n files to write a feature, but there is no denying that MvpClean has great advantages for large projects and collaborative development.

The Clean architecture

In the Clean architecture, code is layered into an onion, wrapped in layers, with one dependency rule: the inner layer cannot depend on the outer layer, that is, the inner layer does not know anything about the outer layer, so the architecture is dependent inward. Take a look at this picture to feel it:

The Clean architecture enables code to have the following characteristics: 1. Easy to test 3. independent of UI 4. independent of database 5. independent of any external class library

MvpClean in Android

A common Android app typically requires the following three layers:

  • Outer layer: Implementation Layer – The interface implementation layer is where the architectural details are embodied. The code that implements the architecture is all the code that doesn’t have to solve a problem. This includes all android-related code, such as creating activities and fragments, sending intents, and other architecture related to networking and databases.

  • Middle layer: Interface adaptation layer – The purpose of the interface adaptation layer is to bridge code from the logical layer to the architecture layer.

  • Inner Layer: Logical layer — The logical layer contains the code that actually solves the problem. This layer does not contain any code to implement the architecture, and it should be possible to run the code without emulators. This gives logical code the advantage of being easy to test, develop, and maintain.

Each layer outside the core layer should be able to turn the external model into an internal model that can be processed by the inner layer. The inner layer cannot hold references to model classes belonging to the outer layer. For example, when the logical layer model cannot be presented directly and elegantly to the user, or when multiple logical layer models need to be presented simultaneously, it is best to create a ViewModel class for better UI presentation. In this case, an outer Converter class is needed to convert the logical layer model into an appropriate ViewModel.

Project structure Generally speaking, the structure of an Android application is as follows: Outer project package: UI, Storage, Network, etc. Middle level project package: Presenter, Converter. Inner project package: Interactor, Model, Repository, Executor.

The outer layer

The outer layer embodies the details of the frame:

  • UI – Includes all activities, fragments, Adapters, and other UI-related Android code.
  • Storage – Interface implementation class for interaction classes to get and store data, containing database-related code. Include components such as ContentProvider or DBFlow.
  • Network – Network operations.

In the middle

Glue Code for bridging implementation Code and logic Code:

  • Presenter – Presenter handles UI events, such as a click event, and usually contains callback methods for the inner Interactor.
  • Converter – Is responsible for converting inner and outer layer models to each other.

The inner layer

The inner layer contains the highest level of code, full of POJO classes. The classes and objects in this layer do not know anything about the outer layer and should be able to run under any JVM:

  • Interactor – Interactors contain logic code to solve problems. The code here executes in the background and passes events to the outer layer via callback methods. In other projects this module is called a Use Case. There may be many small interactors in a project, which is consistent with the single responsibility principle and makes it more acceptable.
  • Model – The business Model that operates in the business logic code.
  • Repository – Contains interfaces for outer classes to implement, such as those that operate on databases. Interactors use the implementation classes of these interfaces to read and store data. This is also called Repository Pattern.
  • Executor – Thread Executor allows Interactor to execute in the background. There is generally no need to modify the code in this package.

A personal feeling can be interpreted as a transaction

Online Clean code

Writing from the logical layer helps advance testing

  • Domain package

Create initial base class:

interface Interactor {
    fun execute()
}Copy the code

Implementing Interactor in the background:

abstract class AbstractInteractor constructor(val threadExecutor: Executor, val mainThread: MainThread) : Interactor {

    protected var mIsCanceled: Boolean = false
    protected var mIsRunning: Boolean = false

    abstract fun run()

    fun cancel() {
        mIsCanceled = true
        mIsRunning = false
    }

    fun isRunning() = mIsRunning

    fun onFinished() {
        mIsRunning = false
        mIsCanceled = false
    }

    override fun execute() {
        // mark this interactor as running
        mIsRunning = true

        // start running this interactor in a background thread
        threadExecutor.execute(this)
    }
}Copy the code
interface MainThread {
    fun post(runnable: Runnable)
}Copy the code

The implementation ensures that the Runnable object runs in the UI thread

class MainThreadImpl : MainThread { companion object { private var sMainThread: MainThread? = null fun getInstance() = sMainThread ? : synchronized(this) { sMainThread ? : MainThreadImpl().also { sMainThread = it } } } private val mHandler: Handler = Handler(Looper.getMainLooper()) override fun post(runnable: Runnable) { mHandler.post(runnable) } }Copy the code

MainThreadImpl needs to be initialized on the main thread

Background thread pool:

interface Executor {
    fun execute(interactor: AbstractInteractor)
}Copy the code
/** * This singleton class will make sure that each interactor operation gets a background thread. */ class ThreadExecutor : Executor { companion object { // This is a singleton private var sThreadExecutor: ThreadExecutor? = null private val CORE_POOL_SIZE = 3 private val MAX_POOL_SIZE = 5 private val KEEP_ALIVE_TIME = 120 private val TIME_OUT = TimeUnit.SECONDS private val WORK_QUEUE = LinkedBlockingQueue<Runnable>() /** * Returns a singleton instance of this executor. If the executor is not initialized then it initializes it and returns * the instance. */ fun getInstance() = sThreadExecutor ? : synchronized(this) { sThreadExecutor ? : ThreadExecutor().also { sThreadExecutor = it } } } private var mThreadPoolExecutor: ThreadPoolExecutor init { val keepAlive = KEEP_ALIVE_TIME.toLong() mThreadPoolExecutor = ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, keepAlive, TIME_OUT, WORK_QUEUE) } override fun execute(interactor: AbstractInteractor) { mThreadPoolExecutor.submit { // run the main logic interactor.run() // mark it as finished interactor.onFinished() } } }Copy the code

Write the inner Interactor

Start by writing an Interactor that contains code to handle the business logic. All interactors should run in the background without affecting the UI presentation.

interface WelcomingInteractor : Interactor {
    interface Callback {
        fun onSuccess(msg: String)
        fun onFailed(error: String)
    }
}Copy the code

Callback communicates with the main thread’s UI component, and having it in WelcomingInteractor avoids naming all Callback interfaces differently and allows them to be distinguished.

Implement the logic to get the message to get the data:

interface MessageRepository {
    fun getWelcomeMsg(): String
}Copy the code

Implement the Interactor interface with business logic code:

class WelcomingInteractorImpl constructor(threadExecutor: ThreadExecutor, mainThread: MainThread, val callback: WelcomingInteractor.Callback, val messageRepository: MessageRepository) : AbstractInteractor(threadExecutor, mainThread), WelcomingInteractor { private fun notifyError() { mainThread.post { callback.onFailed("nothing to welcome you") } } private fun postMessage(msg: String) { mainThread.post { callback.onSuccess(msg) } } override fun run() { val msg = messageRepository.getWelcomeMsg()  if (msg == null || msg? .length == 0) { notifyError() return } postMessage(msg) } }Copy the code

This Interactor gets the data and then sends it or an error to the UI layer, which is the heart of the logic. This sends information to the UI through a Callback that plays the role of presenter.

Note: By implementing the AbstractInteractor interface, the code is executed in the background.

Because the code above does not rely on the Android libraries, you can test Junit directly

Write the presentation layer

  • The presentation package

The Presentation layer is the outer layer of the architecture that depends on the framework and contains the code for the UI Presentation.

Create initial base class:

interface BaseView {
    fun showProgress()
    fun hideProgress()
    fun showError()
}Copy the code
interface BasePresenter {
    fun resume()
    fun pause()
    fun stop()
    fun destroy()
    fun onError(msg: String)
}Copy the code
abstract class AbstractPresenter(val executor: Executor,
                                 val mainThread: MainThread) {
}Copy the code

Specific performance:

Write the Presenter and View interface first:

interface MainPresenter : BasePresenter {
    interface View : BaseView {
        fun displayWelMsg(msg: String)
    }
}Copy the code
class MainPresenterImpl(threadExecutor: Executor,
                        mainThread: MainThread,
                        val view: MainPresenter.View,
                        val messageRepository: MessageRepository) : AbstractPresenter(threadExecutor, mainThread), MainPresenter, WelcomingInteractor.Callback {
    override fun resume() {
        view.showProgress()

        val interactor = WelcomingInteractorImpl(
                executor,
                mainThread,
                this,
                messageRepository
        )

        interactor.execute()
    }

    override fun pause() {
    }

    override fun stop() {
    }

    override fun destroy() {
    }

    override fun onError(msg: String) {
        view.showError(msg)
    }


    override fun onSuccess(msg: String) {
        view.hideProgress()
        view.displayWelMsg(msg)
    }

    override fun onFailed(error: String) {
        view.hideProgress()
        onError(error)
    }
}Copy the code

Inheriting BasePresenter and overriding the onResume() method to start Interactor, the execute() method calls the Run () method of the WelcomingInteractorImpl class in a background thread.

Pass the following properties to Interactor in the code above: -threadeXECUtor: Used to run Interactor in background threads. It is recommended to design this class as a singleton. This class is part of the Domain package and does not need to be implemented in the outer layer. – MainThreadImpl: Runnable object used to execute Interactor in the main thread. The main thread is accessible in the outer layer of code that depends on the framework, so this class is implemented in the outer layer. – This: Because MainPresenter also implements the Callback interface, Interactor updates the UI via Callback. – MessageRepository: Implements the WelcomMessageRepository class to get data from Interactor, as shown below.

Listen for Interactor events by implementing the Callback interface:

    override fun onSuccess(msg: String) {
        view.hideProgress()
        view.displayWelMsg(msg)
    }

    override fun onFailed(error: String) {
        view.hideProgress()
        onError(error)
    }Copy the code

A view is a UI-level object that implements the MainPresenter.View interface, such as an activity

class MainActivity : AppCompatActivity(), MainPresenter.View { companion object { private val TAG = this::class.java.simpleName } private lateinit var mPresenter:  MainPresenter override fun showProgress() { Log.d(TAG, "show") } override fun hideProgress() { Log.d(TAG, "hide") } override fun showError(msg: String) { Log.d(TAG, "error") } override fun displayWelMsg(msg: String) { Log.d(TAG, msg) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mPresenter = MainPresenterImpl( ThreadExecutor.getInstance(), MainThreadImpl.getInstance(), this, WelcomeMsgRepository()) } override fun onResume() { super.onResume() mPresenter.resume() } }Copy the code

Write the Storage layer

  • Storage bag

The interface in repository is implemented at the storage layer. All the data related code is here. The source of the data in the repository mode is uncertain, whether it is a database, server, or file.

class WelcomeMsgRepository : MessageRepository { override fun getWelcomeMsg(): String {val MSG = "Hello World" try {thread.sleep (2000)} catch (e: Exception) { e.printStackTrace() } return msg } }Copy the code

This is the data source above. In practical applications, it can be encapsulated according to different data sources or some caching technologies can be added

conclusion

As you can see, the MvpClean architecture further decouples the logical layer, which is more decoupled than the MVP architecture, but it also creates more classes, so it’s better for large projects than it is for small projects.

The official demo