Reading Coroutine official guide thoroughly — Part 5 — Dive 1

Callback, Reactive and Coroutine

Myungpyo Shim
7 min readAug 18, 2021

[Korean] English

(Return to the table of contents)

As a developer, you and I always eager to study brand new shining tech tools that make us productive and comfortable. However we would go through hard time to study and understand those new tools or skills. Especially we might feel more painful when the way of using it or the concept of it is pretty difficult and complicate to understand. When you bumped into the circumstances, you might look over your pre-existing knowledge which are similar to the tool. If you find some tools you’ve already known, you can easily understand the concept of the new skill for good through contrast and comparison between them 😎 . The reason why this strategy can work well is the most of the new tools evolved from its ancestors. If we had studied or used the origin of the new tool, we can infer the internal operation of it from the knowledge we’ve already got.

Today, I am going to make a sample application with three different ways: callback, Rx and Coroutine so that I can compare those three ways; after that I am going to look inside of coroutine in order to figure out the role of suspending function and coroutine job and the relationship between them.

Scenario
- Provide functionality that can sync user settings.
- User settings are stored locally and synced from remote server on-demand.
- User can have local-only settings.
- User cannot modify synced settings directly.

The fourth spec above is to simplify the example. I am going to make application layers like below.

- main function : mimic UI Views (Main thread)
- ViewModel : not extends from Android ViewModel but use simplified one.
- UserSettingRepository : abstract data layer as handling various of data sources.
- LocalUserSettingDataSource : represents device local data source such as local database and preferences.
- RemoteUserSettingDataSource : reporesents remote data source such as remote database through API.

Implementation using Callback

Let’s use callback to implement the new feature.
You can see the full source code here.

Let’s take a look the lowest layer — DataSource layer.

Local and Remote data source implement UserSettingDataSource interface and their implementations merely wait for a second for mocking long running task.

Let’s take a look the repository layer which uses those data sources.

In this example, the repository layer chooses which thread its exposed functions are executed. This time, I am using cachedThreadPool because all of the tasks are I/O bound ones.

syncUserSetting function do the folowing with the parameters of userId and callback for returning the result.

  1. Load user settings from remote data source
  2. Load user settings from local data source
  3. Sync user setting from remote to local and update to local data source.

All of the steps above are executed as using callback functions.

In this example, the result Future object from ExecutorService.submit() is transformed as self-defined form of Disposable. This disposable is used to cancel the task when the UI finishes (ViewModel.onClear()).

See below to check the ViewModel code which uses the repository.

ViewModel requests user setting synchronization when the syncUserSetting() function is called and cache the result disposable in order to cancel the request when UI finishes.

See below to check the result.

