Jetpack Compose中LazyList items key与Composable key状态复用差异问题
两种Key写法的状态复用差异核心原因
两种写法的作用层级和状态留存逻辑完全不同:
- 当使用
LazyColumn.items(key = { it.id })写法时,传入的key是LazyList组件层面的全局项唯一标识,和项所在的列表位置无关。LazyList会为每个key绑定对应的remember/rememberSaveable状态,存入独立的项状态池:即使对应key的项暂时从列表中移除,状态也会被暂时保留,当后续列表中再次出现相同key的项时,会直接复用之前绑定的状态,减少重组开销。
问题就出在这个逻辑上:第一次新建的临时项key固定为-1L,用户输入内容保存后,该项id变为正整数,相当于key=-1的项从列表中消失,但其绑定的带输入内容的状态仍然留在LazyList的状态池中;当第二次点击FAB新建临时项,新项的key还是-1L,LazyList识别到同key项回归,直接把之前留存的旧状态绑定给新项,就出现了新项显示旧输入内容的问题。 - 当不给
items传key、转而在项内部用key(todo.id)包裹内容时,LazyList默认使用项的列表位置索引作为列表层级的项key,内部key()函数仅作用于当前列表项的子Composable树,状态生命周期和当前位置的项实例绑定。当项因为插入、id变更被移出原有位置或销毁时,对应子树的状态会随Composition的销毁被清理,不会留存在全局状态池中等待同key项复用,因此不会出现跨临时项的状态串用问题。
保留items key前提下的修复方案
核心原则是保证每个不同的列表实体(包括不同批次创建的临时项)在列表中拥有唯一的key,或者让rememberSaveable能区分同id下的不同实体,可选方案如下:
- 方案1(最推荐,符合LazyList设计规范):废弃固定
-1L作为临时项id的逻辑,新建临时项时生成全局唯一的临时id,比如取负的当前时间戳-System.currentTimeMillis()作为临时项id,待项保存到Room拿到正式自增id后,再替换为正式正整数id。该方案从根源上避免了不同临时项共用同一个key的问题,同时完全符合LazyList对key的唯一性要求,不会引发其他复用异常。 - 方案2(兼容现有固定-1L临时id的逻辑):给临时项增加额外的唯一标识字段,修改
rememberTodoItemState的入参和key逻辑,让rememberSaveable可以区分不同的临时项:- 给
TodoModel增加一个可选的tempCreateId字段,新建临时项时生成唯一值(随机字符串、时间戳均可),正式持久化的项该字段为null - 修改状态remember逻辑,将额外标识作为remember的key传入:
该方案下即使临时项id都是-1,只要创建时的临时id不同,@Composable internal fun rememberTodoItemState(todoModel: TodoModel) = rememberSaveable( // 临时项用临时创建id作为状态key,正式项用持久化id作为key keys = arrayOf(if(todoModel.id == -1L) todoModel.tempCreateId else todoModel.id), saver = TodoItemState.Saver ) { TodoItemState(todoModel) }rememberSaveable就会判定为新实体,不复用旧状态。 - 给
- 方案3(不推荐):在临时项保存成功后,手动通过
SavedStateRegistry移除key=-1绑定的状态,该方案需要依赖rememberSaveable自动生成的状态key,版本兼容性差,维护成本高。
内容的提问来源于stack exchange,提问作者z.g.y
相关产品推荐
相关产品推荐

