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

안드로이드 UI 상태 저장 권장사항

Pangmoo
August 24, 2023

안드로이드 UI 상태 저장 권장사항

Pangmoo

August 24, 2023
Tweet

More Decks by Pangmoo

Other Decks in Programming

Transcript

  1. 상상하는 것을 소프트웨어로 구현하는 것을 좋아하는 팡무 GDG Songdo Organizer

    GDSC TUK Lead 전 아우토크립트 안드로이드 개발 팀장 Who Am I? Section 00 @kisa002 @kisa002 @holykisa
  2. Section 00 UI 상태 데이터 사용자 입력 데이터 화면 상태

    진행도 …. 백그라운드 상태 리사이즈 예외 케이스 화면 회전 테마 변경 시스템 리소스 부족
  3. Section 01 구성요소 변경 • 앱 디스플레이 크기 • 화면

    방향 • 글꼴 크기 및 두께 • 언어 • 다크 테마 / 라이트 테마 • 키보드 사용 가능 여부 • And then more… bit.ly/runtime changes
  4. Section 02 ViewModel 메모리 저장 제한된 메모리 구성 변경에서의 생존

    백스택 있는 내비게이션 캐시 UI 상태 빠른 읽고 쓰기
  5. sealed class ProfileUIState { object Loading : ProfileUIState() data class

    Error(val throwable: Throwable? = null) : ProfileUIState() data class Success(val profile: Profile) : ProfileUIState() }
  6. class ProfileViewModel(userRepository: UserRepository) : ViewModel() { val uiState = flow

    { emit(ProfileUIState.Success(userRepository.fetchProfile())) }.catch { ProfileUIState.Error(it) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = ProfileUIState.Loading ) }
  7. class ProfileViewModel(userRepository: UserRepository) : ViewModel() { val uiState = flow

    { emit(ProfileUIState.Success(userRepository.fetchProfile())) }.catch { ProfileUIState.Error(it) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = ProfileUIState.Loading ) }
  8. class ProfileViewModel(userRepository: UserRepository) : ViewModel() { val uiState = flow

    { emit(ProfileUIState.Success(userRepository.fetchProfile())) }.catch { ProfileUIState.Error(it) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = ProfileUIState.Loading ) }
  9. class ProfileViewModel(userRepository: UserRepository) : ViewModel() { val uiState = flow

    { emit(ProfileUIState.Success(userRepository.fetchProfile())) }.catch { ProfileUIState.Error(it) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = ProfileUIState.Loading ) }
  10. Section 02 영구 저장 소규모 단순 데이터 적합 소규모 단순

    데이터 적합 비동기 처리 SharedPreferences DataStore 복잡한 데이터 처리 참조 무결성 Room
  11. Section 02 영구 저장 디스크 저장 제한된 디스크 공간 구성

    변경, 시스템 리소스 부족, 예외 케이스에서의 생존 느린 읽고 쓰기 애플리케이션 데이터
  12. Section 02 상태 저장 API 메모리 저장 제한된 번들 구성

    변경, 시스템 리소스 부족 생존 느린 읽고 쓰기 큰 정보와 리스트 내비게이션, 사용자 입력에 의존하는 일시적인 상태
  13. Section 02 상태 저장 API 느린 읽고 쓰기 큰 정보와

    리스트 내비게이션, 사용자 입력에 의존하는 일시적인 상태 리스트의 스크롤 위치 상세화면의 아이템 ID 사용자 설정 진행 상태
  14. @Composable private fun FaqItem(title: String, content: String) { var visibleContent

    by remember { mutableStateOf(false) } Column { FaqItemTitle( title = title, visibleContent = visibleContent, onChangeVisibleRequest = { visibleContent = it } ) FaqItemContent( content = content, visibleContent = visibleContent ) Divider() } }
  15. @Composable private fun FaqItem(title: String, content: String) { var visibleContent

    by rememberSaveable { mutableStateOf(false) } Column { FaqItemTitle( title = title, visibleContent = visibleContent, onChangeVisibleRequest = { visibleContent = it } ) FaqItemContent( content = content, visibleContent = visibleContent ) Divider() } }
  16. class FaqItemView @JvmOverloads constructor(context: Context, ...) : View(context, ...) {

    private var isExpanded = false override fun onSaveInstanceState(): Parcelable { super.onSaveInstanceState() return bundleOf(IS_EXPANDED to isExpanded) } override fun onRestoreInstanceState(state: Parcelable) { if (state is Bundle) { isExpanded = state.getBoolean(IS_EXPANDED) } super.onRestoreInstanceState(state) } companion object { private const val IS_EXPANDED = "is_expanded" } } View의 ID 지정되야함
  17. class FaqItemTests { @get:Rule val composeTestRule = createComposeRule() @Test fun

    onRecreation_stateIsRestored() { val restorationTester = StateRestorationTester(composeTestRule) val (title, content) = "This is title" to "This is content" restorationTester.setContent { FaqItem(title = title, content = content) } composeTestRule.onNodeWithText(title).performClick() composeTestRule.onNodeWithText(content).assertIsDisplayed() restorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithText(content).assertIsDisplayed() } }
  18. class KeywordViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() {

    var keyword by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } private set fun updateKeyword(newKeyword: TextFieldValue) { keyword = newKeyword } } SavedStateHandle은 액티비티가 Stop 될 때 저장됨
  19. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String, )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set }
  20. @Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )

    = rememberSaveable( articleRepository, initialKeyword, saver = // TODO: Custom Saver ) { SearchKeywordState(articleRepository, initialKeyword) }
  21. @Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )

    = rememberSaveable( articleRepository, initialKeyword, saver = // TODO: Custom Saver ) { SearchKeywordState(articleRepository, initialKeyword) }
  22. @Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )

    = rememberSaveable( articleRepository, initialKeyword, saver = // TODO: Custom Saver ) { SearchKeywordState(articleRepository, initialKeyword) }
  23. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set }
  24. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  25. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  26. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  27. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  28. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  29. @Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )

    = rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(articleRepository) ) { SearchKeywordState(articleRepository, initialKeyword) }
  30. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String

    ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set }
  31. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String

    ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set override fun saveState(): Bundle = bundleOf(KEYWORD to keyword) companion object { private const val KEYWORD = "keyword" private const val PROVIDER = "search_keyword_state" } }
  32. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,

    private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { } }
  33. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,

    private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = registryOwner.savedStateRegistry if (registry.getSavedStateProvider(PROVIDER) == null) { registry.registerSavedStateProvider(PROVIDER, this) } val savedState = registry.consumeRestoredStateForKey(PROVIDER) keyword = savedState?.getString(KEYWORD) ?: initialKeyword } }) } }
  34. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,

    private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = registryOwner.savedStateRegistry if (registry.getSavedStateProvider(PROVIDER) == null) { registry.registerSavedStateProvider(PROVIDER, this) } val savedState = registry.consumeRestoredStateForKey(PROVIDER) keyword = savedState?.getString(KEYWORD) ?: initialKeyword } }) } }
  35. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,

    private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = registryOwner.savedStateRegistry if (registry.getSavedStateProvider(PROVIDER) == null) { registry.registerSavedStateProvider(PROVIDER, this) } val savedState = registry.consumeRestoredStateForKey(PROVIDER) keyword = savedState?.getString(KEYWORD) ?: initialKeyword } }) } }
  36. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,

    private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = registryOwner.savedStateRegistry if (registry.getSavedStateProvider(PROVIDER) == null) { registry.registerSavedStateProvider(PROVIDER, this) } val savedState = registry.consumeRestoredStateForKey(PROVIDER) keyword = savedState?.getString(KEYWORD) ?: initialKeyword } }) } }
  37. class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,

    private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = registryOwner.savedStateRegistry if (registry.getSavedStateProvider(PROVIDER) == null) { registry.registerSavedStateProvider(PROVIDER, this) } val savedState = registry.consumeRestoredStateForKey(PROVIDER) keyword = savedState?.getString(KEYWORD) ?: initialKeyword } }) } }
  38. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  39. @Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )

    { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  40. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(articleRepository, keyword) } ) } }
  41. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set companion object { fun saver(coroutineScope: CoroutineScope, articleRepository: ArticleRepository): Saver<SearchKeywordState, *> = Saver( save = { it.keyword.text }, restore = { keyword -> SearchKeywordState(coroutineScope, articleRepository, keyword) } ) } }
  42. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { val autoCompleteKeywords = snapshotFlow { keyword.text } .transformLatest { if (it.isEmpty()) emit(emptyList()) else { delay(700) emit(articleRepository.fetchAutoCompleteKeywords(it)) } } .stateIn( scope = coroutineScope, started = SharingStarted.Lazily, initialValue = emptyList() ) }
  43. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { val autoCompleteKeywords = snapshotFlow { keyword.text } .transformLatest { if (it.isEmpty()) emit(emptyList()) else { delay(700) emit(articleRepository.fetchAutoCompleteKeywords(it)) } } .stateIn( scope = coroutineScope, started = SharingStarted.Lazily, initialValue = emptyList() ) }
  44. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { val autoCompleteKeywords = snapshotFlow { keyword.text } .transformLatest { if (it.isEmpty()) emit(emptyList()) else { delay(700) emit(articleRepository.fetchAutoCompleteKeywords(it)) } } .stateIn( scope = coroutineScope, started = SharingStarted.Lazily, initialValue = emptyList() ) }
  45. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { val autoCompleteKeywords = snapshotFlow { keyword.text } .transformLatest { if (it.isEmpty()) emit(emptyList()) else { delay(700) emit(articleRepository.fetchAutoCompleteKeywords(it)) } } .stateIn( scope = coroutineScope, started = SharingStarted.Lazily, initialValue = emptyList() ) }
  46. @Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:

    String ) { fun updateKeyword(newKeyword: TextFieldValue) { keyword = newKeyword } fun clearKeyword() { keyword = TextFieldValue("") } }
  47. @Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )

    = rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(articleRepository) ) { SearchKeywordState(articleRepository, initialKeyword) }
  48. @Composable fun rememberSearchKeywordState( coroutineScope: CoroutineScope = rememberCoroutineScope(), articleRepository: ArticleRepository, initialKeyword:

    String = "" ) = rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(articleRepository) ) { SearchKeywordState(articleRepository, initialKeyword) }
  49. @Composable fun rememberSearchKeywordState( coroutineScope: CoroutineScope = rememberCoroutineScope(), articleRepository: ArticleRepository, initialKeyword:

    String = "" ) = rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(coroutineScope, articleRepository) ) { SearchKeywordState(coroutineScope, articleRepository, initialKeyword) }
  50. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  51. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  52. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  53. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  54. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  55. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  56. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  57. @Composable fun KeywordScreen(articleRepository: ArticleRepository) { val keywordSearchState = rememberSearchKeywordState(articleRepository =

    articleRepository) val autoCompleteKeywords by searchKeywordState.autoCompleteKeywords.collectAsState() Column { TextField( value = searchKeywordState.keyword, onValueChange = searchKeywordState::updateKeyword, trailingIcon = { IconButton(onClick = searchKeywordState::clearKeyword) { ... } } ... ) LazyColumn { items(autoCompleteKeywords) { keyword -> Text(text = keyword, ...) } } } }
  58. @Stable class NiaAppState( val navController: NavHostController, val coroutineScope: CoroutineScope, networkMonitor:

    NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, ) { val topLevelDestinationsWithUnreadResources: StateFlow<Set<TopLevelDestination>> = userNewsResourceRepository.observeAllForFollowedTopics() .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> setOfNotNull( FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, ) }.stateIn( coroutineScope, SharingStarted.WhileSubscribed(5_000), initialValue = emptySet(), ) // More code... }
  59. class ArticleWriteViewModel( private val articleRepository: ArticleRepository ) : ViewModel() {

    /* ... */ fun saveDraftArticle() { viewModelScope.launch { articleRepository.saveDraftArticle(title.value, content.value) } } }
  60. // Activity override fun onStop() { super.onStop() viewModel.saveDraftArticle() } 실제

    구현 시에는 업로드 성공 후 저장하지 않도록 개편 필요
  61. private val _title = MutableStateFlow("") private val _content = MutableStateFlow("")

    val title = _title.asStateFlow() val content = _content.asStateFlow() init { viewModelScope.launch { title.combine(content) { title, content -> title to content } .debounce(5_000) .collect { (title, content) -> articleRepository.saveDraftArticle(title, content) } } }
  62. private val _title = MutableStateFlow("") private val _content = MutableStateFlow("")

    val title = _title.asStateFlow() val content = _content.asStateFlow() init { viewModelScope.launch { title.combine(content) { title, content -> title to content } .debounce(5_000) .collect { (title, content) -> articleRepository.saveDraftArticle(title, content) } } }
  63. private val _title = MutableStateFlow("") private val _content = MutableStateFlow("")

    val title = _title.asStateFlow() val content = _content.asStateFlow() init { viewModelScope.launch { title.combine(content) { title, content -> title to content } .debounce(5_000) .collect { (title, content) -> articleRepository.saveDraftArticle(title, content) } } }
  64. private val _title = MutableStateFlow("") private val _content = MutableStateFlow("")

    val title = _title.asStateFlow() val content = _content.asStateFlow() init { viewModelScope.launch { title.combine(content) { title, content -> title to content } .debounce(5_000) .collect { (title, content) -> articleRepository.saveDraftArticle(title, content) } } }
  65. private val _title = MutableStateFlow("") private val _content = MutableStateFlow("")

    val title = _title.asStateFlow() val content = _content.asStateFlow() init { viewModelScope.launch { title.combine(content) { title, content -> title to content } .debounce(5_000) .collect { (title, content) -> articleRepository.saveDraftArticle(title, content) } } }
  66. Section 04 종료될 확률 프로세스 상태 최종 액티비티 상태 매우

    낮음 포그라운드 포커스를 갖거나 갖을 예정 재개됨 낮음 보임 포커스 없음 시작됨/일시정지됨 높음 백그라운드 보이지 않음 중지됨 매우 높음 비어있음 소멸됨 bit.ly/docs activity lifecycle
  67. 구성 변경 메모리 UI 상태 시스템 리소스 필요한 경우 번들

    사용자 입력, 내비게이션 의존하는 일시적인 UI 상태 예상치 못한 앱 종료 디스크 애플리케이션 데이터 생존 공간 용도 ViewModel 상태 저장 API 영구 저장