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
相关产品推荐
相关产品推荐

