Upgrade to PRO for Only $50/Yearโ€”Limited-Time Offer! ๐Ÿ”ฅ

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. ์ƒํƒœ ํ˜ธ์ด์ŠคํŒ… ์ƒํƒœ๋ฅผ ์ปดํฌ์ €๋ธ”์˜ ํ˜ธ์ถœ์ž๋กœ ์ด๋™ํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ํŒจํ„ด ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ

    ํ๋ฆ„์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Œ @Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } 6
  4. Compose ์—์„œ ์ƒํƒœ ๋ณต์› rememberSaveable์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ์œ ์ง€ ๋ฆฌ์ปดํฌ์ง€์…˜๋ฟ๋งŒ ์•„๋‹ˆ๋ผ

    ํ™œ๋™ ์žฌ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ ํ›„์—๋„ ์ƒํƒœ ๋ณด์กด ๊ฐ€๋Šฅ var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Sa mutableStateOf(TextFieldValue(text = typedQuery, selection = TextRange(typedQ } 7
  5. ์ƒํƒœ ํ™€๋” ์ƒํƒœ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์ƒํƒœ ํ™€๋” ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ด€๋ฆฌ UI

    ๋กœ์ง๊ณผ ์ƒํƒœ ๋กœ์ง์„ ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ @Composable private fun rememberMyAppState(windowSizeClass: WindowSizeClass): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } 8
  6. ์ƒํƒœ๋ฅผ ํ˜ธ์ด์ŠคํŒ…ํ•  ๋Œ€์ƒ ์œ„์น˜ UI ์ƒํƒœ๋Š” UI ๋กœ์ง๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง

    ์ค‘ ์–ด๋А ์ชฝ์—์„œ ํ•„์š”ํ•œ์ง€์— ๋”ฐ๋ผ ํ˜ธ์ด์ŠคํŒ… ์œ„์น˜ ๊ฒฐ์ • UI ์ƒํƒœ๋ฅผ ์ฝ๊ณ  ์“ฐ๋Š” ๋ชจ๋“  ์ปดํฌ์ €๋ธ”์˜ ๊ฐ€์žฅ ๋‚ฎ์€ ๊ณตํ†ต ์ƒ์œ„ ์š”์†Œ๋กœ ํ˜ธ์ด์ŠคํŒ…ํ•ด์•ผ ํ•จ ์ƒํƒœ ์†Œ์œ ์ž๋กœ๋ถ€ํ„ฐ ์†Œ๋น„์ž์—๊ฒŒ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ๋ฐ ์ด๋ฒคํŠธ๋ฅผ ๋…ธ์ถœํ•˜์—ฌ ์ƒํƒœ ์ˆ˜์ • 11
  7. UI ์ƒํƒœ ๋ฐ ๋กœ์ง UI ์ƒํƒœ: UI๋ฅผ ์„ค๋ช…ํ•˜๋Š” ์†์„ฑ ํ™”๋ฉด

    UI ์ƒํƒœ: UI๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ํฌํ•จ UI ์š”์†Œ ์ƒํƒœ: UI ์š”์†Œ ์ž์ฒด์˜ ์ƒํƒœ (์˜ˆ: ๋ฒ„ํŠผ ํด๋ฆญ ์—ฌ๋ถ€) ๋กœ์ง ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง: ์•ฑ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ œํ’ˆ ์š”๊ตฌ์‚ฌํ•ญ ๊ตฌํ˜„ UI ๋กœ์ง: UI ์ƒํƒœ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ด€๋ จ๋จ 12
  8. 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
  9. ์ƒํƒœ๋ฅผ 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
  10. 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
  11. 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
  12. ์ •์ง€ ํ•จ์ˆ˜์™€ ์ƒํƒœ ๊ด€๋ฆฌ Compose UI ์š”์†Œ ์ƒํƒœ์˜ ์ผ๋ถ€ ์ •์ง€

    ํ•จ์ˆ˜๋Š” CoroutineScope๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ง€์ •ํ•ด์•ผ ํ•จ viewModelScope์—์„œ ์‹คํ–‰ํ•˜๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ ๊ฐ€๋Šฅ โ†’ rememberCoroutineScope() ์‚ฌ์šฉ fun closeDrawer(uiScope: CoroutineScope) { viewModelScope.launch { withContext(uiScope.coroutineContext) { drawerState.close() } } } 17
  13. 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
  14. LazyListState ์ €์žฅ rememberSaveable์„ ์‚ฌ์šฉํ•˜์—ฌ LazyListState ์œ ์ง€ ์Šคํฌ๋กค ์œ„์น˜ ๋“ฑ์˜ ์ƒํƒœ๋ฅผ

    ์ €์žฅํ•˜๊ณ  ๋ณต์› ๊ฐ€๋Šฅ @Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScroll } } 21
  15. 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
  16. 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
  17. UI ์ƒํƒœ ์ €์žฅ ์š”์•ฝ ์ด๋ฒคํŠธ UI ๋กœ์ง ViewModel ์˜ ๋น„์ฆˆ๋‹ˆ์Šค

    ๋กœ์ง ๊ตฌ์„ฑ ๋ณ€๊ฒฝ rememberSaveable ์ž๋™ ์‹œ์Šคํ…œ์—์„œ ์‹œ์ž‘๋œ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ rememberSaveable SavedStateHandle 24
  18. Compose ์˜ UI ๊ฐœ๋… Compose์˜ UI๋Š” ๋ถˆ๋ณ€(Immutable)ํ•˜๋ฉฐ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜

    ์—†์Œ UI ์ƒํƒœ๋ฅผ ์ œ์–ดํ•˜์—ฌ UI๋ฅผ ๋ณ€๊ฒฝ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด Compose๋Š” ๋ณ€๊ฒฝ๋œ UI ํŠธ๋ฆฌ ๋ถ€๋ถ„์„ ๋‹ค์‹œ ์ƒ์„ฑ ์ปดํฌ์ €๋ธ”์€ ์ƒํƒœ๋ฅผ ์ˆ˜๋ฝํ•˜๊ณ  ์ด๋ฒคํŠธ๋ฅผ ๋…ธ์ถœ ๊ฐ€๋Šฅ var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) 25
  19. Jetpack Compose ์˜ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ TextField์˜ value ๋งค๊ฐœ๋ณ€์ˆ˜์™€ onValueChange

    ์ฝœ๋ฐฑ ํ™œ์šฉ State ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ๋ฆฌ์ปดํฌ์ง€์…˜ ํŠธ๋ฆฌ๊ฑฐ remember์™€ rememberSaveable์„ ์‚ฌ์šฉํ•ด ์ƒํƒœ ์œ ์ง€ ๊ฐ€๋Šฅ 28
  20. ViewModel ๊ณผ Compose ์˜ ์ƒํƒœ ๊ด€๋ฆฌ ViewModel์„ ํ™œ์šฉํ•˜์—ฌ ์ƒํƒœ ๊ด€๋ฆฌ

    ๋ฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ mutableStateOf๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ UI ์ƒํƒœ๋ฅผ ์ €์žฅ class MyViewModel : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.SignedOut) val uiState: State<UiState> get() = _uiState } 29
  21. 30

  22. CompositionLocal ์†Œ๊ฐœ ์ผ๋ฐ˜์ ์œผ๋กœ Compose์—์„œ ๋ฐ์ดํ„ฐ๋Š” UI ํŠธ๋ฆฌ๋ฅผ ๋”ฐ๋ผ ์•„๋ž˜๋กœ ํ๋ฆ„

    ๊ทธ๋Ÿฌ๋‚˜ ์ผ๋ถ€ ๋ฐ์ดํ„ฐ(์˜ˆ: ํ…Œ๋งˆ, ์–ธ์–ด ์„ค์ • ๋“ฑ)๋Š” ๋งค๋ฒˆ ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌํ•˜๊ธฐ ๋ฒˆ๊ฑฐ๋กœ์›€ ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด CompositionLocal์„ ์‚ฌ์šฉ 33
  23. CompositionLocal ์˜ ๋™์ž‘ ๋ฐฉ์‹ โ€ข CompositionLocal ์ธ์Šคํ„ด์Šค๋Š” UI ํŠธ๋ฆฌ์—์„œ ๊ฐ€์žฅ

    ๊ฐ€๊นŒ์šด ์ œ๊ณต๋œ ๊ฐ’ ์‚ฌ์šฉ โ€ข CompositionLocalProvider๋ฅผ ์‚ฌ์šฉํ•ด ํŠน์ • UI ๊ณ„์ธต์—์„œ ๋‹ค๋ฅธ ๊ฐ’ ์ œ๊ณต ๊ฐ€๋Šฅ โ€ข ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด ํ•ด๋‹น ๊ฐ’์„ ์ฝ๋Š” ๋ถ€๋ถ„๋งŒ ๋‹ค์‹œ ๊ตฌ์„ฑ๋จ 35
  24. CompositionLocal ์˜ ๋‹จ์  ์•”์‹œ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋ฏ€๋กœ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Œ

    ์ข…์†์„ฑ์ด ๋ช…ํ™•ํ•˜์ง€ ์•Š์•„ ๋””๋ฒ„๊น…์ด ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Œ ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์Œ 38
  25. CompositionLocal ์‚ฌ์šฉ ์—ฌ๋ถ€ ๊ฒฐ์ • โœ… ์‚ฌ์šฉํ•  ๋•Œ ์•ฑ์˜ ํ…Œ๋งˆ, ์–ธ์–ด

    ์„ค์ • ๋“ฑ ํฌ๋กœ์Šค ์ปคํŒ…ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ๋•Œ โŒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ ํŠน์ • UI ํŠธ๋ฆฌ์—์„œ๋งŒ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ๋•Œ ViewModel๊ณผ ๊ฐ™์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ „๋‹ฌํ•  ๋•Œ 39
  26. ๊ณ ๋ คํ•  ๋Œ€์•ˆ 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