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

Jetpack Compose天气应用:度量单位(华氏/摄氏)存储问题排查

天气应用单位设置功能故障排查

我正在开发一款天气应用,需在SettingsScreen中让用户选择Imperial(华氏度F)或Metric(摄氏度C)作为度量单位,点击保存按钮后完成持久化存储,但当前该功能无法正常工作。以下是相关实现代码,请帮忙定位错误。

SettingsScreen.kt

@Composable
fun SettingsScreen(navController: NavController,
                   settingsViewModel: SettingsViewModel = hiltViewModel()) {

    var unitToggleState by remember { mutableStateOf(false) }
    val measurementUnits = listOf("Imperial (F)", "Metric (C)")
    val choiceFromDb = settingsViewModel.unitList.collectAsState().value

    val defaultChoice = if (choiceFromDb.isNullOrEmpty()) measurementUnits[0] else choiceFromDb[0].unit

    var choiceState by remember {
        mutableStateOf(defaultChoice)
    }

    Scaffold(topBar = {
        WeatherAppBar(
            title = "Settings",
            icon = Icons.Default.ArrowBack,
            false,
            navController = navController){
            navController.popBackStack()
        }
    }) {
        Surface(modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()) {
            Column(verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally) {
                Text(
                    text = "Change Units of Measurement",
                    modifier = Modifier.padding(bottom = 15.dp)
                )

                IconToggleButton(checked = !unitToggleState ,
                    onCheckedChange = {
                        unitToggleState = !it
                        choiceState = if (unitToggleState) {
                            "Imperial (F)"
                        } else {
                            "Metric (C)"
                        }
                        Log.d("TAG", "MainContent: $unitToggleState")

                    }, modifier = Modifier
                        .fillMaxWidth(0.5f)
                        .clip(shape = RectangleShape)
                        .padding(5.dp)
                        .background(Color.Magenta.copy(alpha = 0.4f))) {

                    Text(text = if (unitToggleState) "Fahrenheit ºF" else "Celsius ºC" )


                }
                Button(onClick = {
                    settingsViewModel.deleteAllUnits()
                    settingsViewModel.insertUnit(Unit(unit = choiceState
                    ))

                },
                    modifier = Modifier
                        .padding(3.dp)
                        .align(CenterHorizontally),
                    shape = RoundedCornerShape(34.dp),
                    colors = ButtonDefaults.buttonColors(
                        backgroundColor = Color(0xFFEFBE42)
                    )) {
                    Text(text = "Save",
                        modifier = Modifier.padding(4.dp),
                        color = Color.White,
                        fontSize = 17.sp)

                }


            }
        }


    }

}

SettingsViewModel.kt

@HiltViewModel
class SettingsViewModel @Inject constructor(
    private val repository: WeatherDbRepository
): ViewModel() {
    private val _unitList = MutableStateFlow<List<Unit>>(emptyList())
    val unitList = _unitList.asStateFlow()

    init {
        viewModelScope.launch(Dispatchers.IO) {
            repository.getUnits().distinctUntilChanged()
                .collect { listOfUnits ->
                    if (listOfUnits.isNullOrEmpty()) {
                        repository.insertUnit(Unit("unit"))

                    }else {
                        _unitList.value = listOfUnits

                    }

                }

        }
    }

    fun insertUnit(unit: Unit) = viewModelScope.launch { repository.insertUnit(unit) }
    fun updateUnit(unit: Unit) = viewModelScope.launch { repository.updateUnit(unit) }
    fun deleteUnit(unit: Unit) = viewModelScope.launch { repository.deleteUnit(unit) }
    fun deleteAllUnits() = viewModelScope.launch { repository.deleteAllUnits() }


}

MainScreen.kt

@Composable
fun MainScreen(
    navController: NavController,
    mainViewModel: MainViewModel = hiltViewModel(),
    settingsViewModel: SettingsViewModel = hiltViewModel(),
    city: String
) {
    val curCity: String = if (city.isBlank()) "Stockholm" else city
    val unitFromDb = settingsViewModel.unitList.collectAsState().value
    var unit by remember {
        mutableStateOf("imperial")
    }
    var isImperial by remember {
        mutableStateOf(false)
    }

    if (!unitFromDb.isNullOrEmpty()) {
        unit = unitFromDb[0].unit.split(" ")[0].lowercase()
        isImperial = unit == "imperial"

        val weatherData = produceState<DataOrException<Weather, Boolean, Exception>>(
            initialValue = DataOrException(loading = true)
        ) {
            value = mainViewModel.getWeatherData(
                city = curCity,
                units = unit
            )
        }.value

        if (weatherData.loading == true) {
            CircularProgressIndicator()
        } else if (weatherData.data != null) {
            MainScaffold(
                weather = weatherData.data!!, navController,
                isImperial = isImperial
            )

        }

    }


}

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun MainScaffold(
    weather: Weather, navController: NavController, isImperial: Boolean
) {

    Scaffold(topBar = {
        WeatherAppBar(
            title = weather.city.name + " ,${weather.city.country}",
            navController = navController,
            onAddActionClicked = {
                navController.navigate(WeatherScreens.SearchScreen.name)


            },
            elevation = 5.dp
        ) {
            Log.d("TAG", "MainScaffold: Button Clicked")
        }

    }) {
        MainContent(data = weather, isImperial = isImperial)

    }
}

