kotlin 使用视图模型作为状态句柄来避免重新组合,在组合中处理状态的最佳方法是什么?

cqoc49vn  于 2023-01-17  发布在  Kotlin
关注(0)|答案(1)|浏览(116)

我正在构建一个Android应用程序来计算用户的身体质量指数(BMI):

我不确定使用视图模型作为状态保持器来处理状态的最佳方式是什么。为了帮助您,我将尽可能多地描述我的问题,从以下文件开始:

家庭用户状态.kt

负责存储UI状态。

data class HomeUiState(
    val weight: String,
    val height: String,
    val currentBmiCalculated: String,
)

家庭视图模型.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,或者我是否应该按照文档本身的建议不把视图模型传输给后代函数,但是在我看来,当把HomeContent的ui状态分解成几个参数时,当只更改一个参数时,所有HomeContent组件是否都会受到影响?只是为了澄清我正在考虑的选项:

选项1 -仅将必要内容传递给HomeContent:

这样,通过将UI状态分解为多个参数,如果有一个参数发生变化,整个函数将被重新组合,对吗?

@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}")
    }
}

问题

记住这些选项后,在两个选项中,哪一个最能避免遵循良好社区实践的不必要重组?

56lgkhnf

56lgkhnf1#

选项1将是你应该使用的选项。因为你不应该向下传递ViewModel。原因是viewModel被组合编译器标记为unstable,这导致了可组合函数non-skippable。这样做的问题是,即使你传递的参数没有改变,重组也不会被跳过,你的UI元素也会被重画。
对于选项1,如果1个参数发生变化,它将被重新组合,这是正确的,但由于组合可以智能地仅重新组合发生变化的组件,因此您不会遇到问题。
有关详细信息,您可以查看以下资源。
https://twitter.github.io/compose-rules/rules/
https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8

相关问题