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

Android协程上下文导致异常处理失效问题求助

天气APP无效城市搜索时崩溃,异常未被Activity捕获

我开发的天气APP带有搜索栏,用户可搜索地点获取对应天气数据。设置了异常处理逻辑,期望输入无效城市名时APP不崩溃,而是显示提示Snackbar,但实际输入无效内容时APP仍崩溃。

当输入无效城市名时,地理编码API返回空数组触发EmptyApiResponseException,该异常会依次被数据源、仓库、ViewModel捕获并重新抛出,按设计应该最终被Activity的catch块捕获,但实际未生效,导致崩溃。怀疑是协程上下文的问题,但不知道怎么修复。


Activity中的搜索提交代码

override fun onQueryTextSubmit(query: String): Boolean {
    search.collapseActionView()
    lifecycleScope.launch(Dispatchers.IO) {
        try {
            viewModel.getSearchedLocationWeather(query)
            withContext(Dispatchers.Main) {
                val currentDestination =
                    findNavController(R.id.nav_host_fragment_content_home).currentDestination
                if (currentDestination?.id == R.id.nav_home) {
                    val action =
                        MainScreenFragmentDirections.actionNavHomeToSearchResultFragment()
                    findNavController(R.id.nav_host_fragment_content_home).navigate(
                        action
                    )
                } else if (currentDestination?.id == R.id.nav_saved) {
                    val action =
                        SavedLocationFragmentDirections.actionNavSavedToNavSearch()
                    findNavController(R.id.nav_host_fragment_content_home).navigate(
                        action
                    )
                }
            }
        } catch (e: Exception) {
            Log.d(WEATHER_ERROR, "$e for $query")
            withContext(Dispatchers.Main) {
                val snackbar = Snackbar.make(
                    findViewById(android.R.id.content),
                    getString(R.string.invalid_query_input),
                    Snackbar.LENGTH_SHORT
                )
                val snackbarView = snackbar.view
                val snackbarText =
                    snackbarView.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
                snackbarText.setTextColor(ContextCompat.getColor(this@MainActivity, R.color.white))
                snackbarView.background =
                    ContextCompat.getDrawable(this@MainActivity, R.drawable.snackbar_background)
                snackbar.show()
            }
        }
    }
    searchView.clearFocus()

    return true
}

ViewModel中的方法

fun getSearchedLocationWeather(query: String) {
    viewModelScope.launch(Dispatchers.Main) {
        try {
            weatherRepository.getSearchedWeather(query).collect { data ->
                try {
                    val weatherData = convertWeatherDtoToWeatherModel(data.weather)
                    _currentWeather.value = LocationWeatherModel(
                        location = LocationModel(data.location.cityName, data.location.country),
                        weather = weatherData
                    )
                } catch (e: Exception) {
                    Log.d(WEATHER_ERROR, "$query viewmodel")
                    throw e
                }
            }
        } catch (e: Exception) {
            Log.d(WEATHER_ERROR, "$query viewmodel")
            throw e
        }
    }
}

仓库中的方法

override fun getSearchedWeather(cityName: String): Flow<LocationWithWeatherDataDto> = flow{
    try{ 
        remoteWeatherDataSource.getWeather(cityName).collect{
            emit(it)
        }
    }
    catch (e : Exception){
        Log.e(WEATHER_ERROR, "Error fetching weather data for $cityName", e)
        throw e
    }

}.flowOn(Dispatchers.IO)

远程数据源的getWeather方法

override fun getWeather(cityName: String): Flow<LocationWithWeatherDataDto> = flow {
    try {
        getCoordinates(cityName).collect { coordinates ->
            val weatherData: WeatherApiModel
            val preferredUnit = dataStoreRepository.getUnit()
            weatherData = when(preferredUnit){
                Unit.IMPERIAL -> weatherService.getWeather(
                    coordinates.latitude,
                    coordinates.longitude,
                    "minutely,daily,alerts",
                    "imperial",
                    BuildConfig.OWM_KEY
                )
                Unit.METRIC -> weatherService.getWeather(
                    coordinates.latitude,
                    coordinates.longitude,
                    "minutely,daily,alerts",
                    "metric",
                    BuildConfig.OWM_KEY
                )
                else -> {
                    weatherService.getWeather(
                        coordinates.latitude,
                        coordinates.longitude,
                        "minutely,daily,alerts",
                        "imperial",
                        BuildConfig.OWM_KEY
                    )
                }
            }
            val combinedData = LocationWithWeatherDataDto(
                LocationDto(coordinates.name, coordinates.country),
                weatherData.toWeatherDto()
            )
            emit(combinedData)
        }

    } catch (e: HttpException) {
        Log.e(WEATHER_ERROR, "$e in DataSource")
        throw WeatherFetchException("HTTP error fetching weather data", e)
    } catch (e: Exception) {
        Log.e(WEATHER_ERROR, "$e in DataSource")
        throw WeatherFetchException("Error fetching weather data for $cityName in data source", e)
    }
}.flowOn(Dispatchers.IO)

地理编码获取坐标方法

override fun getCoordinates(name: String): Flow<CoordinateApiModel> = flow {
    try {
        val coordinatesList = withContext(Dispatchers.IO) {
            weatherService.getCiyCoordinates(name, 1, BuildConfig.OWM_KEY)
        }
        if (coordinatesList.isNotEmpty()) {
            val firstCoordinate = coordinatesList[0]
            emit(firstCoordinate)
        } else {
            throw EmptyApiResponseException("Coordinates list is empty.")
        }
    }
    catch (e: HttpException) {
        throw WeatherFetchException("HTTP error fetching weather", e)
    }
    catch (e: Exception) {
        throw e
    }
}

