본문 바로가기
Android Application/앱 설계

안드로이드 컴포즈(jetpack compose) - state hoisting

by sdchjjj 2023. 4. 7.
728x90

안녕하세요.

이번 포스팅에서는 상태 호이스팅 패턴을 안드로이드 컴포즈를 통해 구현해 보겠습니다.

 

state hoisting 패턴은 단방향 데이터 흐름과 관계가 있습니다.

이벤트가 발생하면 상태를 필요에 맞게 변경하고 UI에 표시하는 과정을 하나의 방향으로만 흐르도록 설계하는 것입니다.

여기에 state hoisting을 적용한다면, 이벤트가 발생 -> 이벤트를 뷰모델까지 올려줌 -> 뷰모델에서 필요한 기능을 처리 -> 상태를 변경 -> UI가 이에 맞게 변경되는 식으로 동작하게 됩니다.

 

간단한 예제를 작성해 보겠습니다.

 

언어: 코틀린

sdk vsersion

  - compile: 33

  - min: 21

  - target: 33

 

가장 먼저 뷰모델을 작성합니다.

ComposeViewModel.kt

@HiltViewModel
class ComposeViewModel @Inject constructor() : ViewModel() {

    var number = mutableStateOf(0)
        private set

    fun onButtonClicked() {
        number.value += 1
    }
}

mutableState 변수와 버튼 클릭 시 호출될 함수가 있습니다.

 

텍스트와 버튼을 하나씩 작성합니다.

ComposeActivity.kt

@Composable
fun ExampleText(number: Int) {
    Text(text = "Hello $number!")
}

@Composable
fun ExampleButton(onClicked: () -> Unit) {
    Button(
        modifier = Modifier.wrapContentSize(),
        onClick = onClicked
    ) {
        Text(text = "버튼")
    }
}

텍스트는 Int를, 버튼은 함수를 인자로 받습니다.

 

위에서 작성한 텍스트와 버튼을 그려주는 MainScreen을 작성합니다.

ComposeActivity.kt

@Composable
fun MainScreen(viewModel: ComposeViewModel) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .weight(.9f),
            contentAlignment = Alignment.Center
        ) {
            ExampleText(viewModel.number.value)
        }
        Box(
            modifier = Modifier
                .fillMaxSize()
                .weight(.1f),
            contentAlignment = Alignment.BottomCenter
        ) {
            ExampleButton(viewModel::onButtonClicked)
        }
    }
}

인자로 ComposeViewModel을 받으며, 텍스트에는 mutableState number를, 버튼에는 onButtonClicked 함수를 넘겨줍니다.

 

이제 viewModel을 주입받고 MainScreen을 호출하는 클래스를 작성합니다.

ComposeActivity.kt

@AndroidEntryPoint
class ComposeActivity : ComponentActivity() {
    
    private val viewModel: ComposeViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LaboratoryTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainScreen(viewModel = this@ComposeActivity.viewModel)
                }
            }
        }
    }
}

MainScreen을 호출하면서 주입받은 viewModel을 넘겨줍니다.

 

버튼을 클릭하면 이벤트가 뷰모델까지 올라가고, 뷰모델에서 +1을 계산한 뒤 mutableState에 set 해주면 변경된 값을 사용해 ExampleText가 다시 그려지게 됩니다.

이때, 이벤트에 따라 변화하는 state(예제에서의 number)를 말단 Composable이 가지고 있는 것이 아니라 위쪽으로 올려서 관리를 하고, state가 변할 시 위쪽에서 Composable을 다시 그려주는 방식에서 hoisting이라는 표현을 씁니다.

 

전체 코드입니다.

ComposeActivity.kt

@AndroidEntryPoint
class ComposeActivity : ComponentActivity() {
    
    private val viewModel: ComposeViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LaboratoryTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainScreen(viewModel = this@ComposeActivity.viewModel)
                }
            }
        }
    }
}

@Composable
fun MainScreen(viewModel: ComposeViewModel) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .weight(.9f),
            contentAlignment = Alignment.Center
        ) {
            ExampleText(viewModel.number.value)
        }
        Box(
            modifier = Modifier
                .fillMaxSize()
                .weight(.1f),
            contentAlignment = Alignment.BottomCenter
        ) {
            ExampleButton(viewModel::onButtonClicked)
        }
    }
}

@Composable
fun ExampleText(number: Int) {
    Text(text = "Hello $number!")
}

@Composable
fun ExampleButton(onClicked: () -> Unit) {
    Button(
        modifier = Modifier.wrapContentSize(),
        onClick = onClicked
    ) {
        Text(text = "버튼")
    }
}

 

에뮬레이터에서 돌려보겠습니다.

figure1. run

 

정상적으로 동작합니다.

 

이상 포스팅을 마치겠습니다.

728x90

댓글