JetPack Compose : 복잡한 state 관리하기

timothy jeong·2022년 3월 27일
2

Android with Kotlin

목록 보기
69/69

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) {
                ...
            }
            ...
        }
    }
}
profile
개발자

0개의 댓글