Skip to content

Why Introduce a Thread Scheduling Framework

  • Java is inherently a multithreaded language. In Android, which has a UI component, the platform sets up UI and IO threads: UI rendering happens on the UI thread, while common data requests happen on IO threads. Otherwise, data requests could block rendering and cause UI lag. This makes proper thread scheduling extremely important.

  • In the early days, Android did not provide a thread scheduling framework. Developers had to rely on Java’s built-in thread pools to create their own scheduling tools. This requires deep understanding of thread pools, which is a complex and advanced topic. Even experienced Java developers, including those in SpringBoot or Android, often find it challenging.

  • Therefore, introducing an easy-to-use thread scheduling framework is very important for Android development.


AsyncTask

  • Google once introduced AsyncTask to help developers switch between UI and IO threads easily. It’s an abstract class and very simple to use.
  • However, over time it became clear that AsyncTask does not bind to lifecycles, which can cause resource leaks. Lifecycle awareness is crucial in frontend development.
  • As a result, Google deprecated AsyncTask in Android SDK 30.

RxJava

RxJava is a widely used asynchronous thread scheduling framework. Its strengths include:

  • Thread scheduling capabilities
  • Chained calls, which can be seen as a variant of the builder pattern
  • Combined with Java lambdas, this allows for concise code

Conceptually, RxJava represents all possible data-fetching scenarios as observable states. At the Presenter or ViewModel layer, the developer only needs to focus on passing these states to the View, without worrying too much about the data layer. This enables a single source of truth for the data request chain.


Example: Serial Task Execution

kotlin
interface Service {
    fun fetchDataElementLength(): Int // delay(1000L), returns 5
    fun fetchDataList(length: Int): List<String> // delay(3000L), returns list<String>
}

fun fetchABWithSerialOperation(): Single<String> {
    return currentService.fetchDataElementLength()
        .flatMap { length -> currentService.fetchDataList(length) }
        .map { it.joinToString(", ") }
}

Example: Parallel Task Execution

kotlin
interface Service {
    fun fetchDataA(): Single<String> // delay(1000L), returns "A"
    fun fetchDataB(): Single<String> // delay(3000L), returns "B"
}

fun fetchABWithParallelOperation(): Single<String> {
    return Single.zip(
        currentService.fetchDataA(),
        currentService.fetchDataB()
    ) { dataA, dataB -> (dataA + dataB) }
}

Thread Scheduling in Action

kotlin
val currentViewModel = ViewProvider(this or parentFragment or activity)[CurrentViewModel::class.java]

val startTimeWithSerialOperation = System.currentTimeMillis()
currentViewModel.fetchABWithSerialOperation()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(object : SingleObserver<String> {
        override fun onSubscribe(d: Disposable) {}
        override fun onError(e: Throwable) {}
        override fun onSuccess(t: String) {
            println("Result: $t")
            println("--- Time spent: ${(System.currentTimeMillis() - startTimeWithSerialOperation)/1000}s")
        }
    })

Note: In practice, subscriptions should be done in the ViewModel/Presenter, and results are exposed via LiveData.


Lifecycle Awareness

When using RxJava, it’s important to cancel requests when the ViewModel is cleared to prevent memory leaks:

kotlin
class CurrentViewModel(
    private val currentRepository: CurrentRepository,
) : ViewModel() {

    private val compositeDisposable = CompositeDisposable()
    private val _error = MutableLiveData<Throwable>()
    val error: LiveData<Throwable> = _error
    private val _success = MutableLiveData<String>()
    val success: LiveData<String> = _success

    fun fetchABWithSerialRelation() {
        currentRepository.fetchABWithParallelRelation()
            .subscribeOn(Schedulers.io())
            .observeOn(Schedulers.io())
            .blockingSubscribe(object : SingleObserver<String> {
                override fun onSubscribe(d: Disposable) {
                    compositeDisposable.add(d)
                }
                override fun onError(e: Throwable) { _error.value = e }
                override fun onSuccess(t: String) { _success.value = t }
            })
    }

    override fun onCleared() {
        compositeDisposable.clear()
    }
}

RxJava has many operators, which should be chosen based on business requirements. Memorizing all operators is unnecessary.


Misuse of Observable

Many developers mistakenly use Observable for one-time network requests, e.g.:

kotlin
interface RetrofitService {
    @GET("airport/fetchAll")
    fun fetchAllAirports(): Observable<List<AirportDetails>>
}
  • Observable is intended for continuous updates. For example, if Page A updates a local database, Page B can observe the changes in real-time.
  • For network requests that return a single response, Single is more appropriate:
kotlin
interface RetrofitService {
    @GET("airport/fetchAll")
    fun fetchAllAirports(): Single<List<AirportDetails>>
}

Kotlin Coroutines

  • Kotlin is a multiplatform language, running on JVM, JS, etc. On JVM, coroutines are Kotlin’s thread scheduling framework.
  • Conceptually, it’s based on JavaScript/Dart Futures, with withContext providing a synchronous-style way to write non-blocking asynchronous code.
  • The suspend keyword marks a subtask that can be awaited without blocking the parent thread.

Serial Tasks with Coroutines

kotlin
interface Service {
    suspend fun fetchDataElementLength(): Int
    suspend fun fetchDataList(length: Int): List<String>
}

suspend fun fetchABWithSerialRelation(): String = withContext(Dispatchers.IO) {
    val length = currentService.fetchDataElementLength()
    val data = currentService.fetchDataList(length)
    return data.joinToString(", ")
}

Parallel Tasks with Coroutines

kotlin
interface Service {
    suspend fun fetchDataA(): List<String>
    suspend fun fetchDataB(): List<String>
}

suspend fun fetchABWithParallelRelation(): String = withContext(Dispatchers.IO) {
    val dataA = async { currentService.fetchDataA() }
    val dataB = async { currentService.fetchDataB() }
    return@withContext (dataA.await() + dataB.await()).joinToString(", ")
}

My Perspective on Thread Scheduling Frameworks

  • Both Rx (multithreaded) and Future (single-threaded) are worth studying, as they represent different philosophies. Understanding how to implement asynchronous scheduling is essential for UI/frontend developers.
  • Kotlin Coroutines can be considered an advanced framework. It combines task-blocking from Future and state-based thinking from Rx, providing a structured approach to concurrency.
  • Not learning Coroutines is feasible—RxJava alone is sufficient for most use cases. Whether to adopt Coroutines depends on team goals and ecosystem considerations.
  • Differences include exception handling and unit testing adjustments in Coroutines.
  • Personally, I enjoy projects with mixed technologies, e.g., maintaining Java & Kotlin or JavaScript & TypeScript in the same project, as long as it’s reliable.

Just something casual. Hope you like it. Built with VitePress