Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
안드로이드 UI 상태 저장 권장사항
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Pangmoo
August 24, 2023
Programming
940
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
410
Compose Web 개발하기
pangmoo
14
1.4k
코틀린으로 멀티플랫폼 만들기
pangmoo
4
1.7k
Kotlin Multiplatform으로 Android/iOS/Desktop 번역기 만들기
pangmoo
0
700
MADC 2023 Kotlin Multiplatform (KMP)
pangmoo
0
190
Compose로 Android&Desktop 멀티플랫폼 만들기
pangmoo
0
610
API 통신, Retrofit 대신 Ktor 어떠신가요
pangmoo
2
970
Other Decks in Programming
See All in Programming
スマートフォンでモールス信号を送受信する 〜スマートフォンのLEDとカメラで作る光通信の設計と実装〜
atsuki_seo
0
150
[GoCon2026] When Goroutines Are Not Enough: Runtime Locality in High-Throughput Go
takehaya
6
2.3k
『寄り添うラジオ』をAIで作る 体験価値から逆算した、会話しないUXと品質設計
theoriatec2024
3
180
ゲームコントローラやキーボードのファームウェアをSwiftで書く
kishikawakatsumi
1
250
AgentCore CLI で進化した AWS での AI エージェントの作り方 : 必要な機能を必要な時に
icoxfog417
PRO
3
360
フロントエンドUIフレームワークのこれまでとこれから
ssssota
5
2.8k
変化を抱擁するドキュメントの作り方 - ビジネスルール駆動開発がもたらす、コードとの新しい関係
ioki
2
220
アクセシビリティから考える情報設計
high_g_engineer
0
380
モデルのリファクタリングが難しいと思ったら、そもそも複雑だったのはビジネス仕様だった ? / is-the-business-domain-the-real-complexity
hatsu38
0
350
TiDB Cloudのカスタムコントローラーによるオートスケール対応
takaidohigasi
0
130
Starting & Sustaining Code-Based E2E Testing for Non-Coding QA Teams( #jasstniigata )
teyamagu
PRO
1
430
iOSDC2026登壇資料.pdf
riofujimon
0
170
Featured
See All Featured
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
530
Side Projects
sachag
456
43k
Fashionably flexible responsive web design (full day workshop)
malarkey
409
67k
Ruling the World: When Life Gets Gamed
codingconduct
0
340
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
510
Why You Should Never Use an ORM
jnunemaker
PRO
61
10k
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
240
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
Prompt Engineering for Job Search
mfonobong
0
450
We Have a Design System, Now What?
morganepeng
55
8.3k
The Curious Case for Waylosing
cassininazir
1
510
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
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