将操作传递给嵌套可组合项

问题描述 投票:0回答:1

我正在开发一个带有表单的屏幕,用于使用 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,
)
android kotlin android-jetpack-compose
1个回答
0
投票

看到需要将 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 }
    
    

© www.soinside.com 2019 - 2024. All rights reserved.