동기
Jetpack Compose 를 활용하여 UI 를 개발하다 보면, Composable 의 테스트 용이성과 재사용성을 높이기 위해 StateHoisting 을 적용하곤 합니다.
제 경우, StateHoisting 을 엄격하게 적용하려다 보니 수많은 람다 파라미터로 인해 Composable 메서드를 호출하는 부분이 과도하게 길어지고, 드릴링이 발생하는 문제가 있었습니다.
저는 이를 CompositionLocal 을 통해 해결하였고, 오늘은 이에 대해 간략하게 작성해보려 합니다.
Stability
불필요한 Recomposition 의 발생을 최대한 줄이기 위해 이벤트 처리와 관련된 로직을 Stable 하게 만들어야 합니다. 이에 관한 내용은 아래에서 확인하실 수 있습니다.
val onEvent = remember { { event: UiEvent -> uiEvent(event) } }
저는 remember 를 이용하여 이와 같이 작성하였습니다. remember 스코프 내부를 이벤트 처리 로직으로 구성합니다. 이를 통해 uiEvent 라는 이름의 람다 변수를 Stable 하게 만들 수 있습니다.
CompositionLocal
이벤트 처리 메커니즘의 핵심인 CompositionLocal 에 대한 설명은 위 포스트에 자세히 작성해두었습니다.
CompositionLocal 에 적용된 값은 UI Tree 를 타고 아래로 흐릅니다. 그 값은 Group 이라는 스코프에 일괄적으로 적용이 되는데, Group 에 값을 적용하는 방법과 그 값에 접근하기 위한 방법에 대해 간단히 소개합니다.
CompositionLocal 을 통해 데이터 제공하기
먼저, 적용하고자 하는 CompositionLocal 의 기본값을 설정해야 합니다. 제공하고자 하는 타입에 따라 달라지게 됩니다. 이 경우에는 이벤트를 처리하기로 하였으므로, 이벤트 타입의 객체를 수신하는 람다를 작성하면 됩니다.
private val LocalUiEvent = compositionLocalOf { { _: UiEvent -> } }
이와 같이 작성하면, LocalUiEvent 라는 이름의 CompositionLocal 에 지정될 데이터에 대한 기본값이 설정됩니다. 즉, compositionLocalOf 의 스코프에 어떠한 타입의 데이터가 적용될지를 결정합니다.
이후, 이를 실제 UI Tree 에 적용하기 위해 CompositionLocalProvider() 메서드를 사용합니다. Slot API 를 통해 생성되는 스코프 내에서 Composable 메서드를 호출하면 해당 Composable 이 속한 Group 에 제공되는 값이 적용됩니다. 이후에는 각 Composable 메서드 내에서 CompositionLocal 에 접근하여 값을 획득하여 사용할 수 있는 구조입니다.
CompositionLocalProvider(LocalUiEvent provides onEvent) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.background)
) {
...
}
}
적용하고자 하는 CompositionLocal 이 CompositionLocal 을 키로 받아 값을 저장하는 Map<K, T> 에 저장될 수 있도록 하기 위해 CompositionLocalProvider 를 사용합니다. 즉, 해당 스코프 내에서 LocalUiEvent 를 참조하면 onEvent 값을 획득할 수 있습니다.
이후 생성되는 스코프 내에 Composable 메서드를 호출하면 사용을 위한 준비는 끝이 납니다.
CompositionLocal 이 제공하는 값 획득하기
Jetpack Compose 를 통해 UI 를 개발해보셨다면, 여기부턴 매우 간단합니다. 단순히 CompositionLocal 이 제공하는 값을 호출하여 사용하면 됩니다.
@Composable
private fun TopSection(displayCount: Int) {
val localUiEvent = LocalUiEvent.current
TopBar(
onClickProfileIcon = { /*TODO*/ },
middleContents = {
Row(verticalAlignment = CenterVertically) {
Icon(
modifier = Modifier.clickableWithoutRipple { localUiEvent(OnClickReset) },
painter = painterResource(id = R.drawable.ic_reset),
tint = AddedIngredientDescColor,
contentDescription = null
)
...
}
}
)
}
Modifier.clickable 에 직접 이와 같이 작성하여 사용하면 되기 때문에, 특수한 이유가 있지 않은 이상 람다 파라미터를 Composable 메서드에 작성할 필요가 없어집니다. 즉, 수많은 드릴링이 발생하는 상황에서, 이벤트 처리를 위한 람다 파라미터 작성을 회피할 수 있습니다.
추가로, 불필요한 Recomposition 이 발생하지는 않는지 확인하기 위해, 이벤트 처리 람다 변수의 해시 코드를 여러 곳에서 출력해보았고, 다음과 같은 결과를 얻을 수 있었습니다.
remembered onEvent hashCode : 266084374
Screen Level LocalComposition value hashCode : 266084374
Composable Level LocalComposition value hashCode : 266084374
이와 같이 작성하여도 Preview 등의 기능을 이용하는 데에는 아무런 문제가 없으므로, 조금은 더 깔끔하게 Jetpack Compose 를 활용한 UI 코드 작성이 가능합니다.
Jetpack Compose 를 통해 UI 를 작성하던 도중, 복잡한 UI 구성으로 인해 깊어지는 프로퍼티 드릴링을 회피하기 위해 사용하게 되었던 방식입니다. 현재로써는 별다른 문제를 야기하지 않는 것 같습니다만, UI 테스트를 작성하게 된다면 또 어떨지 모르겠네요.
'Android > Tech' 카테고리의 다른 글
[Jetpack Compose] 불필요한 Recomposition 을 줄여 앱 퍼포먼스 개선하기 (0) | 2024.02.25 |
---|---|
[Jetpack Compose] HiltViewModel() 을 통해 주입된 ViewModel 의 생애 알아보기 (0) | 2024.02.11 |
OkHttp3 Interceptor 를 통해 표준화된 응답의 에러 처리하기 (0) | 2024.02.01 |
ParentFragmentManager, ChildFragmentManager (0) | 2024.01.21 |
Composable 파라미터 주의 사항 (feat.Recomposition) (0) | 2024.01.17 |