Skip to content

Instantly share code, notes, and snippets.

@linsea
Created December 23, 2019 01:35
Show Gist options
  • Save linsea/0c5ffc1f3f1fa172b85bdc10e3e10fbc to your computer and use it in GitHub Desktop.
Save linsea/0c5ffc1f3f1fa172b85bdc10e3e10fbc to your computer and use it in GitHub Desktop.

Android 如何使用协程

安卓上, 协程可以帮忙解决两大问题:

  • 管理长时间运行的任务, 这些任务可能阻塞主线程, 导致 UI 卡顿.
  • 在主线程上安全地调用网络或磁盘操作.

安卓上使用协程的最好方式是使用官方的架构组件, 它们提供了对协程的支持. 目前 ViewModel, Lifecycle, LiveData , Room 组件提供了对协程一等的支持.

ViewModelScope

ViewModel 的支持主要是在 ViewModel 上提供了一个称为 ViewModelScopeCoroutineScope , 所有在 ViewModelScope 上启动的协程, 当 ViewModelScope 销毁时自动取消. 这样可以有效防止忘记取消任务时导致的资源泄漏.

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // 调用加载数据的挂起函数, 当 ViewModel clear() 时自动取消任务.
        }
    }
}

其实 viewModelScope 的实现非常简单, 就是一个带有 Dispatchers.MainSupervisorJob, 当 ViewModel.clear() 时, 在里面调用 Job.cancel() , 因为结构化并发的原因, 所有在 viewModelScope 范围内启动的协程, 都会级联取消.

LifecycleScope

每个具有生命周期的对象(Lifecycle)都有一个 LifecycleScope , 所有在它的范围内启动的协程, 当生命周期对象销毁时, 都会取消. 生命周期对象的 CoroutineScope 可以通过 lifecycle.coroutineScope 或者 lifecycleOwner.lifecycleScope 属性获取.

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params) //挂起
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

挂起生命周期相关的协程

Activity 或者 Fragment 中, 我们有时需要等到某个生命周期方法时, 或者至少在某个生命周期方法之后才执行某一任务, 如页面状态至少要 STARTED 才可以执行 FragmentTransaction , 对这种需求, 生命周期组件也提供了支持. Lifecycle 提供了 lifecycle.whenCreated, lifecycle.whenStarted, lifecycle.whenResumed 三个方法, 运行在这些方法内的协程, 如果页面的状态不是至少处于要求的最小状态, 协程将会挂起运行.

class MyFragment: Fragment {
    init { // 在构建方法这么早的阶段就启动了协程.
        lifecycleScope.launch {
            whenStarted {
                // 页面 onStart 之后运行, 可调用其他挂起函数
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // 当 checkUserAccess 方法返回, 如果页面不是*至少*处于 STARTED 状态.
                // 下面的代码会挂起. 否则我们至少处于 STARTED 状态, 可以安全地调用
                // fragment transactions 方法
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // 这行代码仅在上面的 whenStarted 代码块运行完成后执行, 因为 whenStarted 是挂起函数

        }
    }
}

如果协程通过上面的 whenXXX 方法启动后, 处于活动状态, 还没有结束, 这时页面销毁了, 则协程会自动取消, 并且会走到下面的 finally 块中, 所在 finally 中, 需要检查页面所处的状态, 再决定做什么动作.

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // 这里调用挂起函数
            } finally {
                // 页面 DESTROYED 时可能会走到这里
                if (lifecycle.state >= STARTED) {
                    // 检查到页面并非处于 DESTROYED 状态
                    // Fragment transactions.
                }
            }
        }
    }
}

注意: 如果页面 restart 重启了, 但协程并不会重启, 总之要确保信息是正确的.

LiveData 中使用协程

一个常见的操作是, 异步加载数据, 然后使用 LiveData 提供出去, 如下:

val user: LiveData<User> = liveData (timeoutInMs = 5000) {
    val data = database.loadUser() // loadUser 是一个挂起函数.
    emit(data)
}

这里 liveData 是一个 builder 函数, 在 builder 代码块中, 它调用挂起函数 loadUser() 然后通过 emit 把加载的数据发射出去.
需要注意的是, 当 LiveData<User> 的状态变为活动时(即有人订阅观察它), 加载动作才会真正执行, 而当它变为不活动时, 并且空闲了 timeoutInMs 毫秒, 它将会自动取消. 如果在完成之前将其取消,则如果LiveData再次变为活动状态,它将重新启动。如果它在先前的运行中成功完成,则不会重新启动。请注意,只有自动取消后,它才会重新启动。如果由于任何其他原因取消了该块(例如,引发CancelationException),则它不会重新启动。
您也可以从块中 emit 多个值。每个 emit 调用都会暂停该块的执行,直到在主线程上设置了LiveData为止。

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

LiveData 也可以与 Transformations 一起使用, 如下:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        // 变换操作指定上下文, IO 线程执行
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

任何时候如果你想发射新值, 你也可以使用 emitSource(source: LiveData<T>) 来发射多个值, 这里它产生值的源改变了. 调用 emit() 或者 emitSource() 会移除之前添加的源, 例如:

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( // 重新用数据库作为源, 因为之前 dispose 了.
                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)
                }
            )
        }
    }
}

Room 对协程的支持

Room 从 v2.1 开始支持协程, 在 DAO 方法中可以定义挂起方法.

@Dao
interface UsersDao {
    @Query("SELECT * FROM users")
    suspend fun getUsers(): List<User>

    @Query("UPDATE users SET age = age + 1 WHERE userId = :userId")
    suspend fun incrementUserAge(userId: String)

    @Insert
    suspend fun insertUser(user: User)

    @Update
    suspend fun updateUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)
}

https://developer.android.com/topic/libraries/architecture/coroutines
https://developer.android.com/kotlin/coroutines
https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb
https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471
https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment