Using coroutines in Android
There is a way to access the coroutine scope associated with the lifecycle of objects such as view models, activities, and fragments.
Add following dependencies:
dependencies {
// for view model scope
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
// for lifecycle scope
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0")
// for liveData function
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
}
view model scope
The viewModelScope property of ViewModel referres to the coroutine scope associated with view model object. It allows launch coroutine that will be automatically canceled if the ViewModel is cleared. This is useful when you have work that needs to be done only if the ViewModel is active.
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
doSomethingOnUI()
// execute code on other dispatcher, i.e. in background
val result = withContext(Dispatchers.IO) {
dataRepository.loadData()
}
}
}
}
lifecycle scope
The lifecycleScope property referres to the coroutine scope associated with activity or fragment. Any coroutine launched in this scope is canceled when the lifecycle is destroyed.
Actually lifecycleScope is Kotlin extension for shorthand lifecycle.coroutineScope and viewLifecycleOwner.lifecycleScope.
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
There are an additional coroutine builder methods:
- launchWhenCreated() - launches and runs the given block when the Lifecycle controlling this LifecycleCoroutineScope is at least in Lifecycle.State.CREATED state.
- launchWhenStarted() - launches and runs the given block when the Lifecycle controlling this LifecycleCoroutineScope is at least in Lifecycle.State.STARTED state.
- launchWhenResumed() - launches and runs the given block when the Lifecycle controlling this LifecycleCoroutineScope is at least in Lifecycle.State.RESUMED state.
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// Call some suspend functions.
} finally {
// This line might execute after Lifecycle is DESTROYED.
if (lifecycle.state >= STARTED) {
// Here, since we've checked, it is safe to run any
// Fragment transactions.
}
}
}
}
}
Similarly you can use following functions within a lifecycle coroutine:
- whenCreated()
- whenStarted()
- whenResumed()
class MyFragment: Fragment {
init {
lifecycleScope.launch {
whenStarted { // will run only when Lifecycle is at least STARTED
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// When checkUserAccess returns, the next line is automatically
// suspended if the Lifecycle is not *at least* STARTED.
// We could safely run fragment transactions because we know the
// code won't run unless the lifecycle is at least STARTED.
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// This line runs only after the whenStarted block above has completed.
}
}
}
live data scope
LiveDataScope interface allows to control a LiveData objects from a coroutine block. It has following methods:
- emit(v) - set's the LiveData's value to the given value. If you've called emitSource previously, calling emit will remove that source. This function suspends until the value is set on the LiveData.
- emitSource(srcLiveData) - add the given LiveData as a source. It will remove any source that was yielded before.
- latestValue - current value in LiveData object
liveData() builder function calls a suspend function, serving the result as a LiveData object.
The block of liveData starts executing when the returned LiveData becomes active (LiveData.onActive). If the LiveData becomes inactive (LiveData.onInactive) while the block is executing, it will be cancelled after timeoutInMs milliseconds unless the LiveData becomes active again before that timeout (to gracefully handle cases like Activity rotation). Any emited value from a cancelled block will be ignored.
After a cancellation, if the LiveData becomes active again, the block will be re-executed from the beginning. If you would like to continue the operations based on where it was stopped last, you can use the latestValue function to get the last emited value.
If the block completes successfully or is cancelled due to reasons other than LiveData becoming inactive, it will not be re-executed even after LiveData goes through active inactive cycle.
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}