2부 두 번째 글에서는 LazyList에서 호출하는 rememberLazyListMeasurePolicy 함수를 살펴볼 예정입니다.
본 글에서 다루는 FlowChart를 빠르게 확인하기 위해서는 아래 링크를 참고해 주세요
LazyList의 두 번째 단계는 measure를 위한 정책을 담당하는 부분입니다. 필요한 item만 compose/measure할 수 있는 인스턴스를 생성합니다.
@Composable
internal fun LazyList(
...
) {
val itemProviderLambda = rememberLazyListItemProviderLambda(state, content)
...
val measurePolicy = rememberLazyListMeasurePolicy(
itemProviderLambda,
...
)
...
}
private Composable 함수인 rememberLazyListMeasurePolicy는 LazyList를 통해서 다수의 파라미터를 그대로 전달받습니다. 함수의 리턴값으로 LazyLayoutMeasureScope.(Constraints) -> MeasureResult
이 리턴됩니다. 리턴 타입을 통해서 알 수 있듯이 LazyLayoutMeasureScope Receiver 확장함수이며 lambda 파라미터로 Constraints 타입을 받아서 최종 MeasureResult 타입을 리턴합니다.
리턴값은 추후 LazyLayout의 measurePolicy 파라미터로 전달됩니다.
@Composable
private fun rememberLazyListMeasurePolicy(
/** List의 Item provider */
itemProviderLambda: () -> LazyListItemProvider,
/** 스크롤 위치를 제어하는 상태 */
state: LazyListState,
/** 전체 콘텐츠에 추가될 내부 패딩 */
contentPadding: PaddingValues,
/** 스크롤 및 레이아웃 방향 바꾸기 여부 */
reverseLayout: Boolean,
/** List 레이아웃 방향 */
isVertical: Boolean,
/** 표시되는 항목 전후의 layout할 item 갯수 */
beyondBoundsItemCount: Int,
/** item을 가로로 정렬하는 Alignment */
horizontalAlignment: Alignment.Horizontal?,
/** item을 세로로 정렬하는 Alignment */
verticalAlignment: Alignment.Vertical?,
/** item의 가로 Arrangement */
horizontalArrangement: Arrangement.Horizontal?,
/** item의 세로 Arrangement */
verticalArrangement: Arrangement.Vertical?,
/** 애니메이션용 Scope */
coroutineScope: CoroutineScope,
/** 그래픽 레이어 생성에 사용 */
graphicsContext: GraphicsContext,
/** sticky item의 Scroll behavior */
stickyItemsPlacement: StickyItemsPlacement?
) : LazyLayoutMeasureScope.(Constraints) -> MeasureResult {
return remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
state,
contentPadding,
reverseLayout,
isVertical,
horizontalAlignment,
verticalAlignment,
horizontalArrangement,
verticalArrangement,
graphicsContext,
stickyItemsPlacement,
) {
{ containerConstraints ->
...
}
}
}
rememberLazyListMeasurePolicy에서 가장 먼저 다뤄볼 것은 리턴에 사용되는 LazyLayoutMeasureScope입니다. LazyLayoutMeasureScope는 LazyLayout의 measure lambda의 receiver scope입니다. measure lambda의 리턴값은 layout 별로 리턴되는 MeasureResult입니다.
LazyLayoutMeasureScope는 Android Developer 사이트에서는 다음과 같이 기술되어 있습니다.
일반적인 커스텀 레이아웃 작성 흐름과의 주요 차이점은 item index와 제약 조건을 받아들이고 item을 기반으로 구성한 다음 item content block에서 방출되는 모든 레이아웃을 측정하는 새로운 함수 LazyLayoutMeasureScope#measure이 있다는 것입니다.
sealed interface LazyLayoutMeasureScope : MeasureScope {
// lazy layout의 item을 Subcompose하고 measure 한다
//
// 리턴되는 Placeable 리스트는
// item composable에 여러 개의 자식을 배출한 경우 여러 개의 placeables을 받게 되며,
// 각 item은 전달된 제약 조건(=constraints)을 사용하여 측정
fun measure(index: Int, constraints: Constraints): List<Placeable>
...
}
LazyLayoutMeasureScope 인터페이스의 구현체는 추후에 언급할 예정입니다.
MeasureScope는 layout의 measure lambda의 receiver scope입니다. measure lambda의 리턴 값은 MeasureScope#layout에서 리턴하는 MeasureResult입니다. MeasureScope에는 기본적인 MeasureResult 처리가 구현되어 있습니다.
interface MeasureScope : IntrinsicMeasureScope {
fun layout(
width: Int,
height: Int,
alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = layout(width, height, alignmentLines, null, placementBlock)
fun layout(
width: Int,
height: Int,
alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
rulers: (RulerScope.() -> Unit)? = null,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
checkMeasuredSize(width, height)
return object : MeasureResult {
...
override fun placeChildren() {
if (this@MeasureScope is LookaheadCapablePlaceable) {
placementScope.placementBlock()
} else {
SimplePlacementScope(width, layoutDirection).placementBlock()
}
}
}
}
}
LazyColumn/LazyRow에서는 MeasureScope 인터페이스를 구현한 LazyLayoutMeasureScopeImpl을 사용하여 measure를 처리합니다.
internal class LazyLayoutMeasureScopeImpl
internal constructor(
private val itemContentFactory: LazyLayoutItemContentFactory,
private val subcomposeMeasureScope: SubcomposeMeasureScope
) : LazyLayoutMeasureScope, MeasureScope by subcomposeMeasureScope {
private val itemProvider = itemContentFactory.itemProvider()
// 이전에 작성된 항목의 캐시이며, 이를 통해 동일한 measur 값 중에 동일한 index로 가져오기 재실행을 지원
private val placeablesCache = mutableIntObjectMapOf<List<Placeable>>()
override fun measure(index: Int, constraints: Constraints): List<Placeable> {
val cachedPlaceable = placeablesCache[index]
// index 기준 캐시된 Placeable 값을 재활용
return if (cachedPlaceable != null) {
cachedPlaceable
} else {
// 1. itemProvider로 부터 key/contentType 취득
val key = itemProvider.getKey(index)
// 2. @Composable () -> Unit 타입의 itemContent 취득
val contentType = itemProvider.getContentType(index)
val itemContent = itemContentFactory.getContent(index, key, contentType)
// 3. itemContent으로 subcomposition 실행. key는 slotId로 사용
// ※ subcompose 호출 시 사용자가 정의한 item DSL의 itemContent Composable이 호출 됨
val measurables = subcomposeMeasureScope.subcompose(key, itemContent)
// 4. measurables의 각 item을 measure한 후, placeablesCache에 캐싱
List(measurables.size) { i -> measurables[i].measure(constraints) }
.also { placeablesCache[index] = it }
}
}
...
}
MeasureResult는 measure된 레이아웃의 크기, 정렬선, 자식의 위치 지정 로직을 보유하는 인터페이스입니다.
현재 Compose에는 아래와 같은 MeasureResult 인터페이스를 구현한 클래스가 있습니다.
interface MeasureResult {
// 레이아웃의 측정된 넓이(픽셀 단위)
val width: Int
// 레이아웃의 측정된 높이(픽셀 단위)
val height: Int
// 부모가 이 레이아웃을 정렬하는데 사용할 수 있는 정렬선
val alignmentLines: Map<out AlignmentLine, Int>
// 자식 레이아웃용 Ruler를 만드는 데 사용되는 람다 함수
val rulers: (RulerScope.() -> Unit)?
get() = null
// 자식을 배치하는 데 사용
fun placeChildren()
}
여백 등의 로직을 제외하고서 rememberLazyListMeasurePolicy에서 살펴볼 첫 번째 로직은 Composable 함수에 전달된 파라미터 중 itemProviderLambda로 lambda 호출 시 LazyListItemProvider를 반환합니다. 이 파라미터는 LazyColumn/LazyRow에서는 LazyListItemProvider 인터페이스 구현체인 LazyListItemProviderImpl이 전달됩니다.
@Composable
private fun rememberLazyListMeasurePolicy(
itemProviderLambda: () -> LazyListItemProvider,
...
) =
remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
...
) {
{ containerConstraints ->
...
// 실제 itemProvider에는 LazyListItemProviderImpl이 사용됨
val itemProvider = itemProviderLambda()
...
}
}
두번째 로직은 itemProvider를 통해 만들어질 Composable의 measure를 담당할 Provider입니다. LazyListMeasuredItemProvider#createItem를 통해서 measure 처리된 item에 관련된 정보를 LazyListMeasuredItem 객체로 반환합니다.
@Composable
private fun rememberLazyListMeasurePolicy(
...
) =
remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
...
) {
{ containerConstraints ->
...
val itemProvider = itemProviderLambda()
...
val measuredItemProvider =
object :
LazyListMeasuredItemProvider(
contentConstraints,
isVertical,
itemProvider,
this
) {
override fun createItem(
index: Int,
key: Any,
contentType: Any?,
placeables: List<Placeable>,
constraints: Constraints
): LazyListMeasuredItem {
...
}
}
...
}
}
rememberLazyListMeasurePolicy ~ LazyListMeasuredItemProvider 소스 출처
LazyListMeasuredItemProvider는 측정 로직(measuring logic)에서 subcomposition을 추상화를 담당하는 클래스입니다. Measure 사용 시 아래의 흐름으로 진행됩니다.
internal abstract class LazyListMeasuredItemProvider(
constraints: Constraints,
isVertical: Boolean,
private val itemProvider: LazyListItemProvider,
private val measureScope: LazyLayoutMeasureScope
) : LazyLayoutMeasuredItemProvider<LazyListMeasuredItem> {
// 자식을 measure 처리할 제약 조건 설정
// isVertical(=LazyColumn) 여부에 따라서 최대 넓이/높이를 정의
val childConstraints =
Constraints(
maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
)
// lazy list의 item을 subcompose하는데 사용.
// 관련 제약 조건으로 measure한 후, placeables은 LazyListMeasuredItem으로 래핑됨
fun getAndMeasure(
index: Int,
constraints: Constraints = childConstraints
): LazyListMeasuredItem {
// itemProvider으로부터 key/contentType을 취득
val key = itemProvider.getKey(index)
val contentType = itemProvider.getContentType(index)
// LazyLayoutMeasureScopeImpl#measure 처리
// ※ LazyLayoutMeasureScopeImpl#measure 호출 시
// 사용자가 정의한 item DSL의 itemContent Composable이 호출 됨
val placeables = measureScope.measure(index, constraints)
// 추상 함수 createItem 호출하여 LazyListMeasuredItem를 반환하도록 위임
return createItem(index, key, contentType, placeables, constraints)
}
...
abstract fun createItem(
index: Int,
key: Any,
contentType: Any?,
placeables: List<Placeable>,
constraints: Constraints
): LazyListMeasuredItem
}
LazyLayoutMeasuredItemProvider 인터페이스는 Lazy Layout들의 MeasuredItemProvider 인터페이스로 각 레이아웃마다 별도로 구현체를 제공하고 있습니다. 본 블로그의 주제인 LazyList(=LazyColumn, LazyRow)에서는 LazyListMeasuredItemProvider가 사용됩니다.
internal interface LazyLayoutMeasuredItemProvider<T : LazyLayoutMeasuredItem> {
fun getAndMeasure(index: Int, lane: Int, span: Int, constraints: Constraints): T
}
다음은 요청한 item의 위치를 측정 및 계산하는 단계입니다. 이전 단계에 설명 LazyListMeasuredItemProvider 인스턴스를 measureLazyList 함수의 파라미터로 전달합니다. layout 파라미터는 layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
타입이며, measured된 레이아웃의 크기, alignment로 자식 item의 위치 지정 로직을 정의하는 블록(MeasureScope#layout)을 호출하고 있습니다.
measureLazyList의 처리 결과인 LazyListMeasureResult 인스턴스를 반환합니다. LazyListMeasureResult는 MeasureResult 인테페이스의 구현체이므로 rememberLazyListMeasurePolicy 함수의 결과로 전달할 수 있습니다.
@Composable
private fun rememberLazyListMeasurePolicy(
...
) =
remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
...
) {
{ containerConstraints ->
...
val itemProvider = itemProviderLambda()
...
val measuredItemProvider = object :
LazyListMeasuredItemProvider(...) { ... }
...
val measureResult =
measureLazyList(
...,
measuredItemProvider = measuredItemProvider,
layout = { width, height, placement ->
layout(
containerConstraints.constrainWidth(width + totalHorizontalPadding),
containerConstraints.constrainHeight(height + totalVerticalPadding),
emptyMap(),
placement
)
}
)
state.applyMeasureResult(measureResult, isLookingAhead)
measureResult
}
}
measureLazyList의 내부 구현 설명은 생략합니다.
이번 글의 마지막인 LazyLayout을 호출하는 부분입니다. LazyList Composable에서 계산된 itemProviderLambda, measurePolicy등의 값들을 통해서 LazyLayout Composable을 호출합니다.
@Composable
internal fun LazyList(
state: LazyListState,
reverseLayout: Boolean,
isVertical: Boolean,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
overscrollEffect: OverscrollEffect?,
...
) {
val itemProviderLambda = rememberLazyListItemProviderLambda(state, content)
val semanticState = rememberLazyListSemanticState(state, isVertical)
...
val measurePolicy = rememberLazyListMeasurePolicy(
itemProviderLambda,
...
)
val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
val beyondBoundsModifier = ...
LazyLayout(
modifier =
modifier
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
.lazyLayoutSemantics(
itemProviderLambda = itemProviderLambda,
state = semanticState,
orientation = orientation,
userScrollEnabled = userScrollEnabled,
reverseScrolling = reverseLayout,
)
.then(beyondBoundsModifier)
.then(state.itemAnimator.modifier)
.scrollingContainer(
state = state,
orientation = orientation,
enabled = userScrollEnabled,
reverseScrolling = reverseLayout,
flingBehavior = flingBehavior,
interactionSource = state.internalInteractionSource,
useLocalOverscrollFactory = false,
overscrollEffect = overscrollEffect
),
prefetchState = state.prefetchState, // item을 미리 가져오도록 예약에 대한 상태
measurePolicy = measurePolicy, // 필요한 item만 compose/measure할 수 있는 measure 정책
itemProvider = itemProviderLambda // Lambda를 사용하여 measurePolicy의 일부로 item을 compose/measure하는 데 사용할 수 있는 item에 대한 모든 필요한 정보를 포함하는 item provider를 생성
)
}
comments powered by Disqus
Subscribe to this blog via RSS.
Jetpack Compose: LazyColumn/LazyRow 내부 코드 분석 ~ 2부 LazyList (2) rememberLazyListMeasurePolicy
Posted on 09 Feb 2025Jetpack Compose: LazyColumn/LazyRow 내부 코드 분석 ~ 2부 LazyList (1)
Posted on 25 Jan 2025Jetpack Compose: LazyColumn/LazyRow 내부 코드 분석 ~ 1부 LazyColumn/LazyRow
Posted on 10 Jan 2025