Jetpack Compose: LazyColumn/LazyRow 내부 코드 분석 ~ 3부 LazyLayout

Jetpack Compose: LazyColumn/LazyRow 내부 코드 분석 ~ 3부 LazyLayout

Apr 20, 2025. | By: pluulove

3부는 LazyList에서 호출하는 LazyLayout Composable 함수를 살펴볼 예정입니다.


본 글에서 다루는 FlowChart를 빠르게 확인하기 위해서는 아래 링크를 참고해 주세요

  • LazyLayout FlowChart : 링크
  • LazyList 전체 FlowChart : 링크

LazyLayout는 LazyColumn/LazyRow 사용 시 LazyList를 통해서 호출되는 Composable 함수입니다.

@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun LazyList(
   state: LazyListState,
   ...
) {
   val itemProviderLambda = rememberLazyListItemProviderLambda(...)
   val measurePolicy = rememberLazyListMeasurePolicy(...)
  
   LazyLayout(
      modifier = ...,
      ...
      prefetchState = state.prefetchState,
      measurePolicy = measurePolicy,
      itemProvider = itemProviderLambda
   )
}

LazyList 소스 출처

LazyLayout

LazyLayout은 필요한 item만 compose 및 배치하는 레이아웃입니다. scrollable layouts을 만드는 데 사용합니다.

  • LazyList : LazyColumn, LazyRow
  • LazyGrid : LazyVerticalGrid, LazyHorizontalGrid
  • LazyStaggeredGrid : LazyVerticalStaggeredGrid, LazyHorizontalStaggeredGrid
  • Pager : VerticalPager, HorizontalPager
@Composable
fun LazyLayout(
   /** measurePolicy의 일부로 item을 compose/measure하는데 사용할 수 있는 item에 대한 정보를 제공하는 ItemProvider를 생성 */
   itemProvider: () -> LazyLayoutItemProvider, 
   modifier: Modifier = Modifier,
   /** item을 Prefetch를 정의 */
   prefetchState: LazyLayoutPrefetchState? = null, 
   /** 필요한 item만 compose/measure 할 수 있는 MeasurePolicy */
   measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult 
) {
  ...
}

LazyLayout 소스 출처

LazyLayoutPrefetchState

lazy items Prefetch를 위한 상태로, lazy lazyout에서 prefetcher를 하는 데 사용됩니다. LazyLayout에서는 아래 2개의 프로퍼티가 사용됩니다

@Stable
class LazyLayoutPrefetchState(
   // Prefetch 요청을 실행하는 데 사용할 PrefetchScheduler 구현을 지정
   // null이 제공되면 플랫폼의 기본 PrefetchScheduler가 사용
   internal val prefetchScheduler: PrefetchScheduler? = null,
   ...
) {
   // PrefetchHandle, PrefetchRequest 생성을 담당
   internal var prefetchHandleProvider: PrefetchHandleProvider? = null
  
   // 새 item에 대한 precomposition을 예약
   @Deprecated(
      "Please use schedulePrecomposition(index) instead",
      level = DeprecationLevel.WARNING
   )
   fun schedulePrefetch(index: Int): PrefetchHandle {
      return prefetchHandleProvider?.schedulePrecomposition(
         index,
         true,
         prefetchMetrics,
      ) ?: DummyHandle
   }
  
   // 새 item에 대한 precomposition을 예약
   fun schedulePrecomposition(index: Int): PrefetchHandle = schedulePrecomposition(index, true)

   internal fun schedulePrecomposition(index: Int, isHighPriority: Boolean): PrefetchHandle {
      return prefetchHandleProvider?.schedulePrecomposition(
         index,
         isHighPriority,
         prefetchMetrics,
      ) ?: DummyHandle
   }
  
   // 새 item에 대한 precomposition과 premeasure를 예약
   @Deprecated(
      "Please use schedulePremeasure(index, constraints) instead",
      level = DeprecationLevel.WARNING
   )
   fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle =
      schedulePrecompositionAndPremeasure(index, constraints, null)
  
   // 새 item에 대한 precomposition과 premeasure를 예약
   fun schedulePrecompositionAndPremeasure(
      index: Int,
      constraints: Constraints,
      onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)? = null
   ): PrefetchHandle =
      schedulePrecompositionAndPremeasure(index, constraints, true, onItemPremeasured)

   internal fun schedulePrecompositionAndPremeasure(
      index: Int,
      constraints: Constraints,
      isHighPriority: Boolean,
      onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)? = null
   ): PrefetchHandle {
      return prefetchHandleProvider?.schedulePremeasure(
         index,
         constraints,
         prefetchMetrics,
         isHighPriority,
         onItemPremeasured
      ) ?: DummyHandle
   }
  
   sealed interface PrefetchHandle {
      // 이전에 예약된 item이 더 이상 필요하지 않음을 Prefetch에게 알림
      // item이 이미 precomposed된 경우 해당 item은 폐기
      fun cancel()

      // Prefetch 요청을 긴급한 것으로 표시하여 요청된 item이 다음 프레임에 필요할 것으로 예상됨을 알림
      fun markAsUrgent()
   }
  
   ...
}

