You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android应用无网络时Ktor连接失败致崩溃问题排查

无网络环境下Ktor请求崩溃问题排查与修复

我使用Kotlin、Dagger-Hilt和Ktor构建了一款Android应用,ViewModel会定期检查远程服务器的新内容,相关代码如下:

ViewModel代码

private fun loadRemoteContent() {
    viewModelScope.launch {
        val lastUpdateCheck = preferencesRepository.readDate(Settings.LAST_UPDATE_CHECK_DATE)
        val lastUpdateCheckDate = LocalDate.ofEpochDay(lastUpdateCheck ?: today.toEpochDay())

        val period = Period.between(lastUpdateCheckDate, today)

        if (lastUpdateCheck == null || period.days >= 1) {
            val remoteHash = repository.getHash()
            val localHash = preferencesRepository.readString(Settings.LOCAL_HASH_STRING)

            if (remoteHash != localHash) {
                val remoteContent = repository.getAllRemote()
                repository.insertAllRemote(remoteContent)

                preferencesRepository.setString(Settings.LOCAL_HASH_STRING, remoteHash)
            }

            preferencesRepository.setDate(Settings.LAST_UPDATE_CHECK_DATE, today.toEpochDay())
        }
    }
}

Repository相关函数

suspend fun getAllRemote(): List<Item> = withContext(backgroundDispatcher) {
    ktorHttpClient
        .prepareGet("https://someurl.com/remotecontent.json")
        .execute { response: HttpResponse ->
            val remoteContent = response.body<List<ItemResponse>>()
            remoteContent.map { remote ->
                Item(
                    id = remote.id,
                    title = remote.title,
                    summary = remote.summary,
                    link = remote.link
                )
            }
        }
}

Ktor配置(AppModule.kt)

@Provides
@Singleton
fun provideKtorHttpClient(): HttpClient {
    return HttpClient(Android) {
        expectSuccess = true
        install(ContentNegotiation) {
            json(
                Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                }
            )
        }
        install(Logging) {
            logger = object : Logger {
                override fun log(message: String) {
                    Log.v("http log: ", message)
                }
            }
            level = LogLevel.ALL
        }

        install(DefaultRequest) {
            header(HttpHeaders.ContentType, ContentType.Application.Json)
        }

        install(HttpRequestRetry) {
            retryOnServerErrors(maxRetries = 5)
            exponentialDelay()
        }
    }
}

崩溃现象

在Android Studio模拟器无网络环境下运行时,应用尝试连接5次后崩溃,Logcat仅输出如下日志:

2023-05-09 22:36:42.321 31747-31747 AndroidRuntime          com.example.myapp          E  FATAL EXCEPTION: main
                                                                                                    Process: com.example.myapp, PID: 31747

问题原因

  1. 未捕获异常:ViewModelScope中发起的协程未处理网络异常,当Ktor请求失败后,异常直接传播到主线程导致崩溃。
  2. 重试策略不匹配:当前Ktor的retryOnServerErrors仅针对服务器错误(5xx状态码),无网络属于客户端连接异常,不会触发精准的重试判断,但默认逻辑仍会尝试连接,最终抛出未处理的异常。
  3. 无网络预检查:发起请求前未判断网络状态,导致无意义的重试操作。

修复方案

1. 捕获协程异常,避免崩溃

在ViewModel的协程中添加异常捕获,确保请求失败时不会直接崩溃:

private fun loadRemoteContent() {
    viewModelScope.launch {
        try {
            val lastUpdateCheck = preferencesRepository.readDate(Settings.LAST_UPDATE_CHECK_DATE)
            val lastUpdateCheckDate = LocalDate.ofEpochDay(lastUpdateCheck ?: today.toEpochDay())

            val period = Period.between(lastUpdateCheckDate, today)

            if (lastUpdateCheck == null || period.days >= 1) {
                val remoteHash = repository.getHash()
                val localHash = preferencesRepository.readString(Settings.LOCAL_HASH_STRING)

                if (remoteHash != localHash) {
                    val remoteContent = repository.getAllRemote()
                    repository.insertAllRemote(remoteContent)

                    preferencesRepository.setString(Settings.LOCAL_HASH_STRING, remoteHash)
                }

                preferencesRepository.setDate(Settings.LAST_UPDATE_CHECK_DATE, today.toEpochDay())
            }
        } catch (e: Exception) {
            // 记录异常日志,可根据需求添加用户提示
            Log.e("ContentLoad", "Failed to load remote content", e)
        }
    }
}

2. 调整Ktor重试策略,覆盖连接异常

修改HttpRequestRetry配置,让其包含客户端连接异常,同时减少重试次数避免不必要的等待:

install(HttpRequestRetry) {
    // 重试服务器错误和异常(包括无网络连接异常)
    retryOnExceptionOrServerErrors(maxRetries = 3)
    exponentialDelay()
    // 自定义判断仅重试连接类异常
    retryIf { cause, _ ->
        cause is IOException || cause is ConnectException
    }
}

3. 添加网络状态预检查

在Repository中注入ConnectivityManager,发起请求前先判断网络是否可用:

// 在Repository构造函数中注入ConnectivityManager
class YourRepository @Inject constructor(
    private val ktorHttpClient: HttpClient,
    private val connectivityManager: ConnectivityManager,
    private val backgroundDispatcher: CoroutineDispatcher
) {
    private fun isNetworkAvailable(): Boolean {
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }

    suspend fun getAllRemote(): List<Item> = withContext(backgroundDispatcher) {
        if (!isNetworkAvailable()) {
            Log.w("NetworkCheck", "No internet connection, skipping remote load")
            return@withContext emptyList()
        }
        // 原有请求代码
        ktorHttpClient
            .prepareGet("https://someurl.com/remotecontent.json")
            .execute { response: HttpResponse ->
                val remoteContent = response.body<List<ItemResponse>>()
                remoteContent.map { remote ->
                    Item(
                        id = remote.id,
                        title = remote.title,
                        summary = remote.summary,
                        link = remote.link
                    )
                }
            }
    }
}

4. 可选:调整expectSuccess配置

如果需要处理非2xx的响应(比如404),可以将expectSuccess = true改为false,然后在请求中手动判断响应状态:

// 在Ktor配置中修改
expectSuccess = false

// 在getAllRemote中处理响应
.execute { response: HttpResponse ->
    if (!response.status.isSuccess()) {
        Log.e("RequestError", "Request failed with status: ${response.status}")
        return@execute emptyList()
    }
    // 原有解析代码
}

内容的提问来源于stack exchange,提问作者zkvvoob

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 12:50:44