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
930
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
400
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
なぜ関数型プログラミングで「型」と「証明」が語られるのか #fp_matsuri
kajitack
3
930
気圧・高度・GPSを記録&可視化するアプリ「Koudo」を作った話
hjmkth
1
360
JAWS-UG横浜 #102 AWSサ終供養LT会 成仏できない AWS サービスたち 〜本日、三体供養します〜
maroon1st
0
200
音楽のための関数型プログラミング言語mimiumにおける多段階計算の活用
tomoyanonymous
1
330
AI時代の仕事技芸論〜ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ(スクフェス仙台 2026バージョン)
kuranuki
0
640
Welcome to the "Parametricity" 🏙️ − Generic だけど Specific な世界 −
guvalif
PRO
1
170
ビデオ通話が繋がる0.2秒で何が起きているのか
supurazako
2
150
LaravelLive Japan の裏方のすべて — 第188回 PHP勉強会@東京 (2026-06-24)
suguruooki
2
150
PHP Application における Kubernetes 内 gRPC 通信
ganchiku
0
440
使用 Meilisearch 建立新聞搜尋工具
johnroyer
0
150
Haskell/Servantを通してWebミドルウェアを捉え直す
pizzacat83
1
580
初めてのKubernetes 本番運用でハマった話
oku053
0
130
Featured
See All Featured
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
2.9k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
220
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
340
sira's awesome portfolio website redesign presentation
elsirapls
0
300
Product Roadmaps are Hard
iamctodd
55
12k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.6k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.2k
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
Raft: Consensus for Rubyists
vanstee
141
7.6k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
123
22k
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
320
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