JetPack Compose : 복잡한 state 관리하기
state 의 수가 적다면 composable 안에서 관리하는 것이 편리할 것이다. 하지만 그 수가 많아지고 로직도 많아진다면 다른 방법이 필요할 것이다.
state 관리는 세가지 방법이 있다.
- Composables
- State holders
- ViewModels
개념
State holders
state holder 는 복잡한 UI 의 state 와 그것과 관련된 logic 을 담고있는 객체이다. 단일 UI 위젯에 관련된 것일 수도, 전체 화면에 관련된 것일 수도 있다. 그리고 state holder 는 상호간 합성 가능(compoundable) 하다는 특징이 있는데, 이러한 특징은 여러 state를 함께 사용할때 유용하다.
- 하나의 UI 위젯은 0 ~ 다수의 state holder 에 의존할 수 있다.
- 어떤 state holder 는 비즈니스 로직이나 화면의 상태(screen state)에 접근해야할 경우 viewModel 에 의존할 수 있다.
- viewModel 은 data 혹은 business layer 에 의존하다.
state 와 logic 의 종류
[state]
- UI element state : UI 위젯의 hoist된 state 이다. 예를 들어 Scaffold의 ScaffoldState 가 있다.
- Screen or UI state : 화면에 표시되는 state 이다. 이러한 state들은 보통 다른 layer 와 연결되어 있어야 한다.
[logic]
- UI behavior or UI logic : state 의 변화를 화면에 어떻게 나타낼 것인가와 관련된 logic 이다. 예를 들어 네비게이션은 다음 화면을 결정하고, 메시지를 snackbar 로 보여줄지, toast 로 보여줄기 정하는 logic 이다. 이러한 logic 은 언제나 composition 내부에 있어야 한다.
- Business logic : state 변화에 따라 하는 행위이다. 회원가입을 한다거나, 특정 정보를 저장하는 등의 행위가 해당된다.
적용 예시
Composable 에 state 와 logic 을 두기
아주 간단한 수준의 UI logic과 UI elements state 이라면 composable 에 보관해두는 것도 좋은 방법일 수 있다.
@Composable
fun MyApp() {
MyTheme {
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
MyContent(
showSnackbar = { message ->
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(message)
}
}
)
}
}
}
하지만, mutable state 일 경우에는 해당 composable의 범위를 벗어나는 경우는 주의해야한다. 만약 다른 composable 에서 state 를 변경할 경우 버그를 찾아내기 어려워지기 때문이다. 위의 예에서는
stateholder 에 state 와 logic 을 두기
다양한 UI에 걸친 UI element state 와 UI logic 이라면 Composable 안에 두는 것은 현명하지 못하다. 이러한 state 와 logic 을 state holder 에 둔다면 복잡성을 줄이고 관심사의 분리 원칙을 지킬 수 있게 된다.
state holder 는 Composition 에서 생성되고 지워지는 객체이다. state holder 는 composition 의 lifecycle 을 따르기때문에 composition 의존성을 가질 수 있다. 아래는 MyAppState 라는 클래스를 통해 stateholder 를 만든 예시이다. 이 클래스는 Compose State 를 리소스로 받기 때문에 remembe 를 통해 클래스를 반환하도록 하는 것은 좋은 조치이다.
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavHostController,
private val resources: Resources,
/* ... */
) {
val bottomBarTabs = /* State */
// Logic to decide when to show the bottom bar
val shouldShowBottomBar: Boolean
get() = /* ... */
// Navigation logic, which is a type of UI logic
fun navigateToBottomBarRoute(route: String) { /* ... */ }
// Show snackbar using Resources
fun showSnackbar(message: String) { /* ... */ }
}
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
resources: Resources = LocalContext.current.resources,
/* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
MyAppState(scaffoldState, navController, resources, /* ... */)
}
@Composable
fun MyApp() {
MyTheme {
// rememberMyAppState() 를 통해 모든 state 를 받음.
val myAppState = rememberMyAppState()
Scaffold(
scaffoldState = myAppState.scaffoldState,
bottomBar = {
if (myAppState.shouldShowBottomBar) {
BottomBar(
tabs = myAppState.bottomBarTabs,
navigateToRoute = {
myAppState.navigateToBottomBarRoute(it)
}
)
}
}
) {
NavHost(navController = myAppState.navController, "initial") { /* ... */ }
}
}
}
viewmodel 을 통해 Business logic, screen state 접근하기
viewmodel 은 비즈니스 로직과, 데이터 레이어에 대한 접근을 하고, 동시에 screen state 등을 가질 수 있기 때문에 stateholder 의 특별한 형태로 볼 수 있다.
하지만 viewmodel 은 ui component 보다 더 긴 lifetcycle 을 가지고 있기 때문에 composition 의 lifecycle 에 종속된 참조를 가지고 있으면 안된다. 이는 viewmodel 이 context 를 갖지 말아야하는 것과 같고, 메모리 leak 을 방지하기 위함이다.
viewmodel 을 stateholder 로 이용하는 것은 화면 수준 composable 에 적절하다.
data class ExampleUiState(
dataToDisplayOnScreen: List<Example> = emptyList(),
userMessages: List<Message> = emptyList(),
loading: Boolean = false
)
class ExampleViewModel(
private val repository: MyRepository,
private val savedState: SavedStateHandle
) : ViewModel() {
var uiState by mutableStateOf<ExampleUiState>(...)
private set
// Business logic
fun somethingRelatedToBusinessLogic() { ... }
}
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
...
Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
Text("Do something")
}
}
viewModel 을 state holder 로 사용함으로써 얻는 이점은
- config 가 바뀌는 와중에도 실행되어야하는 프로세스를 보전하기 좋다.
- Navigation 과의 호환성이 좋다.
- hilt 와의 호환성이 좋다.
Navigation 과 ViewModel
네비게이션은 스크린이 백스택에 있을때 viewModel 을 캐시한다. 덕분에 다시 스크린을 되돌릴때 이전 데이터가 그대로 유지된다. 일반적인 state holder 로는 하기 힘든작업이다.
스크린이 백스택에서 pop 되었을떄 viewModel 도 clear 되며, 이때 State 들도 clear 되어 메모리 leak 을 방지한다.
state holder 와 viewmodel
이상을 살펴봤을 때 state holder 와 viewModel 은 다른 책임을 가지고 있음을 알 수 있다.
state holder 는 UI element state 와 UI behavior(UI logic) 에 대한 책임을, viewModel 은 Screen state(UI state)와 Bussiness logic 에 대한 책임을 갖고 있다. 그렇기 때문에 하나의 composable 에서 둘을 동시에 사용하는 것은 전혀 이상한 것이 아니며, 오히려 권장되어야 하는 사항이다.
private class ExampleState(
val lazyListState: LazyListState,
private val resources: Resources,
private val expandedItems: List<Item> = emptyList()
) { ... }
@Composable
private fun rememberExampleState(...) { ... }
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
val exampleState = rememberExampleState()
LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item) {
...
}
...
}
}
}
Author And Source
이 문제에 관하여(JetPack Compose : 복잡한 state 관리하기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@tjeong/JetPack-Compose-복잡한-state-관리하기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)