问题原因分析

核心问题是协程作用域隔离:

  1. Activity的lifecycleScope协程中调用viewModel.getSearchedLocationWeather(query),但该方法内部启动了viewModelScope的新协程,两个协程属于不同作用域,异常无法跨作用域传播到Activity的catch块。
  2. ViewModel中调用collect收集Flow时,异常被抛出到viewModelScope的协程中,而非Activity的lifecycleScope协程,导致Activity的catch块根本捕获不到。

修复方案

方案1:让ViewModel方法返回Flow,由Activity收集处理

修改ViewModel方法,直接返回Flow,将收集逻辑交给Activity,确保异常在同一协程作用域内被捕获:

// ViewModel中修改
fun getSearchedLocationWeather(query: String): Flow<LocationWeatherModel> = flow {
    weatherRepository.getSearchedWeather(query).collect { data ->
        val weatherData = convertWeatherDtoToWeatherModel(data.weather)
        emit(LocationWeatherModel(
            location = LocationModel(data.location.cityName, data.location.country),
            weather = weatherData
        ))
    }
}.catch { e ->
    Log.d(WEATHER_ERROR, "$query viewmodel")
    throw e
}.flowOn(Dispatchers.Main)

Activity中修改调用逻辑:

// Activity中修改onQueryTextSubmit
override fun onQueryTextSubmit(query: String): Boolean {
    search.collapseActionView()
    lifecycleScope.launch(Dispatchers.IO) {
        try {
            viewModel.getSearchedLocationWeather(query).collect { weatherModel ->
                viewModel._currentWeather.value = weatherModel
                withContext(Dispatchers.Main) {
                    val currentDestination =
                        findNavController(R.id.nav_host_fragment_content_home).currentDestination
                    if (currentDestination?.id == R.id.nav_home) {
                        val action =
                            MainScreenFragmentDirections.actionNavHomeToSearchResultFragment()
                        findNavController(R.id.nav_host_fragment_content_home).navigate(
                            action
                        )
                    } else if (currentDestination?.id == R.id.nav_saved) {
                        val action =
                            SavedLocationFragmentDirections.actionNavSavedToNavSearch()
                        findNavController(R.id.nav_host_fragment_content_home).navigate(
                            action
                        )
                    }
                }
            }
        } catch (e: Exception) {
            Log.d(WEATHER_ERROR, "$e for $query")
            withContext(Dispatchers.Main) {
                val snackbar = Snackbar.make(
                    findViewById(android.R.id.content),
                    getString(R.string.invalid_query_input),
                    Snackbar.LENGTH_SHORT
                )
                val snackbarView = snackbar.view
                val snackbarText =
                    snackbarView.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
                snackbarText.setTextColor(ContextCompat.getColor(this@MainActivity, R.color.white))
                snackbarView.background =
                    ContextCompat.getDrawable(this@MainActivity, R.drawable.snackbar_background)
                snackbar.show()
            }
        }
    }
    searchView.clearFocus()

    return true
}

方案2:在ViewModel中使用LiveData传递错误信息

如果不想修改Flow返回方式,可以在ViewModel中定义LiveData传递错误,避免跨协程作用域抛出异常:

// ViewModel中添加
private val _errorMessage = MutableLiveData<String?>()
val errorMessage: LiveData<String?> = _errorMessage

fun getSearchedLocationWeather(query: String) {
    viewModelScope.launch(Dispatchers.Main) {
        try {
            weatherRepository.getSearchedWeather(query).collect { data ->
                try {
                    val weatherData = convertWeatherDtoToWeatherModel(data.weather)
                    _currentWeather.value = LocationWeatherModel(
                        location = LocationModel(data.location.cityName, data.location.country),
                        weather = weatherData
                    )
                    _errorMessage.value = null // 清除错误
                } catch (e: Exception) {
                    Log.d(WEATHER_ERROR, "$query viewmodel")
                    _errorMessage.value = getString(R.string.invalid_query_input)
                }
            }
        } catch (e: Exception) {
            Log.d(WEATHER_ERROR, "$query viewmodel")
            _errorMessage.value = getString(R.string.invalid_query_input)
        }
    }
}

Activity中观察LiveData显示Snackbar:

// Activity初始化时添加
viewModel.errorMessage.observe(this) { message ->
    message?.let {
        val snackbar = Snackbar.make(
            findViewById(android.R.id.content),
            it,
            Snackbar.LENGTH_SHORT
        )
        val snackbarView = snackbar.view
        val snackbarText =
            snackbarView.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
        snackbarText.setTextColor(ContextCompat.getColor(this, R.color.white))
        snackbarView.background =
            ContextCompat.getDrawable(this, R.drawable.snackbar_background)
        snackbar.show()
        viewModel._errorMessage.value = null // 重置错误信息
    }
}

关键注意点

  • 协程异常仅能在同一协程作用域内被捕获,跨作用域的异常需通过回调、LiveData、Flow等方式传递。
  • viewModelScope的默认异常处理器会将未捕获的异常抛出,导致APP崩溃,因此需在ViewModel内处理异常或传递到上层。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 19:14:54