Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
안드로이드 UI 상태 저장 권장사항
Search
Pangmoo
August 24, 2023
Programming
920
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
안드로이드 UI 상태 저장 권장사항
Pangmoo
August 24, 2023
More Decks by Pangmoo
See All by Pangmoo
게임 개발하던 학생이이 세계에선 안드로이드 개발자?
pangmoo
0
390
Compose Web 개발하기
pangmoo
12
1.4k
코틀린으로 멀티플랫폼 만들기
pangmoo
3
1.6k
Kotlin Multiplatform으로 Android/iOS/Desktop 번역기 만들기
pangmoo
0
690
MADC 2023 Kotlin Multiplatform (KMP)
pangmoo
0
190
Compose로 Android&Desktop 멀티플랫폼 만들기
pangmoo
0
590
API 통신, Retrofit 대신 Ktor 어떠신가요
pangmoo
2
960
Other Decks in Programming
See All in Programming
[2026年度第1回ORセミナー] 計画最適化ベンチャーと競技プログラミング人材
terryu16
0
260
Signal Forms: Details & Live Coding @enterJS 2026 in Mannheim
manfredsteyer
PRO
0
130
依存関係から依存物へ―Dependencyという言葉の歴史をひも解く
j_lee
0
120
Vue × Nuxt × Oxc どこまで使える?実運用の現在地
andpad
0
250
脅威をエンジニアリングの糧にして――現場編 / Turning Threats into Engineering Fuel — Field Edition
nrslib
0
280
軽量Java基盤の設計 DIコンテナに頼らない、長期保守と1秒起動の実現 JJUG CCC 2026 Spring
macha64
0
520
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
AIとASP.NET Coreで雑Webアプリを作った話
mayuki
0
620
コンテキストの使い捨てをやめる — ビジネスルール駆動開発と miko —
ioki
0
200
Semantic Version 単位で戦略を柔軟に変えて、パッケージアップデートを自動化する
daitasu
1
240
過去最大のMCPアップデート! 2026-07-28 RC版の謎に迫る
licux
6
320
「なぜそう決めたのか」を残し続ける仕組み ― Notion AI カスタムエージェント × Slack連携による設計判断の自動記録 - NIKKEI Tech Talk #47
niftycorp
PRO
0
170
Featured
See All Featured
Odyssey Design
rkendrick25
PRO
2
700
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
11k
Why Mistakes Are the Best Teachers: Turning Failure into a Pathway for Growth
auna
0
160
Bash Introduction
62gerente
615
220k
Believing is Seeing
oripsolob
1
140
Joys of Absence: A Defence of Solitary Play
codingconduct
1
390
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
150
RailsConf 2023
tenderlove
30
1.5k
For a Future-Friendly Web
brad_frost
183
10k
Optimising Largest Contentful Paint
csswizardry
37
3.7k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Done Done
chrislema
186
16k
Transcript
안드로이드 UI 상태 저장 권장사항 GwangMoo You GDG Songdo Organizer
GDG TUK Lead
상상하는 것을 소프트웨어로 구현하는 것을 좋아하는 팡무 GDG Songdo Organizer
GDSC TUK Lead 전 아우토크립트 안드로이드 개발 팀장 Who Am I? Section 00 @kisa002 @kisa002 @holykisa
Section 00
상태 저장을 아시나요?
Section 00 UI 상태 데이터 사용자 입력 데이터 화면 상태
진행도 …. 백그라운드 상태 리사이즈 예외 케이스 화면 회전 테마 변경 시스템 리소스 부족
Section 00 By UI UX Expert
Section 00 By UI UX Expert
앱에서의 상태 손실 상태 저장 모범 사례 고급 사용 사례
Section 00
고급 사용 사례 개인적인 팁 모음 요약 정리 Section 00
앱에서의 상태 손실 Section 01
Section 01 구성요소 변경 화면 회전
Section 01 구성요소 변경 테마 변경
Section 01 구성요소 변경 • 앱 디스플레이 크기 • 화면
방향 • 글꼴 크기 및 두께 • 언어 • 다크 테마 / 라이트 테마 • 키보드 사용 가능 여부 • And then more… bit.ly/runtime changes
Section 01 구성요소 변경 시스템 리소스 필요한 경우 메모리 부족
백그라운드 다른 앱 사용
Section 01 성요소 변경 시스템 리소스 필요한 경우 예기치 않은
종료
상태 저장을 위한 모범 사례 Section 02
Section 02 구성요소 변경 시스템 리소스 필요한 경우 예기치 않은
종료
Section 02 구성요소 변경
Section 02 구성요소 변경 ViewModel
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 예기치 않은 종료
Section 02 예기치 않은 종료 영구 저장
Section 02 영구 저장 소규모 단순 데이터 적합 소규모 단순
데이터 적합 비동기 처리 SharedPreferences DataStore 복잡한 데이터 처리 참조 무결성 Room
Section 02 영구 저장 디스크 저장 제한된 디스크 공간 구성
변경, 시스템 리소스 부족, 예외 케이스에서의 생존 느린 읽고 쓰기 애플리케이션 데이터
Section 02 상태 저장 API 시스템 리소스 필요한 경우
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
Section 02 상태 저장 API 비즈니스 로직 ViewModel intergration SavedStateHandle
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
고급 사용 사례 Section 03
Section 03 커스텀 UI 상태 저장
@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) }
Section 03 View System
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 } }) } }
Section 04 개인적인 팁 모음
Section 04 커스텀 UI 상태 저장 응용하기
Section 04 커스텀 UI 상태 저장 응용하기
@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, ...) } } } }
Section 04 실제로도 이렇게 쓰나요?
Section 04 Now in Android 에서도 기본 앱 상태로 사용하고
있음
@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... }
Section 04 영구 저장 응용하기
Section 04 영구 저장 응용하기
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() } 실제
구현 시에는 업로드 성공 후 저장하지 않도록 개편 필요
Section 04 영구 저장 응용하기 Exception 및 강제 종료에는 저장
❌
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 영구 저장 응용하기 Exception 및 강제 종료에도 생존
✅
Section 04 시스템 리소스 필요한 경우 연출하기
Section 04
Section 04
Section 04 활성화되어 있는 경우 ActivityScenario 테스트 시 오류 발생
Section 04 메모리에서 제거될 가능성
Section 04
Section 04
Section 04 종료될 확률 프로세스 상태 최종 액티비티 상태 매우
낮음 포그라운드 포커스를 갖거나 갖을 예정 재개됨 낮음 보임 포커스 없음 시작됨/일시정지됨 높음 백그라운드 보이지 않음 중지됨 매우 높음 비어있음 소멸됨 bit.ly/docs activity lifecycle
Section 05 요약 정리
Section 05 구성요소 변경 시스템 리소스 필요한 경우 예기치 않은
종료
구성 변경 메모리 UI 상태 시스템 리소스 필요한 경우 번들
사용자 입력, 내비게이션 의존하는 일시적인 UI 상태 예상치 못한 앱 종료 디스크 애플리케이션 데이터 생존 공간 용도 ViewModel 상태 저장 API 영구 저장
Section 05 bit.ly/saving states
Thank You GwangMoo You GDG Songdo Organizer GDG TUK Lead
GitHub