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
スマートグラスで並列バイブコーディング
hyshu
0
260
エージェンティックRAGにAWSで入門しよう!
har1101
9
1.7k
正しくソフトウェアを作る、前提を疑うための認知の視点 / doubt-premise
minodriven
21
7k
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
Lessons from Spec-Driven Development
simas
PRO
0
220
技術記事、AIに書かせるか、自分で書くか? 〜それでも私が自分の手で書く理由〜 / #QiitaConference
jnchito
2
1.5k
TSKaigi Night Talks 2026_TypeScriptでサプライチェーンの整合性を型に閉じ込める
geekplus_tech
0
400
Strategic Design in the Frontend: Moduliths & Micro Frontends @DDDEurope
manfredsteyer
PRO
0
130
Contextとはなにか
chiroruxx
1
370
Go1.27で導入されるジェネリクスメソッドでできること
mackee
0
170
ローカルLLMでどこまでコードが書けるか -拡張版 / How much code can be written on a local LLM Extended
kishida
12
4.4k
A2UI という光を覗いてみる
satohjohn
1
150
Featured
See All Featured
A Guide to Academic Writing Using Generative AI - A Workshop
ks91
PRO
1
330
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
870
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
201
75k
So, you think you're a good person
axbom
PRO
2
2.1k
Google's AI Overviews - The New Search
badams
0
1k
Designing Powerful Visuals for Engaging Learning
tmiket
1
420
Six Lessons from altMBA
skipperchong
29
4.3k
Future Trends and Review - Lecture 12 - Web Technologies (1019888BNR)
signer
PRO
0
3.6k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.5k
Navigating Weather and Climate Data
rabernat
0
230
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
1
780
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