我正在开发一个带有表单的屏幕,用于使用 Jetpack Compose 输入乘客数据。乘客数据表功能集成在屏幕功能中。按下“保存”按钮后,ViewModel 会验证输入的数据并更新乘客状态(如果发现任何错误)。我希望 ViewModel 在检测到错误时向乘客表单发出信号,使其滚动到第一个错误字段。但是,我不确定如何在嵌套乘客表单函数中实现此操作。
我尝试从 ViewModel 传递 needScroll SharedFlow。在 ViewModel 中,在更新乘客的状态后,我将 Unit 值发送到 SharedFlow。但是,当 PassengerForm 在收到包含错误的新乘客状态之前收集 SharedFlow 的值时,就会出现问题。
class MyViewModel : ViewModel() {
private val _passengerState = MutableStateFlow(PassengerState())
val passengerState = _passengerState.asStateFlow()
private val _needScroll = MutableSharedFlow<Unit>()
val needScroll = _needScroll.asSharedFlow()
fun onChangeField1(value: String) {
_passengerState.update {
it.copy(field1 = it.field1.copy(value = value, isError = false))
}
}
fun onChangeField2(value: String) {
_passengerState.update {
it.copy(field2 = it.field2.copy(value = value, isError = false))
}
}
fun onSave() {
val cueState = _passengerState.value
if (cueState.field1.value.isEmpty() || cueState.field2.value.isEmpty()) {
_passengerState.update {
it.copy(
field1 = it.field1.copy(isError = cueState.field1.value.isEmpty()),
field2 = it.field2.copy(isError = cueState.field2.value.isEmpty()),
)
}
viewModelScope.launch {
_needScroll.emit(Unit)
}
}
}
}
@Composable
fun MyScreen(
viewModel: MyViewModel,
) {
val passengerState by viewModel.passengerState.collectAsStateWithLifecycle()
val needScroll = remember { mutableStateOf(viewModel.needScroll) }
MyPassengerForm(
passengerState = passengerState,
needScroll = needScroll,
onChangeField1 = remember { { viewModel.onChangeField1(it) } },
onChangeField2 = remember { { viewModel.onChangeField2(it) } },
onSave = remember { { viewModel.onSave() } },
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MyPassengerForm(
passengerState: PassengerState,
needScroll: State<SharedFlow<Unit>>,
onChangeField1: (String) -> Unit,
onChangeField2: (String) -> Unit,
onSave: () -> Unit,
) {
val lifecycle = LocalLifecycleOwner.current.lifecycle
val field1BringIntoView = remember { BringIntoViewRequester() }
val field2BringIntoView = remember { BringIntoViewRequester() }
LaunchedEffect(key1 = passengerState, key2 = needScroll) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
needScroll.value.collect {
val bringIntoView = when {
passengerState.field1.isError -> field1BringIntoView
passengerState.field2.isError -> field2BringIntoView
else -> null
}
launch {
bringIntoView?.bringIntoView()
}
}
}
}
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
TextField(
modifier = Modifier.bringIntoViewRequester(field1BringIntoView),
value = passengerState.field1.value,
onValueChange = onChangeField1,
)
TextField(
modifier = Modifier.bringIntoViewRequester(field2BringIntoView),
value = passengerState.field2.value,
onValueChange = onChangeField2,
)
Button(onClick = onSave) {
Text(text = "Save")
}
}
}
data class PassengerState(
val field1: FieldItem = FieldItem(),
val field2: FieldItem = FieldItem(),
)
data class FieldItem(
val value: String = "",
val isError: Boolean = false,
)
看到需要将 State 或 Flow 对象传递给可组合函数通常表明存在其他问题。 Compose 是声明性的,期望在 compose 树中向下传递不可变状态并向上传递事件。
在这种情况下,您的视图模型应该像这样公开
needScroll
:
private val _needScroll = MutableStateFlow(false)
val needScroll = _needScroll.asStateFlow()
然后你的屏幕可以像往常一样收集它,并且布尔值(不是流量,不是状态)可以传递给
MyPassengerForm
。现在,由于一切都是基于状态的,MyPassengerForm
只需要决定当needScroll
为真时要做什么:
if (needScroll) LaunchedEffect(passengerState) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
val bringIntoView = when {
passengerState.field1.isError -> field1BringIntoView
passengerState.field2.isError -> field2BringIntoView
else -> null
}
bringIntoView?.bringIntoView()
}
}
实际上,每次
passengerState
更改时都没有必要执行 LaunchedEffect,只有当 bringIntoView
更改时才需要执行。所以应该将其移到 LaunchedEffect 之外:
val bringIntoViewRequester = remember(needScroll, passengerState) {
if (needScroll) when {
passengerState.field1.isError -> field1BringIntoView
passengerState.field2.isError -> field2BringIntoView
else -> null
} else
null
}
if (bringIntoViewRequester != null) LaunchedEffect(bringIntoViewRequester) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
bringIntoViewRequester.bringIntoView()
}
}
虽然没有经过彻底测试,但我认为你甚至可以放弃
repeatOnLifecycle
。
您的视图模型的
onSave
方法现在只需要设置 _needScroll.value = true
而不是发出新值。
有趣的部分是如何将其重置为 false。这完全取决于您想要什么行为。如果不重置,每次
bringIntoViewRequester
变化时都会执行 LaunchedEffect。当再次执行 onSave
并且现在另一个字段出现错误时,情况可能是这样。当没有留下任何错误时,bringIntoViewRequester
为空,因此LaunchedEffect
被一起跳过。如果这种行为适合您,您甚至不需要 needScroll
变量和相应的流程,因此可以完全删除。
如果您有其他要求,您可以随时在视图模型中公开一个
onScroll
函数,当 needScroll
应该重置时,可以从可组合项中调用该函数:
fun onScroll() {
_needScroll.value = false
}
与您当前问题无关的一些附加说明:
您将视图模型的函数包装在多个 lambda 中并调用 Remember。相反,您应该只将对函数的引用传递给您的可组合项:
MyPassengerForm(
passengerState = passengerState,
needScroll = needScroll,
onChangeField1 = viewModel::onChangeField1,
onChangeField2 = viewModel::onChangeField2,
onSave = viewModel::onSave,
)
在视图模型中的某个时刻,您检索
_passengerState
流的当前值并将其保存在 cueState
中。在另一点上,您更新流的“then”当前值,并设置一个依赖于流的“older”状态的错误状态。问题是流量可以在这两点之间改变。确定新值所需的流程的每个部分都应始终放置在 update
lambda 中,如下所示:
_passengerState.update {
var newState = it
if (it.field1.value.isEmpty() || it.field2.value.isEmpty()) {
newState = it.copy(
field1 = it.field1.copy(isError = it.field1.value.isEmpty()),
field2 = it.field2.copy(isError = it.field2.value.isEmpty()),
)
_needScroll.value = true
}
newState
}