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

如何在MainActivity加载前通过ViewModel加载Open Weather API数据?

问题解决思路

核心问题定位

你的崩溃主要来自两个关键问题:

  1. ViewModel中_openWeather.value!!的非空断言——若API请求失败或尚未返回时,_openWeather.value为null,直接触发空指针异常。
  2. UI层观察openWeather LiveData时未处理初始null状态,直接访问数据会导致崩溃。

下面提供两种可行的解决方案,你可以根据产品需求选择:


方案一:等待API加载完成后再填充UI

这种方式会先显示加载状态,请求成功后展示真实数据,失败则显示错误提示,适合对数据真实性要求高的场景。

修改ViewModel

去掉冗余的_name LiveData,添加加载、错误状态来通知UI,同时移除非空断言避免崩溃:

class WeatherViewModel : ViewModel() {
    private val _searchText = MutableStateFlow("")
    val searchText = _searchText.asStateFlow()

    private val _isSearching = MutableStateFlow(false)
    val isSearching = _isSearching.asStateFlow()

    private val _openWeather = MutableLiveData<OpenWeather>()
    val openWeather: LiveData<OpenWeather> = _openWeather

    // 新增加载/错误状态
    private val _isLoading = MutableLiveData(false)
    val isLoading: LiveData<Boolean> = _isLoading

    private val _errorMessage = MutableLiveData<String?>()
    val errorMessage: LiveData<String?> = _errorMessage

    init {
        getWeatherData()
    }

    private fun getWeatherData() {
        viewModelScope.launch {
            _isLoading.value = true
            _errorMessage.value = null
            try {
                val weatherData = RetrofitInstance.api.getWeatherUSCity()
                _openWeather.value = weatherData
                Log.v("Name: ", "${weatherData.name}")
                Log.v("WeatherViewModel", "Success!")
            } catch (e: Exception) {
                Log.e("WeatherViewModel", "Failure: ${e.message}")
                _errorMessage.value = e.message ?: "加载失败"
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun onSearchTextChanged(text: String) {
        _searchText.value = text
    }

    fun emptyText() {
        _searchText.value = ""
    }
}

修改MainActivity UI逻辑

根据加载、错误、数据状态切换显示内容:

setContent {
    WeatherTheme {
        val text by viewModel.searchText.collectAsState()
        var active by remember { mutableStateOf(false) }
        val weather by viewModel.openWeather.observeAsState()
        val isLoading by viewModel.isLoading.observeAsState(false)
        val errorMessage by viewModel.errorMessage.observeAsState()
        val prevItems = remember { mutableListOf<String>() }

        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            // 保留原SearchBar代码不变
            SearchBar(
                modifier = Modifier.fillMaxWidth(),
                query = text,
                onQueryChange = viewModel::onSearchTextChanged,
                onSearch = {
                    if(prevItems.size > 14)
                        prevItems.clear()
                    prevItems.add(text)
                    active = false
                },
                active = active,
                onActiveChange = { active = it },
                placeholder = { Text(text = "Search") },
                leadingIcon = {
                    Icon(
                        imageVector = Icons.Default.Search,
                        contentDescription = "Search Icon"
                    )
                },
                trailingIcon = {
                    if (active) {
                        Icon(
                            modifier = Modifier.clickable {
                                if (text.isNotEmpty()) {
                                    viewModel.emptyText()
                                } else {
                                    active = false
                                }
                            },
                            imageVector = Icons.Default.Close,
                            contentDescription = "Close Icon"
                        )
                    }
                }
            ) {
                prevItems.forEach {
                    Row(modifier = Modifier
                        .padding(all = 14.dp)
                        .clickable { viewModel.onSearchTextChanged(it) }
                        .fillMaxWidth()) {
                        Icon(
                            modifier = Modifier.padding(end = 10.dp),
                            imageVector = Icons.Default.History,
                            contentDescription = "Search History Icon"
                        )
                        Text(it)
                    }
                }
            }

            // 根据状态展示不同内容
            when {
                isLoading -> CircularProgressIndicator()
                errorMessage != null -> Text(text = errorMessage!!, color = Color.Red)
                weather != null -> Text(text = weather!!.name)
            }
        }
    }
}

方案二:用假数据占位,API完成后自动更新

利用你已经给OpenWeather设置的默认值,让UI先显示占位数据,API请求成功后自动替换为真实数据,适合追求流畅体验的场景。

修改ViewModel

初始化_openWeather时设置默认假数据,移除非空断言:

class WeatherViewModel : ViewModel() {
    // 保留其他代码不变

    // 初始化时设置默认假数据
    private val _openWeather = MutableLiveData<OpenWeather>(OpenWeather(name = "加载中..."))
    val openWeather: LiveData<OpenWeather> = _openWeather

    private fun getWeatherData() {
        viewModelScope.launch {
            Log.v("WeatherViewModel", "Loading...")
            try {
                val weatherData = RetrofitInstance.api.getWeatherUSCity()
                _openWeather.value = weatherData
                Log.v("Name: ", "${weatherData.name}")
                Log.v("WeatherViewModel", "Success!")
            } catch (e: Exception) {
                Log.e("WeatherViewModel", "Failure: ${e.message}")
            }
        }
    }

    // 保留其他代码不变
}

修改MainActivity UI逻辑

直接使用weather数据,因为初始有默认值,无需处理null:

setContent {
    WeatherTheme {
        val text by viewModel.searchText.collectAsState()
        var active by remember { mutableStateOf(false) }
        // 这里也可以设置默认值,和ViewModel保持一致
        val weather by viewModel.openWeather.observeAsState(OpenWeather(name = "加载中..."))
        val prevItems = remember { mutableListOf<String>() }

        Column(
            modifier = Modifier.fillMaxSize()
        ) {
            // 保留原SearchBar代码不变
            SearchBar(
                modifier = Modifier.fillMaxWidth(),
                query = text,
                onQueryChange = viewModel::onSearchTextChanged,
                onSearch = {
                    if(prevItems.size > 14)
                        prevItems.clear()
                    prevItems.add(text)
                    active = false
                },
                active = active,
                onActiveChange = { active = it },
                placeholder = { Text(text = "Search") },
                leadingIcon = {
                    Icon(
                        imageVector = Icons.Default.Search,
                        contentDescription = "Search Icon"
                    )
                },
                trailingIcon = {
                    if (active) {
                        Icon(
                            modifier = Modifier.clickable {
                                if (text.isNotEmpty()) {
                                    viewModel.emptyText()
                                } else {
                                    active = false
                                }
                            },
                            imageVector = Icons.Default.Close,
                            contentDescription = "Close Icon"
                        )
                    }
                }
            ) {
                prevItems.forEach {
                    Row(modifier = Modifier
                        .padding(all = 14.dp)
                        .clickable { viewModel.onSearchTextChanged(it) }
                        .fillMaxWidth()) {
                        Icon(
                            modifier = Modifier.padding(end = 10.dp),
                            imageVector = Icons.Default.History,
                            contentDescription = "Search History Icon"
                        )
                        Text(it)
                    }
                }
            }

            // 直接展示数据,API返回后自动更新
            Text(text = weather.name)
            // 可扩展展示其他默认数据,比如weather.main.temp等
        }
    }
}

额外优化建议

  • 彻底摒弃!!非空断言,改用?.安全调用或?: Elvis运算符处理null场景。
  • 可将初始化时的API请求改为用户点击搜索后触发,更符合常规交互逻辑。
  • 给OpenWeather的默认值设置更贴近真实场景的占位内容,比如name = "加载中...",提升用户感知。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 22:49:50