MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin Android协程与网络请求

2022-12-106.7k 阅读

Kotlin Android 协程基础

Kotlin 协程是一种轻量级的异步编程模型,它允许我们以一种更简洁、更可读的方式编写异步代码。在 Android 开发中,协程特别适用于处理网络请求、文件 I/O 等耗时操作,避免阻塞主线程,从而保证应用的流畅性。

协程的基本概念

  1. 协程的定义与启动 在 Kotlin 中,我们可以使用 launch 函数来启动一个协程。例如:

    import kotlinx.coroutines.*
    
    fun main() {
        GlobalScope.launch {
            // 这里是协程的执行体
            println("Hello from coroutine")
        }
        println("Hello from main")
        Thread.sleep(1000)
    }
    

    在上述代码中,GlobalScope.launch 启动了一个新的协程。GlobalScope 是一个全局的协程作用域,它会在整个应用的生命周期内存在。协程体中的代码会在一个新的线程(默认情况下)中执行,而主线程会继续执行后续代码。Thread.sleep(1000) 是为了确保主线程等待协程执行完毕,在实际 Android 开发中,我们不会使用这种方式,而是通过更优雅的机制来处理异步结果。

  2. 协程的挂起函数 挂起函数是协程中非常重要的概念。一个函数如果被声明为 suspend,那么它就是一个挂起函数。挂起函数只能在协程或者其他挂起函数中调用。例如:

    suspend fun delayMessage() {
        delay(1000)
        println("Delayed message")
    }
    

    这里的 delay 是 Kotlin 协程库提供的一个挂起函数,它会暂停当前协程的执行,等待指定的时间(这里是 1000 毫秒),然后再继续执行。

  3. 协程的返回值 除了 launch 函数启动无返回值的协程外,我们还可以使用 async 函数启动一个有返回值的协程。例如:

    fun main() = runBlocking {
        val deferred = async {
            // 模拟一个耗时操作
            delay(1000)
            42
        }
        val result = deferred.await()
        println("The result is $result")
    }
    

    在这个例子中,async 启动了一个协程,协程体返回了一个值 42deferred.await() 会暂停当前协程,直到 async 启动的协程执行完毕并返回结果。

Android 中的协程应用场景

在 Android 开发中,协程主要用于以下几个方面:

  1. 网络请求:网络请求是典型的耗时操作,使用协程可以很方便地处理异步网络请求,避免阻塞主线程。
  2. 文件 I/O:读取或写入文件也可能是耗时的,协程可以帮助我们以异步的方式处理这些操作。
  3. 数据库操作:例如使用 Room 数据库时,一些查询操作可能比较耗时,协程可以优化这些操作。

Kotlin Android 协程与网络请求

常用的网络请求库与协程集成

  1. OkHttp 与协程 OkHttp 是 Android 开发中常用的网络请求库。通过 Kotlin 协程的扩展库,我们可以很方便地将 OkHttp 与协程集成。首先,添加依赖:

    implementation 'com.squareup.okhttp3:okhttp:4.9.0'
    implementation 'com.squareup.okhttp3:logging - interceptor:4.9.0'
    implementation 'org.jetbrains.kotlinx:kotlinx - coroutines - okhttp3:1.5.2'
    

    然后,示例代码如下:

    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.withContext
    import okhttp3.OkHttpClient
    import okhttp3.Request
    
    suspend fun fetchData(): String {
        return withContext(Dispatchers.IO) {
            val client = OkHttpClient()
            val request = Request.Builder()
               .url("https://example.com/api/data")
               .build()
            client.newCall(request).execute().use { response ->
                response.body?.string() ?: "No data"
            }
        }
    }
    

    在上述代码中,withContext(Dispatchers.IO) 表示这段代码会在 I/O 线程池中执行,避免阻塞主线程。OkHttpClient 发送网络请求,并返回响应的字符串内容。

  2. Retrofit 与协程 Retrofit 是一个类型安全的网络请求库,它与协程的集成也非常方便。添加依赖:

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter - gson:2.9.0'
    implementation 'com.squareup.retrofit2:adapter - rxjava3:2.9.0'
    implementation 'org.jetbrains.kotlinx:kotlinx - coroutines - retrofit2:1.5.2'
    

    定义 Retrofit 接口:

    import retrofit2.Response
    import retrofit2.http.GET
    
    interface ApiService {
        @GET("api/data")
        suspend fun getData(): Response<String>
    }
    

    调用接口:

    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.withContext
    import retrofit2.Retrofit
    import retrofit2.converter.gson.GsonConverterFactory
    
    suspend fun fetchDataWithRetrofit(): String {
        return withContext(Dispatchers.IO) {
            val retrofit = Retrofit.Builder()
               .baseUrl("https://example.com/")
               .addConverterFactory(GsonConverterFactory.create())
               .addCallAdapterFactory(CoroutineCallAdapterFactory())
               .build()
            val apiService = retrofit.create(ApiService::class.java)
            val response = apiService.getData()
            if (response.isSuccessful) {
                response.body() ?: "No data"
            } else {
                "Request failed"
            }
        }
    }
    

    这里,suspend 修饰的接口方法使得 Retrofit 能够与协程很好地配合。CoroutineCallAdapterFactory 是将 Retrofit 的 Call 对象转换为协程友好的类型。