[main] SampleViewModel 
: syncUserSetting() — start
[Thread-0]UserSettingRepository
: syncUserSetting() — Fetch from remote data source
[Thread-0] UserSettingRepository
: syncUserSetting() — Load from local data source
[Thread-0] UserSettingRepository
: syncUserSetting() — Update to local data source
[Thread-0] UserSettingRepository
: syncUserSetting() — Success
[Thread-0] SampleViewModel : syncUserSetting()
: success : UserSetting(userId=TestUser#1, primaryColor=FFFF0000, secondaryColor=FF0000FF)
[main] SampleViewModel
: onClear()

Implementation using Rx

I am going to implement the same functionality with Rx instead of callback that you saw in the previous section.

You can see the whole project here.

Let’s see the DataSource first which is the deepest layer of the project.

The APIs that are offered from DataSource no longer have callback parameter. Instead, they return Rx Single for future subscription.

Next code snippet is part of repository code that uses the datasource above.

The repository gets user settings from remote and local data sources and merges (zip) them and update local data source with it.

Next code is the ViewModel class which uses the repository.

the ViewModel class choose which thread it will use when it subscribe the result from the repository; in addition it caches the disposable from the subscription in order to cancel active tasks when the UI finishes (ViewModel.onClear).

See below to check the result (Thread name is attached in front of the log).

main SampleViewModel 
: syncUserSetting() - start
RxCachedThreadScheduler-1 RemoteUserSettingDataSource
: loadUserSetting()
RxCachedThreadScheduler-1 LocalUserSettingDataSource
: loadUserSetting()
RxComputationThreadPool-1 LocalUserSettingDataSource
: updateUserSetting()
main SampleViewModel
: syncUserSetting() : success : UserSetting(userId=TestUser#1, primaryColor=FFFF0000, secondaryColor=FF0000FF)
main SampleViewModel
: onClear()

Comparison between Callback and Rx

Those examples are shoddy ones but I am trying to compare them.

  • 👍 With the Rx, you can easily specify which thread should be used to execute the task and to get the result.
    (Of course, you can use executorThreadPool to execute the task and handler to post the result.)
  • 👍 With the Rx, You can the whole procedure you need to make the final result is specified in a row (operators chain) and it increases the readability of the code.
    (Of course, Rx is relatively difficult to use its advanced features. However callback is intuitive although it makes the callback-hell 😈)
  • 👍 With the Rx, When cancellation is occurred, it is up to each operator to conform the cancellation and most of the standard operators are aware of cancellation and cancel the operator immediately.
    With the Callback, To conforming the cancellation event, the implementor should check whether this task is cancelled or not repeatedly as using Thread.interrupted().
  • 👍 With the Rx, you can handle all of the errors as specifying onError handler when you subscribe the rx observable; however you have to use try-catch block to handle and transfer error event to the caller when you use callback.
  • 😰 In the past, debugging the rx chain was not supported properly. On the contrary, you can use break point on the rx chain these days; however many of developers still use simple logging as inserting doOnNext(Log...) in the middle of chain since the break points are not working from time to time.

Implementation using Coroutine

Let’s meet the rising star coroutine! I’ll show you how the code looks different when you use coroutine instead of aforementioned two ways.

You can see the whole project source code here.

Let’s dive into the implementation in the same way we’ve done. The DataSource layer is the first one we will see.

All of the APIs of the DataSource layer use suspending function; So they directly return the specific result type without any warpper. Further more, the implementation detail of each function in DataSource layer looks similar to usual function call.

Let’s take a look at the Repository class.

You can be noticed the internal code of syncUserSetting() function is written in sequential way (You can use async { } coroutine builder to request user settings to all the data sources simultaneously).

Finally, let’s see the code in our ViewModel.

In the code above, the ViewModel implements CoroutineScope; thus it cancels all the coroutines launched in the ViewModelScope when the UI finishes (This is automatically processed when you use androidx.ViewModel but I am using self-defined view model here).

In addition, you can find the code that switch dispatcher to I/O in the syncUserSetting() function so that actual sync logic can be executed on the worker thread of I/O thread pool. Of course, the result after withContext(IO) block is processed in the main thread (runCatching is built-in extension function of kotlin and it provides try-catch functionality to wrap the result as Result<T>.

❗️Notice
IMHO, you should specify the dispatcher which is used to execute the function in the layer that the function is defined. In this way, caller function doesn't have to care about the dispatcher and just call the suspending function in a coroutine.

You can see the result below (Thread and Coroutine name is attached in front of each log).

main @coroutine#1 SampleViewModel 
: syncUserSetting()
main @coroutine#2 SampleViewModel
: syncUserSetting() start
DefaultDispatcher-worker-1 @coroutine#2 UserSettingRepository
: syncUserSetting() - Fetch from remote data source
DefaultDispatcher-worker-1 @coroutine#2 RemoteUserSettingDataSource
: loadUserSetting()
DefaultDispatcher-worker-1 @coroutine#2 UserSettingRepository
: syncUserSetting() - Load from local data source
DefaultDispatcher-worker-1 @coroutine#2 LocalUserSettingDataSource
: loadUserSetting()
DefaultDispatcher-worker-3 @coroutine#2 UserSettingRepository
: syncUserSetting() - Sync and Store to local data source
DefaultDispatcher-worker-3 @coroutine#2 LocalUserSettingDataSource
: updateUserSetting()
main @coroutine#2 SampleViewModel : syncUserSetting() : result
: Success(UserSetting(userId=TestUser#1, primaryColor=FFFF0000, secondaryColor=FF0000FF))
main @coroutine#1 SampleViewModel
: onClear()

You can figure out that the main logic — sync user settings — is executed on worker thread of I/O thread pool. The coroutine is started on the main thread and change its context which has I/O dispatcher to execute its sync logic and then it goes back to the main thread to handle the result.

Compare coroutine to others (Callback and Rx)

  • 👍 Coroutine can also specify execution thread and callback thread easily at the call site like Rx (As I mentioned earlier, I suggest that a callee specifies its execution thread internally so that caller can just call the function without any concerns about the thread).
  • 👍 Let’s Assume that you have very heavy function which is composed of many sub functions. If you choose the way using callback, you might meet the famous callback hell. If you choose the way using Rx, it’s better than the callback but it could be harder when the relationship among sub functions is complex. Coroutine help you write your async code in a sync way thanks to the kotlin coroutine compiler which provides compile time CPS Style code transformation from suspending function. As a user of coroutine, there are not much of things to learn and remember to use it.
  • 👍 Coroutine also provides cancellation of tasks. When a parent job is cancelled, the cancellation event is propagated to all the direct or indirect children and children are also cancelled and throwing CancellationException when they call next suspending function or finish execution (like checking whether the job is cancelled or not right before execute next operator in Rx chain). Of course, you can declare custom suspending function (operator) that eagerly check whether current scope is cancelled or not as using isActive property or yield() function.
  • 👍 You can also handle exceptions as usual. In other words, you can use try-catch phrase to catch exception from itself or children. There is one thing you should care about. Coroutine has two types of exception handling pattern such as launch type and async type. You should check it out before using it 😰.
  • 👍 In the past, it is not that easy to debug coroutine in the IDE; however, you can now debug coroutine easily thanks to JetBrain and collaborators.

I’m going to dive into the internal of coroutine which has been dealt in this posting.

The end.

--

--