【发布时间】:2023-01-16 05:28:49
【问题描述】:
我正在构建一个 android 应用程序来计算用户的体重指数 (BMI):
我不确定使用视图模型作为状态持有者来处理状态的最佳方式是什么。为了让你帮助我,我会尽可能多地描述我的问题,从文件开始:
HomeUiState.kt 网站
负责存储UI状态。
data class HomeUiState(
val weight: String,
val height: String,
val currentBmiCalculated: String,
)
HomeViewModel.kt
负责控制 UI 状态并处理可组合的操作,例如敬酒:
class HomeViewModel(
private val firstState: HomeUiState,
private val stringToFloatConvertor: StringToFloatConvertorUseCase,
private val calculateBmi: CalculateBmiUseCase,
) : ViewModel() {
companion object {
private const val FIRST_VALUE = ""
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
HomeViewModel(
stringToFloatConvertor = StringToFloatConvertorUseCaseImpl(),
calculateBmi = CalculateImcUseCaseImpl(),
firstState = HomeUiState(
weight = FIRST_VALUE,
height = FIRST_VALUE,
currentBmiCalculated = "Not calculated yet",
)
)
}
}
}
private val _uiState: MutableStateFlow<HomeUiState> = MutableStateFlow(firstState)
private val _uiAction = MutableSharedFlow<HomeUiAction>()
val uiState = _uiState.asStateFlow()
val uiAction = _uiAction.asSharedFlow()
fun dispatchUiEvent(uiEvent: HomeUiEvent) {
when (uiEvent) {
is HomeUiEvent.OnEnterHeightValue -> _uiState.update { it.copy(height = uiEvent.value) }
is HomeUiEvent.OnEnterWeightValue -> _uiState.update { it.copy(weight = uiEvent.value) }
is HomeUiEvent.OnCalculateButtonClick -> onCalculateButtonClick()
is HomeUiEvent.OnClearButtonClick -> {
_uiState.update { firstState }
emitAction(HomeUiAction.MoveCursorToHeight)
}
is HomeUiEvent.OnProfileIconClick -> emitAction(HomeUiAction.NavigateToProfileScreen)
}
}
private fun onCalculateButtonClick() {
stringToFloatConvertor(uiState.value.weight)?.let { weight ->
stringToFloatConvertor(uiState.value.height)?.let { height ->
_uiState.update {
it.copy(currentImcCalculated = "${calculateBmi(weight, height)}")
}
} ?: showErrorToast(R.string.invalid_height_value)
} ?: showErrorToast(R.string.invalid_weight_value)
emitAction(HomeUiAction.HideKeyboard)
}
private fun showErrorToast(@StringRes message: Int) =
emitAction(HomeUiAction.ShowErrorToast(message))
private fun emitAction(action: HomeUiAction) {
viewModelScope.launch {
_uiAction.emit(action)
}
}
}
主屏幕.kt
所以在这里我有我的不同之处,因为我不确定我是否应该将我的视图模型完全传输给HomeContent,或者我是否应该根据documentation itself of not transmitting the view model to the descendant functions 的建议进行传输。但在我看来,当将 ui 状态分解为 HomeContent 的几个参数时,仅更改 1 个参数时,所有 HomeContent 组件是否都会受到影响?
只是为了澄清我正在考虑的选项:
选项 1 - 仅将必要的传递给 HomeContent:
这样,通过将 UI 状态分解为多个参数,如果 1 个参数发生变化,整个函数就会重新组合,对吗?
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun HomeScreen(
navController: NavController,
viewModel: HomeViewModel,
) {
val context = LocalContext.current
val focusRequester = remember { FocusRequester.Default }
val keyboardController = LocalSoftwareKeyboardController.current
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(key1 = Unit) {
viewModel.uiAction.collectLatest { action ->
when (action) {
is HomeUiAction.ShowErrorToast -> Toast
.makeText(context, context.getText(action.messageId), Toast.LENGTH_SHORT)
.show()
is HomeUiAction.MoveCursorToHeight -> focusRequester.requestFocus()
is HomeUiAction.NavigateToProfileScreen -> navController.navigate("profile")
is HomeUiAction.HideKeyboard -> keyboardController?.hide()
}
}
}
Scaffold(
topBar = {
HomeAppBar(
onProfileClick = { viewModel.dispatchUiEvent(HomeUiEvent.OnProfileIconClick) }
)
}
) {
HomeContent(
height = uiState.height,
weight = uiState.weight,
onHeightChange = {
viewModel.dispatchUiEvent(HomeUiEvent.OnEnterHeightValue(it))
},
onWeightChange = {
viewModel.dispatchUiEvent(HomeUiEvent.OnEnterWeightValue(it))
},
onClear = {
viewModel.dispatchUiEvent(HomeUiEvent.OnClearButtonClick)
},
onCalculate = {
viewModel.dispatchUiEvent(HomeUiEvent.OnCalculateButtonClick)
},
focusRequester = focusRequester,
bmiResult = uiState.currentImcCalculated
)
}
}
@Composable
private fun HomeContent(
height: String,
weight: String,
bmiResult: String,
onHeightChange: (String) -> Unit,
onWeightChange: (String) -> Unit,
onClear: () -> Unit,
onCalculate: () -> Unit,
focusRequester: FocusRequester,
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "BMI Calculator",
style = TextStyle(
color = Color.Black,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
)
)
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
BmiEditText(
value = height,
label = "Height (m)",
onValueChange = onHeightChange,
modifier = Modifier
.fillMaxWidth(2 / 5f)
.focusRequester(focusRequester)
)
Spacer(modifier = Modifier.fillMaxWidth(1 / 3f))
BmiEditText(
value = weight,
label = "Weight (kg)",
onValueChange = onWeightChange,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onCalculate) {
Text(text = "Calculate")
}
Button(onClick = onClear) {
Text(text = "Clear")
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = "BMI result: $bmiResult")
}
}
选项 2 - 将整个视图模型传递给 HomeContent
这样,由于视图模型实例保持不变,函数将不再被重构,对吧?
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun HomeScreen(
navController: NavController,
viewModel: HomeViewModel,
) {
val context = LocalContext.current
val focusRequester = remember { FocusRequester.Default }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(key1 = Unit) {
viewModel.uiAction.collectLatest { action ->
when (action) {
is HomeUiAction.ShowErrorToast -> Toast
.makeText(context, context.getText(action.messageId), Toast.LENGTH_SHORT)
.show()
is HomeUiAction.MoveCursorToHeight -> focusRequester.requestFocus()
is HomeUiAction.NavigateToProfileScreen -> navController.navigate("profile")
is HomeUiAction.HideKeyboard -> keyboardController?.hide()
}
}
}
Scaffold(
topBar = {
HomeAppBar(
onProfileClick = { viewModel.dispatchUiEvent(HomeUiEvent.OnProfileIconClick) }
)
}
) {
HomeContent(
viewModel = viewModel,
focusRequester = focusRequester,
)
}
}
@Composable
private fun HomeContent(
viewModel: HomeViewModel,
focusRequester: FocusRequester,
) {
val uiState by viewModel.uiState.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "BMI Calculator",
style = TextStyle(
color = Color.Black,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
)
)
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
BmiEditText(
value = uiState.height,
label = "Height (m)",
onValueChange = {
viewModel.dispatchUiEvent(HomeUiEvent.OnEnterHeightValue(it))
},
modifier = Modifier
.fillMaxWidth(2 / 5f)
.focusRequester(focusRequester)
)
Spacer(modifier = Modifier.fillMaxWidth(1 / 3f))
BmiEditText(
value = uiState.weight,
label = "Weight (kg)",
onValueChange = {
viewModel.dispatchUiEvent(HomeUiEvent.OnEnterWeightValue(it))
},
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
viewModel.dispatchUiEvent(HomeUiEvent.OnCalculateButtonClick)
}
) {
Text(text = "Calculate")
}
Button(
onClick = {
viewModel.dispatchUiEvent(HomeUiEvent.OnClearButtonClick)
}
) {
Text(text = "Clear")
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = "BMI result: ${uiState.currentBmiCalculated}")
}
}
问题
考虑到这些选项,遵循良好的社区实践,这两者中哪一个最能避免不必要的重组?
【问题讨论】:
-
我会推荐 - “只将必要的传递给 HomeContent”。
-
但在这种情况下,
HomeContent不会在 1 个参数更改时完全不必要地重组自身吗? @Abhimanyu -
如果屏幕托管在 Activity/Fragment 内,则您只能从 Activity/Fragment 传递 uiState 并监听 Activity/Fragment 内的 uistate (collectAsState) 的变化,如果您使用组合导航,只需将 uiState 发送到屏幕和导航 NavHost 的可组合 {} 块内仅传递 uiState
标签: android kotlin android-jetpack-compose