LazyLayoutPrefetchState 소스 출처

PrefetchScheduler

PrefetchScheduler 인터페이스는 schedulePrefetch를 통해 Prefetch 요청을 수락하고, frame idle 시간 등 사용자 경험에 미치는 영향을 최소화하는 방식으로 실행 시기를 결정합니다. 요청은 PrefetchRequest.execute를 호출하여 실행합니다.

interface PrefetchScheduler {
   // Prefetch 요청을 수락합니다.
   // 구현은 UX에 미치는 영향을 최소화할 수 있는 실행 시간을 찾아야 함
   fun schedulePrefetch(prefetchRequest: PrefetchRequest)
}

PrefetchScheduler 소스 출처

별도 처리없이 기본 LazyColumn을 사용한 경우에는 LazyLayoutPrefetchState에 전달되는 PrefetchScheduler는 null입니다. 대신, PrefetchScheduler를 구현한 LazyListPrefetchStrategyrememberLazyListState에 전달하면 지정된 PrefetchScheduler가 사용됩니다.

LazySaveableStateHolderProvider

LazySaveableStateHolderProvider는 LazyLayout에서 가장 큰 영역을 차지하는 Composable 함수입니다.

@Composable
fun LazyLayout(...) {
   ...
   LazySaveableStateHolderProvider { saveableStateHolder ->
   }
}

이 함수는 lazy layout 항목과 함께 사용할 SaveableStateHolder를 제공합니다. 이를 통해 LazyRow 스크롤 위치와 같은 item의 상태를 저장/복원할 수 있습니다. 또한, SaveableStateHolder가 제공하는 기본 기능 외에도 부모 SaveableStateRegistrySaveableStateRegistry#performSave를 호출할 때에는 현재 보이는 항목만 저장합니다.

현재 보이는 item만 저장하여 Bundle에서 사용하는 공간을 절약하여 TransactionTooLargeException과 충돌을 방지합니다.

LazySaveableStateHolderProvider 내부에서는 LazySaveableStateHolder 클래스 인스턴스를 생성하여 rememberSaveable한 holder를 만듭니다. 그리고, 이 holder는 CompositionLocalProvider를 통해 content에서 SaveableStateRegistry 접근 시 사용되도록 합니다.

@Composable
internal fun LazySaveableStateHolderProvider(content: @Composable (SaveableStateHolder) -> Unit) {
   val currentRegistry = LocalSaveableStateRegistry.current
   val wrappedHolder = rememberSaveableStateHolder()
   val holder =
      rememberSaveable(
         currentRegistry,
         saver = LazySaveableStateHolder.saver(currentRegistry, wrappedHolder)
      ) {
         LazySaveableStateHolder(currentRegistry, emptyMap(), wrappedHolder)
      }
   CompositionLocalProvider(LocalSaveableStateRegistry provides holder) { content(holder) }
}

LazySaveableStateHolderProvider 소스 출처

LazySaveableStateHolder

LazySaveableStateHolder는 SaveableStateRegistrySaveableStateHolder 인터페이스를 구현합니다. 또한 Savable에 필요한 Saver도 제공하고 있습니다.

생성자 파라미터로 SaveableStateRegistry/SaveableStateHolder를 전달 받고, 이를 통해서 compose의 subtree를 지우기 전에 rememberSaveable로 상태를 저장 및 복원하여 다시 composition할 수 있도록 합니다.

