컴포저블 함수에서 다양한 함수들에 의해 불러와 지거나 수정되어지는 상태(state)는 공통적인 부모내에 존재한다. 이러한 처리를 상태 끌어올리기(state hoisting) 이라고 한다. hoist의 의미는 끌어 올려지는 것을 의미한다.(lift or elevate)

상태를 끌어올려 만들면 중복되는 상태 및 버그를 피할 수 있고, 컴포저블 함수의 재사용성과 테스트 용이성을 상당히 좋게 만들수 있다. 컴포저블 함수의 부모가 제어 할 필요 없는 상태는 끌어올릴 필요 없다. 상태를 생성하거나 제어하는 쪽에 source of truth를 두도록 하자

예를 들기위해, 앱에서 온보딩 스크린을 생성해보자

MainActivity.kt 코드는 다음과 같이 추가 한다.

@Composable
fun OnboardingScreen() {
    // TODO: 이 state는 hoisting 되야 한다.
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = { shouldShowOnboarding = false } 
            ) {
                Text("Continue")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

이 코드는 다음과 같은 몇가지 새로운 내용을 포함한다.

  • OnboardingScreen이라는 새로운 컴포저블 함수와 preview를 추가했다. 프로젝트를 빌드하면 동시에 여러 preview를 가질 수 있다. 콘텐츠가 올바르게 정렬되었는지 확인하기 위해 고정적인 높이를 추가했다.
  • Column의 설정값은 화면 중앙에 콘텐츠를 보여줄 수 있도록 설정된다.
  • alignment는 컴포저블 함수를 row 또는 column내에서 정렬할 때 사용된다.
  • shouldShowOnboarding 변수는 = 연산자 대신 by 키워드를 사용하고 있다. property delegate로 매번 .value를 타이핑해서 해당 값에 접근하는 수고를 덜어준다.
  • 버튼을 클릭하면 shouldShowOnboarding은 false로 설정되지만, 아직 이 상태값을 참조하지는 않는다.

이제 새로운 온보딩 스크린을 앱에 추가할 수 있게 되었다. 이를 실행시에 보여주고 사용자가 “Continue” 버튼을 눌렀을 때 숨기도록 하자.

View시스템에서는 INVISIBLE 또는 GONE 을 활용하여 View를 숨길 수 있었다. 컴포즈에서는 UI 요소를 숨기지(hide) 말자. UI 구성(Composition)시에 컴포저블 함수를 호출하지 않으면 간단히 해결할 수 있다. 그래서 컴포즈가 생성하는 UI 트리에 추가되지 않으면 된다. 컴포즈에서는 코틀린 조건문으로 간단히 이를 구현할 수 있다. 예를들어 OnboardingScreen 또는 Greeting 목록을 보여주기 위해 다음과 같이 코드를 작성할 수 있다.

// 아직 이 코드를 사용하지 마세요
@Composable
fun MyApp() {
    if (shouldShowOnboarding) { // 이건 어디서 온걸까?
        OnboardingScreen()
    } else {
        Greetings()
    }
}

그러나 이 코드에서 확인할 수 있듯이 shouldShowOnboarding에는 접근할 수가 없다. OnboardingScreen에서 생성한 state를 MyApp 함수에서 공유해야만 한다.

어떤 값에 대한 상태를 부모레벨에서 공유하는 것 대신에, state를 상위로 끌어올리면된다. 간단히 state를 공통 부모로 올려 이를 접근할 수 있게 한다.

먼저 MyApp으로 보여줄 컨텐츠를 옮기고 이를 Greetings라는 컴포저블 함수로 만들자.

@Composable
fun MyApp() {
     Greetings()
}

@Composable
private fun Greetings(names: List<String> = listOf("World", "Compose")) {
    Column(modifier = Modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

MyApp에서 보여줄 다른 방식으로 보여줄 로직을 추가하고, 이에 대한 상태(shouldShowOnboarding)도 끌어올리자.

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(/* TODO */)
    } else {
        Greetings()
    }
}

shouldShowOnboarding을 OnBoadingScreen 컴포저블 함수에 공유해야 하지만 그냥 직접적으로 전달하지 않는다. “Continue” 버튼을 사용자가 눌렀을 때 shouldShowOnboarding 값을 변경하도록 하자.

이를 위해 OnboardingScreen 함수의 파라미터로 onContinueClicked라는 함수파라미터를 정의하고, 이를 Button 함수의 onClick 파라미터로 전달하자. 사용자 클릭 시 shouldShowOnboarding의 값을 다음과 같이 변경한다.

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
    } else {
        Greetings()
    }
}

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier
                    .padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

OnboardingScreen에 state를 전달하는게 아닌 함수를 전달하는 것으로, 컴포저블 함수를 좀 더 재사용가능하게 만들고 다른 컴포저블 함수로부터 상태가 변경되는 것을 방지할 수 있게 되었다. 일반적으로 이러한 방식은 코드를 좀 더 단순하게 유지 할 수 있게 만들어 준다.

다음 코드는 OnBoardingPreview가 어떻게 수정되어야 하는지 보여준다.

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // 아무것도 안함.
    }
}

미리보기용으로 onContinueClicked에 빈 람다표현식을 적용했다.

실제 빌드를 하여 MyApp 컴포저블 함수가 실행된 것을 확인하면 다음과 같다.

지금까지 작성한 전체코드는 다음과 같다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp()
            }
        }
    }
}

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
    } else {
        Greetings()
    }
}

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

@Composable
private fun Greetings(names: List<String> = listOf("World", "Compose")) {
    Column(modifier = Modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

State hoisting에 도움이 되는 규칙

  • State는 State를 사용하는 모든 하위 컴포저블들의 공통 상위 항목으로 끌어올린다.
  • Recomposition 빈도를 적게 하기 위해, State는 최소한 수정 할 수 있는 내에서 최고 수준으로 끌어올린다.
  • 동일한 이벤트에 대한 응답으로 두 State가 변경되는 경우 하나의 State로 처리하자.
카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.