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
问题原因
- 未捕获异常:ViewModelScope中发起的协程未处理网络异常,当Ktor请求失败后,异常直接传播到主线程导致崩溃。
- 重试策略不匹配:当前Ktor的
retryOnServerErrors仅针对服务器错误(5xx状态码),无网络属于客户端连接异常,不会触发精准的重试判断,但默认逻辑仍会尝试连接,最终抛出未处理的异常。 - 无网络预检查:发起请求前未判断网络状态,导致无意义的重试操作。
修复方案
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
相关产品推荐
相关产品推荐