private class LazySaveableStateHolder(
   private val wrappedRegistry: SaveableStateRegistry,
   private val wrappedHolder: SaveableStateHolder
) : SaveableStateRegistry by wrappedRegistry, SaveableStateHolder {
  ...
  private val previouslyComposedKeys = mutableScatterSetOf<Any>()

  // 등록된 모든 value providers를 실행하고 value를 map으로 반환
  override fun performSave(): Map<String, List<Any?>> {
    previouslyComposedKeys.forEach { wrappedHolder.removeState(it) }
    return wrappedRegistry.performSave()
  }

  @Composable
  override fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) {
    wrappedHolder.SaveableStateProvider(key, content)
    DisposableEffect(key) {
      previouslyComposedKeys -= key
      onDispose { previouslyComposedKeys += key }
    }
  }

  // 전달된 key와 연관된 저장된 상태를 제거
  override fun removeState(key: Any) {
    wrappedHolder.removeState(key)
  }

  companion object {
    fun saver(parentRegistry: SaveableStateRegistry?, wrappedHolder: SaveableStateHolder) =
      Saver<LazySaveableStateHolder, Map<String, List<Any?>>>(
        save = { it.performSave().ifEmpty { null } },
        restore = { restored ->
          LazySaveableStateHolder(parentRegistry, restored, wrappedHolder)
        }
    )
  }  
}

LazySaveableStateHolder 소스 출처

SubcomposeLayoutState

LazySaveableStateHolderProvider에 전달되는 content lambda 내부 로직을 볼 차례입니다. 처음 만나는 LazyLayoutItemContentFactory는 람다 캐시 및 제공하는 역할 담당하는 Factory 클래스입니다.

LazyLayoutItemContentFactory/LazyLayoutItemReusePolicy는 지난 블로그에서 다루고 있습니다

@Composable
fun LazyLayout(
   itemProvider: () -> LazyLayoutItemProvider,
   modifier: Modifier = Modifier,
   prefetchState: LazyLayoutPrefetchState? = null,
   measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult
) {
   val currentItemProvider = rememberUpdatedState(itemProvider)

   LazySaveableStateHolderProvider { saveableStateHolder ->
      val itemContentFactory = remember {
         LazyLayoutItemContentFactory(saveableStateHolder) { currentItemProvider.value() }
      }
      val subcomposeLayoutState = remember {
         SubcomposeLayoutState(LazyLayoutItemReusePolicy(itemContentFactory))
      }
      ...
   }
}

그 중 SubcomposeLayout을 호출하기 전 상태를 담당하는 SubcomposeLayoutState가 있습니다. 레이아웃 재사용을 위해서 slot의 저장을 담당하는 SubcomposeSlotReusePolicy를 생성자 파라미터로 받습니다. 여기까지오면 레이아웃에 따른 아이템 생성, 상태 저장/복원을 하는 추상적인 로직들을 살펴본 것 입니다.

class SubcomposeLayoutState(
   private val slotReusePolicy: SubcomposeSlotReusePolicy
) {
   ...
   // slotId에 대한 content를 작성.
   // content가 이미 compose되어있으면 measure 단계 중 scope.subcompose(slotId) 호출이 더 빨라짐.
   fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle = 
      state.precompose(slotId, content)
   
   // slotId에 대한 PausedPrecomposition을 호출
   // 점진적인 방식으로 composition을 수행
   // 전체 또는 부분 precomposition을 수행하면 content가 이미 composed 되어 있으므로 
   // measure 단계 중 다음 scope.subcompose(slotId) 호출이 더 빨라짐
   fun createPausedPrecomposition(
      slotId: Any?,
      content: @Composable () -> Unit
   ): PausedPrecomposition = state.precomposePaused(slotId, content)
   ...
}

SubcomposeLayoutState 소스 링크

LazyLayoutItemReusePolicy는 이전 블로그에서 다루고 있습니다.

rememberDefaultPrefetchScheduler

prefetchState가 null이 아닐 때 동작하는 코드 중 prefetchState.prefetchScheduler가 없을 때 제공하는 기본 PrefetchScheduler(rememberDefaultPrefetchScheduler)가 있습니다.

기본 Android PrefetchScheduler가 궁금하면 다음 링크를 참고하세요 : AndroidPrefetchScheduler 소스

@Composable
fun LazyLayout(
   itemProvider: () -> LazyLayoutItemProvider,
   modifier: Modifier = Modifier,
   prefetchState: LazyLayoutPrefetchState? = null,
   measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult
) {
   val currentItemProvider = ...

   LazySaveableStateHolderProvider { saveableStateHolder ->
      val itemContentFactory = ...
      val subcomposeLayoutState = ...
      if (prefetchState != null) {
         val executor = prefetchState.prefetchScheduler ?: rememberDefaultPrefetchScheduler()
         DisposableEffect(prefetchState, itemContentFactory, subcomposeLayoutState, executor) {
            // prefetchHandleProvider에 PrefetchHandleProvider 인스턴스 설정
            prefetchState.prefetchHandleProvider =
               PrefetchHandleProvider(itemContentFactory, subcomposeLayoutState, executor)
            onDispose { prefetchState.prefetchHandleProvider = null }
         }
      }
      ...
   }
}

