Asynchronism in mobile

le 11/04/2017 par Nicolas Mouchel
Tags: Software Engineering

In Android, asynchronous tasks are done to avoid long operations in the main thread. Android documentation gives a good advice to the community to avoid ANR (Android Not Responding):

Therefore, any method that runs in the UI thread should do as little work as possible on that thread. In particular, activities should do as little as possible to set up in key life-cycle methods such as onCreate()and onResume(). Potentially long running operations such as network or database operations, or computationally expensive calculations such as resizing bitmaps should be done in a worker thread (or in the case of databases operations, via an asynchronous request). -- Keeping Your App Responsive - How to Avoid ANRs

The point is that long operations like network, file system or database can freeze the UI, and that these kinds of operations must be done in a worker thread rather than in the main thread.

What is often misunderstood here is where to put asynchronism. The most current pattern is to protect the UI from long operations. Thus, long operations are wrapped with AsyncTask, Service, Thread, Executor or libraries which can be called asynchronously.

MVP long operation isolatedMVP architecture with isolated long operations

But there is another approach to this problem: instead of isolating long operations from the main thread, it is possible to isolate the main thread from all other operations (long or short).

MVP view isolatedMVP architecture with isolated view

The theory is pretty simple. With an architecture like MVP, View is isolated from the rest of the code base. There is also an abstraction between View and Presenter: interfaces. Some say that interfaces are a waste of time, but in this case, there will be two useful implementations for each layer:

  • a decorator for the presenter that executes methods on worker thread
  • a decorator for the view that executes methods on the main thread

Thread swapping schemeHandling threads with Executor and Decorator

A example of this principle in kotlin code:

interface View {
    fun display(viewModel: ViewModel)
}

data class ViewModel(val name: String)

interface Presenter {
    fun doOperation()
}

Define interfaces for View and Presenter, and data class for ViewModel

class DecoratorPresenter(val executor: Executor, val decorated: Presenter) : Presenter {
    override fun doOperation() = executor.execute { decorated.doOperation() }
}

class DecoratorView(val executor: Executor, val decorated: WeakReference) : View {
    override fun display(viewModel: ViewModel) = executor.execute {
        decorated.get()?.display(viewModel)
    }
}

Implementations of Decorators for View and Presenter

val mainThreadExecutor = object : Executor {
    val handler = Handler(Looper.getMainLooper())

    override fun execute(action: Runnable) {
        handler.post(action)
    }
}

Implementation of an Executor with a Handler to move from the Worker thread to the Main thread

val otherThreadExecutor = Executors.newSingleThreadExecutor()

Use a simple Executor to leave the Main thread for a Worker thread

class RealPresenter(val view: View) : Presenter {
    override fun doOperation() {
        view.display(ViewModel("name"))
    }
}

class RealView: View{

    lateinit var presenter: Presenter

    fun onCreate() {
        val view = DecoratorView(mainThreadExecutor,WeakReference(this))
        val realPresenter: Presenter = RealPresenter(view)
        val presenter = DecoratorPresenter(otherThreadExecutor, realPresenter)
    }

    override fun display(viewModel: ViewModel) {
        // do stuff
    }

    fun onAction(){
        presenter.doOperation()
    }
}

Real implementations of Presenter and View

Decorators are boilerplate codes that can be easily generated, and there is a library for that Executor Decorator

To conclude, managing threads is a real responsibility that you need to handle on your own. By mastering it, you can easily build acceptance test suites with Robolectric and even Espresso (see custom IdlingResource)

Note: If you want to avoid parallelism in your application, share the single thread executor between Presenters. And if for one case you need parallelism, just create another executor.