处理网络请求中的错误

在网络请求过程中,可能会遇到各种错误,如网络连接失败、服务器响应错误等。使用协程,我们可以优雅地处理这些错误。

  1. OkHttp 错误处理 在 OkHttp 的网络请求中,我们可以通过 try - catch 块来捕获异常。例如:

    suspend fun fetchDataWithErrorHandling(): String {
        return try {
            withContext(Dispatchers.IO) {
                val client = OkHttpClient()
                val request = Request.Builder()
                   .url("https://example.com/api/data")
                   .build()
                client.newCall(request).execute().use { response ->
                    response.body?.string() ?: "No data"
                }
            }
        } catch (e: Exception) {
            "Error: ${e.message}"
        }
    }
    

    这里,如果网络请求过程中出现任何异常,catch 块会捕获并返回错误信息。

  2. Retrofit 错误处理 Retrofit 中,当服务器返回非成功状态码时,response.isSuccessful 会返回 false。我们可以根据这个来处理错误。例如:

    suspend fun fetchDataWithRetrofitErrorHandling(): String {
        return try {
            withContext(Dispatchers.IO) {
                val retrofit = Retrofit.Builder()
                   .baseUrl("https://example.com/")
                   .addConverterFactory(GsonConverterFactory.create())
                   .addCallAdapterFactory(CoroutineCallAdapterFactory())
                   .build()
                val apiService = retrofit.create(ApiService::class.java)
                val response = apiService.getData()
                if (response.isSuccessful) {
                    response.body() ?: "No data"
                } else {
                    "Request failed with code ${response.code()}"
                }
            }
        } catch (e: Exception) {
            "Error: ${e.message}"
        }
    }
    

    这样,无论是网络连接问题还是服务器响应错误,都能得到妥善处理。

