@Composable
fun MyApp(modifier: Modifier = Modifier) {
Scaffold(
content = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
Greeting("Android")
}
}
)
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val expandedPadding = if (expanded) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(bottom = expandedPadding)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
현재 Greeting 함수는 내부에 expanded라는 상태를 포함하고 있다. 이렇게, 내부에 상태를 가진 컴포저블을 Stateful 컴포저블이라고 한다.
Stateful 컴포저블은 호출자가 상태를 직접 제어하고 관리하지 않아도 되는 경우에 유용하다. 그러나 이렇게 내부에 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트가 힘들다.
반대로, 상태를 보유하지 않는 컴포저블은 Stateless 컴포저블이라한다. Stateful 컴포저블을 Stateless로 만들기 위해서는 상태를 두 개의 매개변수로 바꾸면 된다.
value: T: 표시할 현재 값onValueChange: (T) → Unit: 값이 새 값 T로 변경되도록 요청하는 이벤트위 규칙에 따라 위 코드를 stateless바꿔보면 아래와 같이 변경된다.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
Scaffold(
content = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
Greeting(
expanded,
{ expanded = !expanded },
"Android"
)
}
}
)
}
@Composable
fun Greeting(expanded: Boolean, onClick: () -> Unit, name: String, modifier: Modifier = Modifier) {
val expandedPadding = if (expanded) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(bottom = expandedPadding)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = onClick
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
이렇게 컴포저블을 Stateless로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴을 상태 호이스팅이라 한다.
그리고 Stateless가 되면서, UI가 이벤트를 생성하여 위쪽으로 전달하고 상태는 아래쪽으로 전달되어 UI를 표시하는 흐름이 생겼다.
이러한 흐름을 단방향 데이터 흐름(UDF)이라고 하며, 상태 호이스팅은 이 아키텍쳐를 Compose에서 구현하는 방법이다.
이렇게 끌어올린 상태에는 중요한 속성이 있다.
expanded를 읽으려는 경우 호이스팅을 통해 가능하게 할 수 있다.expanded를 ViewModel로 옮길 수 있다.그렇다면 상태를 어디까지 끌어올려야 할까?
공식문서에서는 아래와 같이 제시하고있다.
각각을 예제로 살펴보자.
상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 한다.
@Composable
fun GrandParentComponent(modifier: Modifier = Modifier) {
// var name by rememberSaveable { mutableStateOf("P1") } // 공통 상위 위치이지만 가장 낮지 않다.
ParentComponent()
}
@Composable
fun ParentComponent(modifier: Modifier = Modifier) {
var name by rememberSaveable { mutableStateOf("P1") } // 가장 낮은 공통 상위 위치
Column {
Button(
onClick = { name += "!" }
) {
Text("Add !")
}
ChildComponent1(name)
ChildComponent2(name)
}
}
@Composable
fun ChildComponent1(name: String, modifier: Modifier = Modifier) {
Text(text = "I'm child1, My parent name is $name")
}
@Composable
fun ChildComponent2(name: String, modifier: Modifier = Modifier) {
Text(text = "I'm child2, My parent name is $name")
}
상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 한다.
@Composable
fun ParentComponent(modifier: Modifier = Modifier) {
var name by rememberSaveable { mutableStateOf("P1") } // 가장 높은 위치
Column {
Button(
onClick = { name += "!" }
) {
Text("Add !")
}
ChildComponent1(name)
ChildComponent2(name)
}
}
@Composable
fun ChildComponent1(name: String, modifier: Modifier = Modifier) {
Text(text = "I'm child1, My parent name is $name")
}
@Composable
fun ChildComponent2(name: String, modifier: Modifier = Modifier) {
Text(text = "I'm child2, My parent name is $name")
}
name의 상태변화는 ChildComponent1, 2에 영향을 미친다.
그 중 가장 높은 곳에 상태를 끌어올려야 한다.
동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 두 상태를 함께 끌어올려야 한다.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Scaffold(
content = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
Greeting("Android")
}
}
)
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val expandedPadding = if (expanded) 48.dp else 0.dp
var backgroundColor by remember { mutableStateOf(Color.Gray) }
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(bottom = expandedPadding)
.background(color = backgroundColor)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = {
expanded = !expanded
backgroundColor = if(expanded) Color.Green else Color.Gray
}
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
backgroundColor라는 상태가 추가되었고 이는 expanded의 상태에 따라 결정된다.
이제 상태 호이스팅을 통해 Stateless하게 만들어 주려면, expanded뿐만 아니라 관련된 backgroundColor도 같이 끌어올려져야 한다.
아래는 개선된 코드이다.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
var backgroundColor by remember { mutableStateOf(Color.Gray) }
Scaffold(
content = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
Greeting(
expanded,
backgroundColor,
{
expanded = !expanded
backgroundColor = if (expanded) Color.Green else Color.Gray
},
"Android"
)
}
}
)
}
@Composable
fun Greeting(
expanded: Boolean,
backgroundColor: Color,
onClick: () -> Unit,
name: String,
modifier: Modifier = Modifier,
) {
val expandedPadding = if (expanded) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(bottom = expandedPadding)
.background(color = backgroundColor)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = onClick
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}