위 git에서와 같이 Show more을 클릭하면 ui가 바뀌는 코드를 짜고 싶다. 그러면 우선 ui가 어떤 상태인지 알아야 하고, 이 상태를 변수에 저장해 알고자 한다. 그러면 다음과 같이 expanded
변수를 할당할 수 있을 것이다.
@Composable
private fun Greeting(name: String) {
var expanded = false
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
하지만 Compose에서는 위 코드가 동작하지 않는다! 데이터가 변경되면 새 데이터로 함수를 재시작하는 것을 Compose에서는 리컴포지션이라고 하는데, 컴포저블의 내부 상태를 추가하기 위해서는 mutableStateOf
함수를 사용해야 하기 때문이다. mutableStateOf
를 사용하면 Compose가 상태를 읽어 함수를 재구성한다.
import androidx.compose.runtime.mutableStateOf
...
@Composable
fun Greeting() {
val expanded = mutableStateOf(false)
}
하지만 하나의 스텝이 더 남아있다. 컴포저블 내 변수에 mutableStateOf
만 할당해서는 안 된다. 저 함수 하나만 사용하면 false 값을 가진 상태로 재설정하는 리컴포지션이 언제든지 일어나 수 있기 때문에, remember
를 사용해 변경 가능한 상태를 기억해야 한다.
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...
@Composable
fun Greeting() {
val expanded = remember { mutableStateOf(false) }
...
}
완성된 최종 코드다!
@Composable
private fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
하지만 Compose에서 사용하는 모든 변수가 mutableStateOf와 remember로 할당되어야 하는 건 아니다. 리컴포지션에 대비해 상태와 값을 알아야 하는 변수에서만 사용한다는 걸 기억하자.
다음과 같이 =
가 아니라 by
키워드를 사용하면 속성 위임이 가능하다. 매번 .value
를 입력하지 않아도 된다는 말이다.
var expanded by remember { mutableStateOf(false) }
하나 더 알아가자. remember
는 컴포지션이 유지되는 동안만 작동하기 때문에, 기기를 회전해 액티비티를 재시작하면 상태가 유지되지 않지 않고 손실된다. 이럴 때는 remember
대신 remeberSaveable
을 사용하면 된다.
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
animateDpAsState
컴포저블을 사용하면 위와 같은 애니메이션을 만들 수 있다.
아래 해당 dp에 도달할 때까지 객체를 펼치는 코드다.
@Composable
fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
onClick = { expanded = !expanded },
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
스프링 기반의 애니메이션 적용도 가능하다. 여기서는 중단이 좀 더 자연스럽게 보인다!
@Composable
fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
) {
Text(text = "Hello,")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded },
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
앱의 테마는 일반적으로 MaterialTheme
를 사용하는 것이 좋다.
// Do not copy
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
// ...
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
material 디자인 지정 원칙을 반영했기 때문인데, 다음과 같이 활용할 수 있다.
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
) {
Text(text = "Hello, ")
Text(text = name, style = MaterialTheme.typography.headlineMedium)
}
하지만 가끔은 지정한 스타일 외로 설정을 해줘야할 때가 있다. 이때는 copy
를 이용해 정의해둔 스타일을 수정한다.
Text(
text = name,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold
)
)
@Preview(
showBackground = true,
widthDp = 320,
uiMode = UI_MODE_NIGHT_YES,
name = "Dark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
FirstjetpackcomposeTheme {
Greetings()
}
}