Share
Embed
Transcript
115 slides
-
상상하는 것을 소프트웨어로 구현하는 것을 좋아하는 팡무 GDG Songdo Organizer
GDSC TUK Lead 전 아우토크립트 안드로이드 개발 팀장 Who Am I? Section 00 @kisa002 @kisa002 @holykisa -
Section 00 UI 상태 데이터 사용자 입력 데이터 화면 상태
진행도 …. 백그라운드 상태 리사이즈 예외 케이스 화면 회전 테마 변경 시스템 리소스 부족 -
앱에서의 상태 손실 상태 저장 모범 사례 고급 사용 사례
Section 00 -
Section 01 구성요소 변경 • 앱 디스플레이 크기 • 화면
방향 • 글꼴 크기 및 두께 • 언어 • 다크 테마 / 라이트 테마 • 키보드 사용 가능 여부 • And then more… bit.ly/runtime changes -
Section 01 구성요소 변경 시스템 리소스 필요한 경우 메모리 부족
백그라운드 다른 앱 사용 -
Section 02 ViewModel 메모리 저장 제한된 메모리 구성 변경에서의 생존
백스택 있는 내비게이션 캐시 UI 상태 빠른 읽고 쓰기 -
sealed class ProfileUIState { object Loading : ProfileUIState() data class
Error(val throwable: Throwable? = null) : ProfileUIState() data class Success(val profile: Profile) : ProfileUIState() } -
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 ) } -
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 ) } -
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 ) } -
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 ) } -
Section 02 영구 저장 소규모 단순 데이터 적합 소규모 단순
데이터 적합 비동기 처리 SharedPreferences DataStore 복잡한 데이터 처리 참조 무결성 Room -
Section 02 영구 저장 디스크 저장 제한된 디스크 공간 구성
변경, 시스템 리소스 부족, 예외 케이스에서의 생존 느린 읽고 쓰기 애플리케이션 데이터 -
Section 02 상태 저장 API 메모리 저장 제한된 번들 구성
변경, 시스템 리소스 부족 생존 느린 읽고 쓰기 큰 정보와 리스트 내비게이션, 사용자 입력에 의존하는 일시적인 상태 -
Section 02 상태 저장 API 느린 읽고 쓰기 큰 정보와
리스트 내비게이션, 사용자 입력에 의존하는 일시적인 상태 리스트의 스크롤 위치 상세화면의 아이템 ID 사용자 설정 진행 상태 -
Section 02 상태 저장 API Jetpack Compose rememberSaveable View System
onSaveInstanceState -
@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() } } -
@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() } } -
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 지정되야함 -
Section 02 상태 저장 API 테스트 Jetpack Compose StateRestorationTester View
System ActivityScenario.recreate -
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() } } -
Section 02 상태 저장 API UI 로직 Jetpack Compose rememberSaveable
View System onSaveInstanceState -
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 될 때 저장됨 -
Section 02 Jetpack Compose View System UI 로직 rememberSaveable onSaveInstanceState
비즈니스 로직 SavedStateHandle SavedStateHandle -
@Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String, )
{ var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set } -
@Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )
= rememberSaveable( articleRepository, initialKeyword, saver = // TODO: Custom Saver ) { SearchKeywordState(articleRepository, initialKeyword) } -
@Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )
= rememberSaveable( articleRepository, initialKeyword, saver = // TODO: Custom Saver ) { SearchKeywordState(articleRepository, initialKeyword) } -
@Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )
= rememberSaveable( articleRepository, initialKeyword, saver = // TODO: Custom Saver ) { SearchKeywordState(articleRepository, initialKeyword) } -
@Stable class SearchKeywordState( private val articleRepository: ArticleRepository, initialKeyword: String )
{ var keyword by mutableStateOf(TextFieldValue(initialKeyword)) private set } -
@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) } ) } } -
@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) } ) } } -
@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) } ) } } -
@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) } ) } } -
@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) } ) } } -
@Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )
= rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(articleRepository) ) { SearchKeywordState(articleRepository, initialKeyword) } -
class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String
) { var keyword = initialKeyword private set } -
class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String
) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set } -
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" } } -
class SearchKeywordState( private val articleRepository: ArticleRepository, private val initialKeyword: String,
private val registryOwner: SavedStateRegistryOwner ) : SavedStateRegistry.SavedStateProvider { var keyword = initialKeyword private set init { } } -
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 } }) } } -
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 } }) } } -
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 } }) } } -
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 } }) } } -
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 } }) } } -
@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) } ) } } -
@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) } ) } } -
@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) } ) } } -
@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) } ) } } -
@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() ) } -
@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() ) } -
@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() ) } -
@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() ) } -
@Stable class SearchKeywordState( coroutineScope: CoroutineScope, private val articleRepository: ArticleRepository, initialKeyword:
String ) { fun updateKeyword(newKeyword: TextFieldValue) { keyword = newKeyword } fun clearKeyword() { keyword = TextFieldValue("") } } -
@Composable fun rememberSearchKeywordState( articleRepository: ArticleRepository, initialKeyword: String = "" )
= rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(articleRepository) ) { SearchKeywordState(articleRepository, initialKeyword) } -
@Composable fun rememberSearchKeywordState( coroutineScope: CoroutineScope = rememberCoroutineScope(), articleRepository: ArticleRepository, initialKeyword:
String = "" ) = rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(articleRepository) ) { SearchKeywordState(articleRepository, initialKeyword) } -
@Composable fun rememberSearchKeywordState( coroutineScope: CoroutineScope = rememberCoroutineScope(), articleRepository: ArticleRepository, initialKeyword:
String = "" ) = rememberSaveable( articleRepository, initialKeyword, saver = SearchKeywordState.saver(coroutineScope, articleRepository) ) { SearchKeywordState(coroutineScope, articleRepository, initialKeyword) } -
@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, ...) } } } } -
@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, ...) } } } } -
@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, ...) } } } } -
@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, ...) } } } } -
@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, ...) } } } } -
@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, ...) } } } } -
@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, ...) } } } } -
@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, ...) } } } } -
@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... } -
class ArticleWriteViewModel( private val articleRepository: ArticleRepository ) : ViewModel() {
/* ... */ fun saveDraftArticle() { viewModelScope.launch { articleRepository.saveDraftArticle(title.value, content.value) } } } -
// Activity override fun onStop() { super.onStop() viewModel.saveDraftArticle() } 실제
구현 시에는 업로드 성공 후 저장하지 않도록 개편 필요 -
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) } } } -
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) } } } -
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) } } } -
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) } } } -
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) } } } -
Section 04 종료될 확률 프로세스 상태 최종 액티비티 상태 매우
낮음 포그라운드 포커스를 갖거나 갖을 예정 재개됨 낮음 보임 포커스 없음 시작됨/일시정지됨 높음 백그라운드 보이지 않음 중지됨 매우 높음 비어있음 소멸됨 bit.ly/docs activity lifecycle -
구성 변경 메모리 UI 상태 시스템 리소스 필요한 경우 번들
사용자 입력, 내비게이션 의존하는 일시적인 UI 상태 예상치 못한 앱 종료 디스크 애플리케이션 데이터 생존 공간 용도 ViewModel 상태 저장 API 영구 저장