Android协程
本文以网络请求为例,由浅入深,来说明协程在Android中的使用方式。后半部分介绍一些协程概念。
(1)添加依赖项
如下:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
(2)网络请求函数
这是一个同步的阻塞函数,调用它的线程会阻塞。如下:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// Function that makes the network request, blocking the current thread
fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
val url = URL(loginUrl)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json; utf-8")
setRequestProperty("Accept", "application/json")
doOutput = true
outputStream.write(jsonBody.toByteArray())
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
(3)触发网络请求
用户点击时,触发网络请求,如下:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
此时,会阻塞主线程。通过使用协程将它移出主线程,如下:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
先说明一下viewModelScope属性。它是ViewModel的扩展属性,在androidx.lifecycle.ViewModelKt中定义的。viewModelScope可以对协程进行管理。Dispatchers.IO说明该协程是运行在IO线程中的。
现在基本满足了要求。但是,对于(2)中的makeLoginRequest()方法来讲,如果调用方忘记了把它从主线程中移出,那么就会出问题。虽然可以通过注释等方式提醒调用者,但总有忘记的可能。下面就是杜绝这种可能的方式。
(4)主线程安全
如果函数不会阻塞主线程的UI刷新,那么该函数是主线程安全的。(2)中的makeLoginRequest()不是主线程安全的,使用它时必须移出主线程。下面的方式将它改为主线程安全的:
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// Blocking network request code
}
}
}
调用方:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
val result = loginRepository.makeLoginRequest(jsonBody)
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
因为makeLoginRequest()现在是suspend函数,所以必须在协程中调用。上面的示例通过viewModelScope.launch 启动一个协程来调用它。注意,该协程是运行在主线程中的,但不会阻塞主线程。根据状态机的改变,在适当时候再在主线程中执行when部分。
(5)异常处理
通过try-catch来处理异常部分,如下:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun makeLoginRequest(username: String, token: String) {
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
(6)请求响应处理
上面的内容并没有涉及请求响应的处理。但一个正常的请求,处理请求响应是必须的。下面就来展示这一点。
when (result) {
is Result.Success<LoginResponse> -> showData(result.data)
else -> showError()
}
suspend fun showData(data : LoginResponse){
withContext(Dispatchers.Main){
val name = data.name
nameText.setText(name)
}
}
suspend fun showError(){
withContext(Dispatchers.Main){
errorView.show()
}
}
showData()和showError()中都使用了withContext(Dispatchers.Main)来进行线程切换。这是基于调用它的协程运行在未知线程上考虑的。如果可以确定运行在主线程,那么不需要进行切换,suspend修饰符也不再需要。如下:
fun showData(data : LoginResponse){
val name = data.name
nameText.setText(name)
}
fun showError(){
errorView.show()
}
这里并没有使用JetPack Compose UI的更新方式,而是使用了原View体系。当然,使用Compose方式也是可以的,这里只是为了方便。
(7)launch和async
协程的启动方式有两种:launch和async。launch启动新协程,但不会把结果返回调用方。async启动的协程允许使用await()函数返回结果。通常,launch用于从常规函数启动新协程,而async是在suspend函数或者其他协程内使用。
一个示例如下:
suspend fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
(8)协程范围CoroutineScope
CoroutineScope用于跟踪它使用launch和async创建的协程。可以使用scope.cancel()来取消正在运行的协程。有一些类有自己的Scope,如ViewModel有viewModelScope,Lifecycle有lifecycleScope。自定义一个CoroutineScope也是可以的,示例如下:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
(9)作业Job
Job是协程的句柄,可以对协程进行管理。示例:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
(10)协程上下文CoroutineContext
CoroutineContext使用下面几个类来定义协程的行为:
- Job :控制协程的生命周期;
- CoroutineDispatcher:将工作分配到适当的线程;
- CoroutineName:协程名称;
- CoroutineExceptionHandler:异常处理。
当在一个Scope内创建一个协程时,一个Job instance随之分配。其他的CoroutineContext相关元素则从该Scope继承。覆写继承的元素也是可以的,只需要传递一个新的CoroutineContext。如下:
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Over !