Jetpack Compose Data Class重组异常:子函数更新致字段数据丢失
调试Jetpack Compose地址组件时发现,编辑一个字段会清除其他字段的修改。将内部的onChange从函数定义改为Lambda赋值后问题解决,但不清楚背后的机制差异。两种写法逻辑完全一致,仅定义方式不同:
@Composable fun Address( modifier: Modifier = Modifier, config: AddressConfiguration = AddressConfiguration(), address: AddressResponse = AddressResponse(), onUpdate: (response: AddressResponse, isValid: Boolean) -> Unit, ) { // 可正常工作的Lambda写法 val onChange: (update: AddressUpdate) -> Unit = { update -> val response = when (update) { is AddressUpdate.Component -> address.copyWith(update.component, update.value) is AddressUpdate.Address -> update.address } val isValid = config.isValid(response) onUpdate(response, isValid) } // 会导致状态丢失的函数写法 // fun onChange(update: AddressUpdate): Unit { // val response = when (update) { // is AddressUpdate.Component -> address.copyWith(update.component, update.value) // is AddressUpdate.Address -> update.address // } // val isValid = config.isValid(response) // onUpdate(response, isValid) // } ... }
核心原因解析
问题本质在于Compose的重组判定机制与函数/Lambda的稳定性、闭包捕获行为的差异:
1. Compose的重组判定逻辑
Compose通过对比子组件参数的相等性决定是否触发重组,对于函数类型参数默认使用引用相等性判断。同时Compose会对参数做稳定性推断:若参数被判定为稳定(不会发生变化),则不会触发子组件重组。
2. 两种onChange定义的关键差异
Lambda赋值写法
每次Address组件因address参数更新而重组时,都会生成一个新的Lambda实例。Compose无法推断Lambda的稳定性,会判定TextInput的onChange参数发生变化,触发TextInput重组。重组后的TextInput持有新Lambda,该Lambda捕获了当前重组时的最新address参数值,因此编辑字段时会基于最新地址数据修改,不会丢失之前的变更。函数定义写法
Kotlin中,Composable内部定义的函数会被Compose推断为稳定参数(因函数逻辑固定,Compose默认认为其引用不会变化)。当address更新导致Address重组时,TextInput的onChange参数被判定为未变化,因此不会触发TextInput重组。此时TextInput仍持有第一次重组时创建的旧函数引用,该函数闭包捕获的是初始或更早的旧address参数值。切换到其他字段编辑时,调用旧函数会基于旧address生成新的AddressResponse,导致之前的字段修改全部丢失。
3. 同一字段编辑正常的原因
编辑同一字段时,每次输入都会触发onUpdate更新外部状态,Address重组后,当前编辑的TextInput的value参数会基于最新address更新,因此显示正常。但其他字段的TextInput未重组,仍持有旧的onChange函数,切换后就会出现数据丢失。
兼容函数写法的解决方案
如果想保留函数定义的写法,可以通过rememberUpdatedState包裹address参数,确保函数能访问到最新值:
@Composable fun Address( modifier: Modifier = Modifier, config: AddressConfiguration = AddressConfiguration(), address: AddressResponse = AddressResponse(), onUpdate: (response: AddressResponse, isValid: Boolean) -> Unit, ) { val latestAddress = rememberUpdatedState(address) fun onChange(update: AddressUpdate): Unit { val response = when (update) { is AddressUpdate.Component -> latestAddress.value.copyWith(update.component, update.value) is AddressUpdate.Address -> update.address } val isValid = config.isValid(response) onUpdate(response, isValid) } // ... }
内容的提问来源于stack exchange,提问作者Aaron Hayman