@Composable
fun MainContent(data: Weather, isImperial: Boolean) {
    val weatherItem = data.list[0]
    val imageUrl = "https://openweathermap.org/img/wn/${weatherItem.weather[0].icon}.png"

    Column(
        Modifier
            .padding(4.dp)
            .fillMaxWidth(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text(
            text = formatDate(weatherItem.dt), // Wed Nov 30
            style = MaterialTheme.typography.caption,
            color = MaterialTheme.colors.onSecondary,
            fontWeight = FontWeight.SemiBold,
            modifier = Modifier.padding(6.dp)
        )

        Surface(
            modifier = Modifier
                .padding(4.dp)
                .size(200.dp),
            shape = CircleShape,
            color = Color(0xFFFFC400)
        ) {

            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                WeatherStateImage(imageUrl = imageUrl)
                Text(
                    text = formatDecimals(weatherItem.temp.day) + "º",
                    style = MaterialTheme.typography.h4,
                    fontWeight = FontWeight.ExtraBold
                )
                Text(
                    text = weatherItem.weather[0].main,
                    fontStyle = FontStyle.Italic
                )
            }
        }
        HumidityWindPressureRow(weather = data.list[0], isImperial = isImperial)
        Divider()
        SunsetSunRiseRow(weather = data.list[0])
        Text(
            "This Week",
            style = MaterialTheme.typography.subtitle1,
            fontWeight = FontWeight.Bold
        )

        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight(),
            color = Color(0xFFEEF1EF),
            shape = RoundedCornerShape(size = 14.dp)
        ) {
            LazyColumn(
                modifier = Modifier.padding(2.dp),
                contentPadding = PaddingValues(1.dp)
            ) {
                items(items = data.list) { item: WeatherItem ->
                    WeatherDetailRow(weather = item)

                }

            }

        }

    }

}

错误定位与修复方案

1. SettingsScreen 中的UI状态逻辑错误

  • checked状态反转问题:IconToggleButton的checked = !unitToggleState会导致UI状态与实际选择的单位完全颠倒,点击后状态更新逻辑混乱。应该直接将checked绑定到unitToggleState,并在初始化时同步数据库的默认值。
  • 初始状态不同步:unitToggleState初始值固定为false,没有根据数据库返回的defaultChoice设置正确状态。比如当数据库存储的是"Imperial (F)"时,unitToggleState应该设为true。

修复后的SettingsScreen关键代码:

// 初始化时根据数据库默认值同步toggle状态
var unitToggleState by remember { 
    mutableStateOf(defaultChoice == measurementUnits[0]) 
}
// ...
IconToggleButton(
    checked = unitToggleState,
    onCheckedChange = {
        unitToggleState = it
        choiceState = if (it) measurementUnits[0] else measurementUnits[1]
        Log.d("TAG", "MainContent: $unitToggleState")
    },
    // ...
)

2. SettingsViewModel 初始化逻辑错误

  • 默认插入无效值:当数据库为空时,代码插入了Unit("unit")这个无效字符串,导致MainScreen中通过split(" ")解析单位时出错,无法正确转换为"imperial"或"metric"。
  • 冗余的null判断:repository.getUnits()返回的Flow类型为List<Unit>,永远不会为null,isNullOrEmpty()可以简化为isEmpty()。

修复后的SettingsViewModel init块:

init {
    viewModelScope.launch(Dispatchers.IO) {
        repository.getUnits().distinctUntilChanged()
            .collect { listOfUnits ->
                if (listOfUnits.isEmpty()) {
                    // 插入默认单位Imperial (F)
                    repository.insertUnit(Unit(measurementUnits[0]))
                } else {
                    _unitList.value = listOfUnits
                }
            }
    }
}

3. MainScreen 状态更新与数据刷新问题

  • remember状态未同步:unit和isImperial用remember存储,但没有监听unitFromDb的变化,导致数据库更新后这两个状态不会自动刷新。
  • produceState未监听依赖变化:当前produceState没有绑定curCity和unit作为依赖,切换单位或城市后不会重新请求天气数据。

修复后的MainScreen关键代码:

val unitFromDb = settingsViewModel.unitList.collectAsState().value
// 用derivedStateOf根据数据库值派生单位状态
val unit = remember(unitFromDb) {
    if (unitFromDb.isNotEmpty()) {
        unitFromDb[0].unit.split(" ")[0].lowercase()
    } else {
        "imperial"
    }
}
val isImperial = remember(unit) { unit == "imperial" }

// 添加curCity和unit作为produceState的依赖,变化时重新请求数据
val weatherData = produceState<DataOrException<Weather, Boolean, Exception>>(
    initialValue = DataOrException(loading = true),
    key1 = curCity,
    key2 = unit
) {
    value = mainViewModel.getWeatherData(
        city = curCity,
        units = unit
    )
}.value

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 08:42:40