Upgrade to Pro — share decks privately, control downloads, hide ads and more …

W02 Jetpack Compose - State & CompositionLocal

Avatar for Eunice Eunice
February 23, 2025

W02 Jetpack Compose - State & CompositionLocal

Avatar for Eunice

Eunice

February 23, 2025
Tweet

More Decks by Eunice

Other Decks in Education

Transcript

  1. 상태란? 앱의 상태는 시간이 지남에 따라 변할 수 있는 값

    Room 데이터베이스부터 클래스 변수까지 포함 모든 Android 앱에서는 상태가 사용자에게 표시됨 3
  2. 상태 및 구성 Compose는 선언적 방식으로 UI 업데이트 상태 업데이트

    시 재구성이 실행됨 @Composable private fun HelloContent() { Column( modifier = Modifier .padding(16.dp) ) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } } 4
  3. 컴포저블의 상태 remember API를 사용하여 메모리에 객체 저장 mutableStateOf를 통해

    관찰 가능한 MutableState<T> 생성 var name by remember { mutableStateOf("") } 5
  4. 상태 호이스팅 상태를 컴포저블의 호출자로 이동하여 관리하는 패턴 단방향 데이터

    흐름을 유지할 수 있음 @Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } 6
  5. Compose 에서 상태 복원 rememberSaveable을 사용하여 상태를 유지 리컴포지션뿐만 아니라

    활동 재생성 및 프로세스 종료 후에도 상태 보존 가능 var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Sa mutableStateOf(TextFieldValue(text = typedQuery, selection = TextRange(typedQ } 7
  6. 상태 홀더 상태가 많아지면 상태 홀더 클래스를 사용하여 관리 UI

    로직과 상태 로직을 분리 가능 @Composable private fun rememberMyAppState(windowSizeClass: WindowSizeClass): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } 8
  7. 상태를 호이스팅할 대상 위치 UI 상태는 UI 로직과 비즈니스 로직

    중 어느 쪽에서 필요한지에 따라 호이스팅 위치 결정 UI 상태를 읽고 쓰는 모든 컴포저블의 가장 낮은 공통 상위 요소로 호이스팅해야 함 상태 소유자로부터 소비자에게 변경 불가능한 상태 및 이벤트를 노출하여 상태 수정 11
  8. UI 상태 및 로직 UI 상태: UI를 설명하는 속성 화면

    UI 상태: UI를 렌더링하는 데 필요한 데이터 포함 UI 요소 상태: UI 요소 자체의 상태 (예: 버튼 클릭 여부) 로직 비즈니스 로직: 앱 데이터에 대한 제품 요구사항 구현 UI 로직: UI 상태를 표시하는 방법과 관련됨 12
  9. UI 로직에서의 상태 호이스팅 UI 상태를 읽거나 써야 하는 경우

    UI 수명 주기에 따라 상태 범위 지정 필요 상태 소유자로서의 컴포저블 상태와 로직이 단순하면 컴포저블에 UI 로직과 UI 요소 상태 포함 가능 단순한 상태는 내부 유지 가능 (예: 애니메이션 상태) @Composable fun ChatBubble(message: Message) { var showDetails by rememberSaveable { mutableStateOf(false) } ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } ) if (showDetails) { Text(message.timestamp) } } 13
  10. 상태를 UI 계층 구조의 상위로 호이스팅하기 여러 위치에서 상태를 공유하고

    UI 로직을 적용해야 할 경우 상위 컴포저블로 호이스팅 LazyColumn 상태를 ConversationScreen으로 호이스팅하여 UI 로직 적용 @Composable private fun ConversationScreen() { val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() MessagesList(messages, lazyListState) UserInput( onMessageSent = { scope.launch { lazyListState.scrollToItem(0) } } ) } 14
  11. ViewModel 을 이용한 상태 관리 비즈니스 로직이 관련된 경우 ViewModel을

    통해 상태를 관리하는 것이 적절함 ViewModel은 컴포지션 외부에 저장되며, UI 상태의 가장 낮은 공통 상위 요소 역할을 수행 class ConversationViewModel(channelId: String, messagesRepository: MessagesReposi val messages = messagesRepository.getLatestMessages(channelId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList fun sendMessage(message: Message) { /* ... */ } } 15
  12. UI 요소 상태와 비즈니스 로직 UI 요소 상태를 읽거나 써야

    하는 비즈니스 로직이 있다면 ViewModel에서 상태 관리 사용자의 입력에 따라 UI 상태를 변경하는 예시: class ConversationViewModel : ViewModel() { var inputMessage by mutableStateOf("") private set val suggestions: StateFlow<List<Suggestion>> = snapshotFlow { inputMessage } .filter { hasSocialHandleHint(it) } .mapLatest { getHandle(it) } .mapLatest { repository.getSuggestions(it) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), empty fun updateInput(newInput: String) { inputMessage = newInput } } 16
  13. 정지 함수와 상태 관리 Compose UI 요소 상태의 일부 정지

    함수는 CoroutineScope를 올바르게 지정해야 함 viewModelScope에서 실행하면 예외 발생 가능 → rememberCoroutineScope() 사용 fun closeDrawer(uiScope: CoroutineScope) { viewModelScope.launch { withContext(uiScope.coroutineContext) { drawerState.close() } } } 17
  14. UI 로직에서 상태 저장 rememberSaveable을 사용하여 상태 유지 구성 변경

    및 프로세스 종료 후에도 UI 요소 상태 복원 가능 @Composable fun ChatBubble(message: Message) { var showDetails by rememberSaveable { mutableStateOf(false) } ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } ) if (showDetails) { Text(message.timestamp) } } 20
  15. LazyListState 저장 rememberSaveable을 사용하여 LazyListState 유지 스크롤 위치 등의 상태를

    저장하고 복원 가능 @Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScroll } } 21
  16. ViewModel 을 이용한 상태 저장 ViewModel을 사용하면 구성 변경 후에도

    상태 유지 SavedStateHandle을 활용하여 시스템 종료 후에도 상태 복원 가능 class ConversationViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } private set fun update(newMessage: TextFieldValue) { message = newMessage } } 22
  17. StateFlow 를 사용한 상태 저장 getStateFlow()를 사용하여 상태 저장 StateFlow를

    활용하여 지속적으로 UI 상태 업데이트 가능 private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey" class ChannelViewModel( channelsRepository: ChannelsRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow( key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS ) fun setFiltering(requestType: ChannelsFilterType) { savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType } } 23
  18. UI 상태 저장 요약 이벤트 UI 로직 ViewModel 의 비즈니스

    로직 구성 변경 rememberSaveable 자동 시스템에서 시작된 프로세스 종료 rememberSaveable SavedStateHandle 24
  19. Compose 의 UI 개념 Compose의 UI는 불변(Immutable)하며 직접 업데이트할 수

    없음 UI 상태를 제어하여 UI를 변경 상태가 변경되면 Compose는 변경된 UI 트리 부분을 다시 생성 컴포저블은 상태를 수락하고 이벤트를 노출 가능 var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) 25
  20. Jetpack Compose 의 단방향 데이터 흐름 TextField의 value 매개변수와 onValueChange

    콜백 활용 State 객체를 사용해 상태 변경 시 리컴포지션 트리거 remember와 rememberSaveable을 사용해 상태 유지 가능 28
  21. ViewModel 과 Compose 의 상태 관리 ViewModel을 활용하여 상태 관리

    및 이벤트 처리 mutableStateOf를 사용하여 UI 상태를 저장 class MyViewModel : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.SignedOut) val uiState: State<UiState> get() = _uiState } 29
  22. 30

  23. CompositionLocal 소개 일반적으로 Compose에서 데이터는 UI 트리를 따라 아래로 흐름

    그러나 일부 데이터(예: 테마, 언어 설정 등)는 매번 명시적으로 전달하기 번거로움 이를 해결하기 위해 CompositionLocal을 사용 33
  24. CompositionLocal 예제 @Composable fun SomeTextLabel(labelText: String) { Text( text =

    labelText, color = MaterialTheme.colorScheme.primary ) } 34
  25. CompositionLocal 의 동작 방식 • CompositionLocal 인스턴스는 UI 트리에서 가장

    가까운 제공된 값 사용 • CompositionLocalProvider를 사용해 특정 UI 계층에서 다른 값 제공 가능 • 값을 변경하면 해당 값을 읽는 부분만 다시 구성됨 35
  26. CompositionLocal 의 단점 암시적으로 데이터를 전달하므로 이해하기 어려울 수 있음

    종속성이 명확하지 않아 디버깅이 어려울 수 있음 과도하게 사용하면 코드 가독성이 떨어질 수 있음 38
  27. CompositionLocal 사용 여부 결정 ✅ 사용할 때 앱의 테마, 언어

    설정 등 크로스 커팅한 데이터를 전달할 때 ❌ 사용하지 않을 때 특정 UI 트리에서만 필요한 데이터를 전달할 때 ViewModel과 같은 비즈니스 로직을 전달할 때 39
  28. 고려할 대안 1. 명시적 매개변수 전달 2. 컨트롤 역전(Inversion of

    Control) @Composable fun MyDescendant(data: DataToDisplay) { Text(text = data.title) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } } 40