并发与串行网络请求

  1. 并发网络请求 在某些情况下,我们可能需要同时发起多个网络请求,并等待所有请求都完成后再进行下一步操作。使用协程的 asyncawaitAll 可以很方便地实现并发网络请求。例如,假设我们有两个 API 接口,分别获取用户信息和用户的订单信息:

    interface UserApiService {
        @GET("api/user")
        suspend fun getUser(): Response<User>
    }
    
    interface OrderApiService {
        @GET("api/orders")
        suspend fun getOrders(): Response<List<Order>>
    }
    
    data class User(val name: String, val age: Int)
    data class Order(val orderId: String, val amount: Double)
    
    suspend fun fetchUserDataAndOrders(): Pair<User?, List<Order>?> {
        return withContext(Dispatchers.IO) {
            val retrofit = Retrofit.Builder()
               .baseUrl("https://example.com/")
               .addConverterFactory(GsonConverterFactory.create())
               .addCallAdapterFactory(CoroutineCallAdapterFactory())
               .build()
            val userApiService = retrofit.create(UserApiService::class.java)
            val orderApiService = retrofit.create(OrderApiService::class.java)
    
            val userDeferred = async { userApiService.getUser() }
            val orderDeferred = async { orderApiService.getOrders() }
    
            val userResponse = userDeferred.await()
            val orderResponse = orderDeferred.await()
    
            if (userResponse.isSuccessful && orderResponse.isSuccessful) {
                Pair(userResponse.body(), orderResponse.body())
            } else {
                Pair(null, null)
            }
        }
    }
    

    在这个例子中,async 启动了两个并发的网络请求,await 等待每个请求完成,最后返回两个请求的结果。

  2. 串行网络请求 有时候,我们需要一个网络请求的结果作为另一个网络请求的参数,这就需要进行串行网络请求。例如,先获取用户 ID,然后根据用户 ID 获取用户详细信息:

    interface UserIdApiService {
        @GET("api/userid")
        suspend fun getUserId(): Response<String>
    }
    
    interface UserDetailApiService {
        @GET("api/user/{id}")
        suspend fun getUserDetail(@Path("id") userId: String): Response<UserDetail>
    }
    
    data class UserDetail(val name: String, val email: String)
    
    suspend fun fetchUserDetail(): UserDetail? {
        return withContext(Dispatchers.IO) {
            val retrofit = Retrofit.Builder()
               .baseUrl("https://example.com/")
               .addConverterFactory(GsonConverterFactory.create())
               .addCallAdapterFactory(CoroutineCallAdapterFactory())
               .build()
            val userIdApiService = retrofit.create(UserIdApiService::class.java)
            val userDetailApiService = retrofit.create(UserDetailApiService::class.java)
    
            val userIdResponse = userIdApiService.getUserId()
            if (userIdResponse.isSuccessful) {
                val userId = userIdResponse.body()?: return@withContext null
                val userDetailResponse = userDetailApiService.getUserDetail(userId)
                if (userDetailResponse.isSuccessful) {
                    userDetailResponse.body()
                } else {
                    null
                }
            } else {
                null
            }
        }
    }
    

    这里,先获取用户 ID,然后使用这个 ID 去获取用户详细信息,实现了串行的网络请求。

协程与 Android 生命周期的结合

在 Android 开发中,我们需要确保网络请求等异步操作在 Activity 或 Fragment 销毁时能够正确取消,以避免内存泄漏等问题。AndroidX 库提供了 lifecycle - runtime - ktx 库来帮助我们将协程与 Android 生命周期结合。

  1. 在 Activity 中使用 首先,添加依赖:

    implementation "androidx.lifecycle:lifecycle - runtime - ktx:2.4.1"
    

    然后在 Activity 中:

    class MainActivity : AppCompatActivity() {
        private val viewModel: MainViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            lifecycleScope.launchWhenCreated {
                viewModel.fetchData()
               .collect { data ->
                    // 更新 UI
                    textView.text = data
                }
            }
        }
    }
    

    这里,lifecycleScope.launchWhenCreated 启动的协程会在 Activity 创建时开始执行,并在 Activity 销毁时自动取消,确保了资源的正确管理。

  2. 在 ViewModel 中使用 在 ViewModel 中,我们可以使用 viewModelScope 来管理协程。例如:

    class MainViewModel : ViewModel() {
        private val _data = MutableStateFlow("")
        val data: StateFlow<String> = _data
    
        suspend fun fetchData(): Flow<String> {
            return flow {
                val result = // 网络请求获取数据
                emit(result)
            }
        }
    }
    

    viewModelScope 会在 ViewModel 被销毁时取消所有正在执行的协程,保证了内存的安全。

性能优化与注意事项

  1. 协程上下文与线程调度 合理选择协程上下文对于性能优化很重要。例如,对于 I/O 操作,我们应该使用 Dispatchers.IO,而对于 CPU 密集型操作,应该使用 Dispatchers.Default。避免在主线程执行耗时操作,否则会导致应用卡顿。
  2. 内存管理 正如前面提到的,将协程与 Android 生命周期结合,确保异步操作在合适的时机取消,避免内存泄漏。同时,注意协程中使用的资源(如网络连接、文件句柄等)的正确关闭。
  3. 错误处理的全面性 在网络请求中,要全面考虑各种可能的错误情况,包括网络连接失败、服务器响应错误、解析错误等。提供友好的错误提示给用户,提高应用的稳定性和用户体验。

通过以上对 Kotlin Android 协程与网络请求的详细介绍,我们可以看到协程为 Android 开发中的网络请求处理带来了极大的便利和优雅性。合理使用协程,可以提高应用的性能和用户体验,同时降低代码的复杂度。在实际开发中,我们需要根据具体的业务需求,灵活运用协程的各种特性,打造出高质量的 Android 应用。