AndroidX의 Compose도 Multiplatform 지원으로 rememberDefaultPrefetchScheduler는 expect로 선언되어 있으며, Android에서는 AndroidPrefetchScheduler가 최종 사용되는 구조입니다.

@ExperimentalFoundationApi
@Composable
internal expect fun rememberDefaultPrefetchScheduler(): PrefetchScheduler

// foundation-android-1.8.0-rc03
@ExperimentalFoundationApi
@Composable
internal actual fun rememberDefaultPrefetchScheduler(): PrefetchScheduler {
   return if (RobolectricImpl != null) {
      RobolectricImpl
   } else {
      val view = LocalView.current
      remember(view) { AndroidPrefetchScheduler(view) }
   }
}

// (2025.04.19) rememberDefaultPrefetchScheduler
@ExperimentalFoundationApi
@Composable
internal actual fun rememberDefaultPrefetchScheduler(): PrefetchScheduler {
   return if (RobolectricImpl != null) {
      RobolectricImpl
   } else {
      val view = LocalView.current
      remember(view) {
         val existing = view.getTag(R.id.compose_prefetch_scheduler) as? PrefetchScheduler
         if (existing == null) {
            val scheduler = AndroidPrefetchScheduler(view)
            view.setTag(R.id.compose_prefetch_scheduler, scheduler)
            scheduler
         } else {
            existing
         }
      }
   }
}

rememberDefaultPrefetchScheduler 소스 출처

PrefetchHandleProvider

PrefetchHandleProvider는 prefetch를 예약하기 위한 API를 제공하는 LazyLayoutPrefetchState를 인덱스에서 key/content를 확인하는 LazyLayoutItemContentFactory, precompose/premeasure 방법을 아는 SubcomposeLayoutState, 요청을 실행하는 데 사용되는 특정 PrefetchScheduler에 연결하는 데 사용됩니다.

// (2025.04.19) PrefetchHandleProvider
internal class PrefetchHandleProvider(
   private val itemContentFactory: LazyLayoutItemContentFactory,
   private val subcomposeLayoutState: SubcomposeLayoutState,
   private val executor: PrefetchScheduler
) {
   fun schedulePrecomposition(
      index: Int,
      isHighPriority: Boolean,
      prefetchMetrics: PrefetchMetrics,
   ): PrefetchHandle =
      HandleAndRequestImpl(index, prefetchMetrics, executor as? PriorityPrefetchScheduler, null)
         .also { ... }
  
   fun schedulePremeasure(
      index: Int,
      constraints: Constraints,
      prefetchMetrics: PrefetchMetrics,
      isHighPriority: Boolean,
      onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)?
   ): PrefetchHandle =
      HandleAndRequestImpl(
         index,
         constraints,
         prefetchMetrics,
         executor as? PriorityPrefetchScheduler,
         onItemPremeasured
      ).also { ... }

   ...

   @ExperimentalFoundationApi
   private inner class HandleAndRequestImpl(
      override val index: Int,
      private val prefetchMetrics: PrefetchMetrics,
      private val priorityPrefetchScheduler: PriorityPrefetchScheduler?,
      private val onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)?,
   ) : PrefetchHandle, PrefetchRequest, LazyLayoutPrefetchResultScope {
      ...
   }
}

PrefetchHandleProvider 소스 출처

prefetch 요청 순서

대략적인 prefetch 요청은 아래와 같은 순서로 호출됩니다.

  1. 사용자 및 시스템에 의해서 스크롤 시 LazyListState#onScroll 호출 [링크]
  2. DefaultLazyListPrefetchStrategy#LazyListPrefetchScope.onScroll 호출 [링크]
    1. 스크롤 방향에 따라서 prefetch할 index 탐색
  3. LazyListState#prefetchScope에서 prefetchState#schedulePrecompositionAndPremeasure 호출 [링크]
  4. LazyLayoutPrefetchState#schedulePrecompositionAndPremeasure에서 prefetchHandleProvider#schedulePremeasure 호출 [링크]
  5. HandleAndRequestImpl 인스턴스 생성하여 PrefetchHandle 반환 [링크]
    1. 생성된 PrefetchHandle는 DefaultLazyListPrefetchStrategy#currentPrefetchHandle에 반영

SubcomposeLayout

마지막은 LazyLayout 내부에서 호출되는 SubcomposeLayout입니다. LazyLayout에서는 파라미터로 SubcomposeLayoutState, Modifier, measurePolicy를 전달합니다.

