LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유

LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유

Nov 30, 2024. | By: pluulove

본 글에서는 동일한 Key를 사용시 크래시가 발생하는 내부 코드 통해 이유를 살펴보겠습니다.

Android Developers의 Compose 가이드에서 설명하는 것처럼 LazyColumn/Row에서의 Key는 고유한 값을 사용하도록 안내하고 있습니다.

Key가 동일하면 크래시가 발생하는 것이 이유입니다.

java.lang.IllegalArgumentException: Key "1" was already used. If you are using LazyColumn/Row please make sure you provide a unique key for each item.

Key로 “1”을 반환하도록 했을 때의 결과

실제 크래시가 발생하는 내부 코드

LayoutNodeSubcompositionsState#subcompose로 전달된 파라미터 slotId로 처리 시, root.foldedChildren에서 node로 위치를 찾은 결과(=itemIndex)가 현재 위치(=currentIndex)보다 이전의 위치일 때 해당 크래시가 발생합니다.

internal class LayoutNodeSubcompositionsState(
   private val root: LayoutNode,
   ...
) : ComposeNodeLifecycleCallback {
   // this map contains active slotIds (without precomposed or reusable nodes)
   private val slotIdToNode = hashMapOf<Any?, LayoutNode>()
   ...
   fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
      ...
      val node = slotIdToNode.getOrPut(slotId) { ... }

      if (root.foldedChildren.getOrNull(currentIndex) !== node) {
         // the node has a new index in the list
         val itemIndex = root.foldedChildren.indexOf(node)
         requirePrecondition(itemIndex >= currentIndex) {
            "Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " +
               "sure you provide a unique key for each item."
         }
         if (currentIndex != itemIndex) {
            move(itemIndex, currentIndex)
         }
      }
      currentIndex++

      subcompose(node, slotId, content)

      return ...
   }
}

소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt#L480-L483

LayoutNodeSubcompositionsState#subcompose를 호출하는 곳은 LazyLayoutMeasureScopeImpl이며, item의 key값이 subcompose 함수의 slotId 파라미터로 전달됩니다.

internal class LazyLayoutMeasureScopeImpl
internal constructor(
   private val itemContentFactory: LazyLayoutItemContentFactory,
   private val subcomposeMeasureScope: SubcomposeMeasureScope
) : LazyLayoutMeasureScope, MeasureScope by subcomposeMeasureScope {
   ...
   override fun measure(index: Int, constraints: Constraints): List<Placeable> {
      val cachedPlaceable = placeablesCache[index]
      return if (cachedPlaceable != null) {
         cachedPlaceable
      } else {
         val key = itemProvider.getKey(index)
         val contentType = itemProvider.getContentType(index)
         val itemContent = itemContentFactory.getContent(index, key, contentType)
         val measurables = subcomposeMeasureScope.subcompose(key, itemContent)
         ...
      }
   }

소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt#L116-L128

크래시가 발생하는 흐름

LayoutNodeSubcompositionsState#subcompose 내부에서 크래시가 나는 케이스를 간단하게 정리하면 아래와 같습니다.

  1. 첫 번째 아이템 Key “1”로 slotIdToNode에 Node가 추가
  2. 현재 위치를 담당하는 currentIndex를 1 증가
  3. 두 번째 아이템 Key “1”로 slotIdToNode#getOrPut에 의해 1번에서 추가된 Node가 반환
  4. 3번에서 반환된 Node를 사용해 root.foldedChildren에서 위치를 검색하면 1번에서 추가된 Node의 위치가 반환(=itemIndex)
  5. requirePrecondition(itemIndex >= currentIndex)에 의해서 itemIndex위치가 currentIndex보다 값이 작으므로 Crash가 발생

comments powered by Disqus

Currnte Pages Tags

Android AndroidX

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links