Asynchronism in mobile
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()
andonResume()
. 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 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 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
Handling 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 Decorator
s 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 Presenter
s. And if for one case you need parallelism, just create another executor.