By default, all Android app components use the Main thread of execution. We should off-load expensive calculations to background threads to avoid blocking UI, and we must use background threads for network communication, otherwise, NetworkOnMainThreadException will be thrown.
Apart from asynchronous computations, Kotlin coroutines also provide scoping and cancellation propagation, making it easier to stop all the tasks needed by an Activity, Fragment, or ViewModel which is being destroyed.
Integration
First of all, we need to add dependencies to the kotlinx.coroutines library and Android integration library:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2")
Let’s run the first coroutine in our Android app. Just a counter, for instance:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView)
GlobalScope.launch { // don't do this
var count = 0
while (true) {
textView.text = count++.toString()
delay(1000)
}
}
}
}
The problem is that the loop never stops and leaks the View hierarchy, but we'll address this later in this topic.
Dispatchers
Just to refresh your memory: with the help of dispatchers we can define the kind of threads on which the calculations will be performed.
Let's revisit the built-in coroutine dispatchers:
-
Default. Used when we create coroutines without an explicit dispatcher specified. Intended to perform CPU-intensive work. The number of threads is equal to the number of processor cores (but never less than two).
-
IO. Intended for I/O operations like reading or writing files and network conenctions. The number of threads is equal to the number of processor cores but never less than 64.
File and network IO should never contend for the same thread pool. In your application code, it's better to separate different types of IO. For example:val FileIO = Dispatchers.IO.limitedParallelism(3) -
Unconfined. Never moves execution to any specific thread.
With the integration of coroutines in Android came another dispatcher: Dispatchers.Main. It is designed to run coroutines on the main (UI) thread of an Android application. Coroutines launched using Dispatchers.Main can update the user interface, process user input, or perform other UI-related tasks, like any other code in the main application thread.
Under the hood, Dispatchers.Main is implemented using Android Looper and Handler machinery.
Lifecycle scope
Activity.lifecycleScope and Fragment.lifecycleScope are coroutine scopes that bound to the lifecycle of an Activity or Fragment. This means that any coroutine launched in this scope will automatically be canceled when the Activity or Fragment is destroyed.
So, back to our example. Let's use this scope:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView)
lifecycleScope.launch { // here
var count = 0
while (true) {
textView.text = count++.toString()
delay(1000)
}
}
}
}
This loops only until the Activity is destroyed, and never leaks.
Similarly, ViewModel.viewModelScope is a scope which will be automatically destroyed when ViewModel.onCleared is called.
State change hooks
The repeatOnLifecycle method is used to perform an action repeatedly while the lifecycle reaches a certain State, and cancel once the lifecycle leaves that State.
This is useful, for instance, for subscribing to some data source when the UI is visible to the user and dropping the subscription when the user leaves.
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.someData.collect { value ->
updateUi(value)
}
}
}
Before repeatOnLifecycle arrived, there were separate launchWhenCreated, launchWhenStarted, launchWhenResumed methods. Now they are deprecated, so if you come across them in some old code, just replace them with repeatOnLifecycle(appropriate state).
Conclusion
By design, coroutines are just functions that can be executed partially, suspended, and resumed later. The kotlinx.coroutines library turns them into a powerful tool for asynchronous computations. Android integration library adds the Main dispatcher and lifecycle support, making Kotlin coroutines a first-class citizen of Android development. Putting Android and coroutines together, you can now use auto-cancelling scopes and hook them into lifecycle changes.