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
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
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
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:
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.:
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:
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
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
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.