表单组件

Flutter 的 Form 组件,ReactNative 中的 RHF 都是体验非常不错的表单组件,特别是 flutter 的。

但是 Compose 一直没有找到合适的表单组件,所以我根据自身需求自定义了一个:

  • 首次提交后触发自动校验
  • 支持交叉校验(例如密码和确认密码)
  • 动态表单(根据条件,隐藏或显示表单项)

如何收集表单项

Compose 中最大的难点在于收集表单项,需要实现 FormField 的注册和反注册。如果参考 RHL 的话通过 DisposeEffect 是很合适的方案。

但是坑爹的地方在于这种方案和 Compose 的路由是不匹配的,因为 Compose 路由是不堆叠组件的,切换页面时即销毁当前页面,页面中的所有组件都会依次执行 dispose。

所以暂时想到的是每次 Form 渲染时重新收集表单:

val LocalFormCollector = compositionLocalOf<MutableList<FieldState<*>>> {
    error("No LocalFormCollector found")
}

@Composable
fun Form(
    controller: FormController,
    content: @Composable () -> Unit
) {
    val collectedFields = remember { mutableStateListOf<FieldState<*>>() }
    // 每次Form重组时重新收集表单
    collectedFields.clear()
    Log.d("Form", "Composing Form")

    CompositionLocalProvider(LocalFormCollector provides collectedFields) {
        content()
    }

    // 同步字段顺序到 controller
    LaunchedEffect(collectedFields) {
        snapshotFlow { collectedFields.toList() }
            .collectLatest { controller.updateFields(it) }
    }
}

@Composable
fun <T> FormField(state: FieldState<T>, content: @Composable FieldRenderScope<T>.() -> Unit) {
    val collector = LocalFormCollector.current
    if (!collector.contains(state)) collector.add(state)
    Log.d("FormField", "Composing FormField:${state.key}")
    FieldRenderScope(
        value = state.value,
        onValueChange = { newValue -> state.value = newValue },
        error = state.error
    ).content()
}

性能优化

其实从性能方面来说能优化的点不多了,额外的优化带来的是使用上的不便。但是最近发现确实有个可以优化的点,对于一个比较大的表单来说可能会有性能上的提升。

FormField 由于状态变化,所以重组是非常频繁的,比如输入框内的内容发生变化都会导致重组。而 collector.contains(state) 这行代码的时间复杂度是 O(n), 对于大型表单来说比较耗费计算资源。

FormCollector 原本只是一个 SnapshotStateList,我们将它改造一下,现在 add 方法的时间复杂度提升到 O(1) 级别

private class FormCollector {
    val fields = mutableStateListOf<FieldState<*>>()
    private val seen = IdentityHashMap<FieldState<*>, Boolean>()

    fun clear() {
        fields.clear()
        seen.clear()
    }

    fun add(field: FieldState<*>) {
        if (seen.put(field, true) == null) {
            fields.add(field)
        }
    }
}

相应的代码其它部分也要调整,这里有不贴代码了,可以到 GitHub 上直接查看整个项目仓库。

完整参考

我使用自定义的表单组件写了一个简单的 Demo,支持文章开头说的那些特性: https://github.com/aitsuki/ComposeForm