我正在开发一个带有表格的屏幕,可以使用Jetpack Compose输入乘客数据.乘客数据表功能集成在屏幕功能中.按下"保存"按钮后,视图模型会验证输入的数据,并在发现错误时更新乘客状态.当检测到错误时,我希望视图模型向乘客表格发送信号,以滚动到第一个错误字段.然而,我不确定如何在嵌套乘客表单功能中实现此操作.

我try 从View Model传递needScroll SharedFlow.在View Model中,在乘客状态更新后,我将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对象传递给Compable函数通常表明其他地方出了问题.Compose是声明性的,期望不可变状态被传递,事件被传递到编写树中.

在这种情况下,您的视图模型应该像这样expose 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,而不是发出新值.

有趣的部分是如何将其重置为假.这完全取决于你想要什么样的行为.如果您不重置它,则每次进行bringIntoViewRequester次更改时都会执行LaunchedEffect.当再次执行onSave并且现在另一个字段出现错误时,这可能是真的.当没有留下错误时,bringIntoViewRequester为空,因此将全部跳过LaunchedEffect.如果这种行为对您来说没问题,那么您甚至不需要needScroll变量和相应的流,因此可以完全删除它.

如果您有一些其他要求,您可以随时在视图模型中公开一个onScroll函数,当应该重置needScroll时,可以从您的组合中调用该函数:

fun onScroll() {
    _needScroll.value = false
}

一些与您当前问题无关的附加注释:

  • 您将视图模型的函数包装在多个lambadas中并调用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
    }
    

Android相关问答推荐

Android可组合继承?

我到底应该如何分享我的应用程序中的图片?

添加可组合元素的列表?

Jetpack Compose:带芯片的Textfield

从惰性列中删除项目时Jetpack Compose崩溃

Android 12+BLE字节不同

从我的 Android 应用程序发送到 Gin 时失败,但从 Postman 发送到 Gin 时成功

Android:使用依赖项 ViewModelProviderFactory 初始化 ViewModel 的正确方法

ArrayList 上的 Android intent.getParcelableArrayListExtra 引发 Nullpointer 异常

Jetpack Compose 中带有权重的行和 AnimatedVisibility 会 destruct UI

在模块 jetified-kotlin-stdlib-1.8.10 中发现重复的类 kotlin.random.jdk8,带有启动基准

compose 导航参数字符串包含花括号?

Jetpack Compose UI - 在 AlertDialog 中单击时按钮宽度会发生变化

如何使用 Jetpack Compose 制作两个圆圈

Compose Accompaniist Pager 中的 TabRow/Tab 重组问题

Jetpack Compose:如何绘制这样的路径/线

使用 Room 在 SQLite 中保存复杂的 JSON 响应

Android WebView 没有在第一次页面完成时从本地存储读取数据?

可组合的 fillMaxSize 和旋转不起作用

lambda 函数中的类型不匹配 - Kotlin