성장기록지
State hoisting 탐구 (재사용성, plain state holder class) 본문
컴포저블 내부에서 호이스팅
그림과 같은 예시를 보자.
전체 화면을 ConverSationScreen이라고 하고,
메시지가 보이는 목록들이 MessagesList,
Jump to Bottom이라고 작성되어있는 버튼을 Button,
Hi there! 이라고 적혀있는 입력창은 userInput을 의미한다.
그렇다면 요구사항은 다음과 같을 것이다.
"Jump to bottom을 누르면 MessagesList 가장 아래로 가게 해줘"
"새로운 메시지를 send해도 가장 아래로 가게 해줘"
그렇다면 계층구조는 어떻게 되어있을까?
그림과 같이 연결되어 있을 것이다.
여기서 우리는 State hoisting을 이용해 재사용성을 높이고 싶다는 생각이 들 것이다.
그렇다면 어느 State를 어디까지 호이스팅 해야할까?
공식문서에서 아래와 같이 친절하게 GIF 로 설명이 되어있다.
LazyColumn의 스크롤을 조작해야 하기 때문에, UserInput과 Button 모두 LazyListState가 필요할 것이다.
그렇다면 이 모든 컴포저블 함수의 가장 가까운 공통 조상(The lowest common ancestor)
까지 hoisting을 하면 될 것이다!
요약하자면 아래의 그림과 같이 될 것이다.
이렇게 UI 요구 사항에 따라 Lowest common ancestor까지 state hoisting을 하는 건
꽤나 자주 있는 일이므로 가까운 공통 조상을 찾는것을 명심하면 좋을것이다!
아래는 설명에 대한 전체 코드이다.
@Composable
private fun ConversationScreen(/*...*/) {
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen
MessagesList(messages, lazyListState) // Reuse same state in MessageList
UserInput(
onMessageSent = { // Apply UI logic to lazyListState
scope.launch {
lazyListState.scrollToItem(0)
}
},
)
}
@Composable
private fun MessagesList(
messages: List<Message>,
lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {
LazyColumn(
state = lazyListState // Pass hoisted state to LazyColumn
) {
items(messages, key = { message -> message.id }) { item ->
Message(/*...*/)
}
}
val scope = rememberCoroutineScope()
JumpToBottom(onClicked = {
scope.launch {
lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
}
})
}
아래는 LazyListState와 같은 plain state holder class를 설명해보겠다.
Plain state holder class를 State Owner로 사용하기
공식문서에서는, "컴포저블 함수에서 복잡한 UI 로직을 다룰 때 그 로직을 plain state holder class와 같은
상태 홀더 클래스에 위임하라" 고 되어있다.
이렇게 하면 테스트 용이, 복잡성 감소, 책임 분리 원칙 준수와 같은 효과가 있고,
컴포저블 함수 호출자에게 편리한 함수를 제공하여, 호출자가 이 로직을 직접 작성할 필요가 없게 만든다고 한다.
plain state holder class의 예시는 방금 언급한 LazyListState가 있고,
rememberLazyState()와 같은 편리한 함수를 제공한다. (컴포저블 생명주기를 따르므로 가능하다.)
(참고, rememberLazyState는 remember가 아닌 rememberSavable을 사용한다.)
그렇다면 LazyListState는 어떠한 복잡한 UI 로직을 다루는 걸까? 코드를 살펴보자.
LazyListState는 이 UI 요소의 scrollPosition을 저장하는 LazyColumn의 상태를 캡슐화한다.
또한 특정 항목으로 스크롤하는 등의 방식으로 스크롤 위치를 수정하는 메서드도 노출한다.
이렇게 컴포저블의 책임을 늘리면 상태 홀더의 필요성이 증가하게 된다!
그렇다면 기존에 있는 State holder class만 사용하는가? 당연히 아니다.
필요에 따라서 프로젝트 참여자들과의 조율로 plain state holder class를 커스텀하면 된다!!
@Stable
class LazyListState constructor(
firstVisibleItemIndex: Int = 0,
firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
/**
* The holder class for the current scroll position.
*/
private val scrollPosition = LazyListScrollPosition(
firstVisibleItemIndex, firstVisibleItemScrollOffset
)
suspend fun scrollToItem(/*...*/) { /*...*/ }
override suspend fun scroll() { /*...*/ }
suspend fun animateScrollToItem() { /*...*/ }
}
참고 자료
https://developer.android.com/develop/ui/compose/state-hoisting?hl=ko
https://nanamare.tistory.com/251
'안드로이드 > 안드로이드 지식' 카테고리의 다른 글
안드로이드 MVVM 아키텍쳐란? (0) | 2025.01.13 |
---|---|
data binding이란? (0) | 2025.01.11 |
Compose State와 State hoisting (0) | 2025.01.08 |
Android의 Clean Architecture란? (0) | 2025.01.07 |
android 권장 아키텍쳐란? (0) | 2025.01.02 |