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 = " OST" 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 ! |