@Composable
fun LazyLayout(
   itemProvider: () -> LazyLayoutItemProvider,
   modifier: Modifier = Modifier,
   prefetchState: LazyLayoutPrefetchState? = null,
   measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult
) {
   val currentItemProvider = ...
  
   LazySaveableStateHolderProvider { saveableStateHolder ->
      val itemContentFactory = ...
      val subcomposeLayoutState = ...
      ...
      SubcomposeLayout(
         subcomposeLayoutState,
         modifier.traversablePrefetchState(prefetchState),
         remember(itemContentFactory, measurePolicy) {
            { constraints ->
               with(LazyLayoutMeasureScopeImpl(itemContentFactory, this)) {
                  measurePolicy(constraints)
               }
            }
         }
      )
   }
}

Modifier.traversablePrefetchState

/**
 * TraversablePrefetchStateNode 횡단을 통해 
 * LazyLayout의 LazyLayoutPrefetchState를 검색할 수 있도록 하는 Modifier
 */
@ExperimentalFoundationApi
internal fun Modifier.traversablePrefetchState(
   lazyLayoutPrefetchState: LazyLayoutPrefetchState?
): Modifier {
   return lazyLayoutPrefetchState?.let { this then TraversablePrefetchStateModifierElement(it) }
      ?: this
}

@ExperimentalFoundationApi
private class TraversablePrefetchStateNode(
   var prefetchState: LazyLayoutPrefetchState,
) : Modifier.Node(), TraversableNode {

   override val traverseKey: String = TraversablePrefetchStateNodeKey
}

@ExperimentalFoundationApi
private data class TraversablePrefetchStateModifierElement(
   private val prefetchState: LazyLayoutPrefetchState,
) : ModifierNodeElement<TraversablePrefetchStateNode>() {
   override fun create() = TraversablePrefetchStateNode(prefetchState)

   override fun update(node: TraversablePrefetchStateNode) {
      node.prefetchState = prefetchState
   }

   override fun InspectorInfo.inspectableProperties() {
      name = "traversablePrefetchState"
      value = prefetchState
   }
}

AndroidX 소스 코드를 통해서 살펴본바로 위 설정으로 TraversablePrefetchStateNode를 통해 prefetchState가 사용되는 곳은 nested로 prefetchState한 곳입니다.

internal class PrefetchHandleProvider(...) {
   private inner class HandleAndRequestImpl(...) {
      private var nestedPrefetchController: NestedPrefetchController? = null
      
      override fun PrefetchRequestScope.execute(): Boolean {
         ...
         nestedPrefetchController = resolveNestedPrefetchStates()
      }
      
      private fun resolveNestedPrefetchStates(): NestedPrefetchController? {
         ...
         var nestedStates: MutableList<LazyLayoutPrefetchState>? = null
         precomposedSlotHandle.traverseDescendants(TraversablePrefetchStateNodeKey) {
            val prefetchState = (it as TraversablePrefetchStateNode).prefetchState
            nestedStates =
               nestedStates?.apply { add(prefetchState) } ?: mutableListOf(prefetchState)
            TraverseDescendantsAction.SkipSubtreeAndContinueTraversal
         }
         return nestedStates?.let { NestedPrefetchController(it) }
      }
   }
}

소스 출처

SubcomposeLayout

SubcomposeLayout의 동작은 measure 단계에서 실제 content를 subcompose할 수 있게 해줍니다. 본 글에서는 다루지 않지만 SubcomposeLayout은 커스텀 레이아웃 작업시 자주 사용됩니다.

@Composable
@UiComposable
fun SubcomposeLayout(
   state: SubcomposeLayoutState,
   modifier: Modifier = Modifier,
   measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
) {
   val compositeKeyHash = currentCompositeKeyHashCode.hashCode()
   val compositionContext = rememberCompositionContext()
   val materialized = currentComposer.materialize(modifier)
   val localMap = currentComposer.currentCompositionLocalMap
   ReusableComposeNode<LayoutNode, Applier<Any>>(
      factory = LayoutNode.Constructor,
      update = {
         set(state, state.setRoot)
         set(compositionContext, state.setCompositionContext)
         set(measurePolicy, state.setMeasurePolicy)
         set(localMap, SetResolvedCompositionLocals)
         set(materialized, SetModifier)
         set(compositeKeyHash, SetCompositeKeyHash)
      }
   )
   if (!currentComposer.skipping) {
      SideEffect { state.forceRecomposeChildren() }
   }
}

SubcomposeLayout 소스 출처

SubcomposeLayout 이해에 도움이 되는 영상

comments powered by Disqus

Currnte Pages Tags

Android AndroidX

About

Pluu, Android Developer Blog Site

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

Using Theme : SOLID SOLID Github

Social Links