TIL) 230316

Hanseul Lee·2023년 3월 20일
0

TIL

목록 보기
13/23

Compose에서 상태를 저장하는 변수 할당하기, mutableStateOf와 remember

위 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

animateDpAsState 컴포저블을 사용하면 위와 같은 애니메이션을 만들 수 있다.

  • 애니메이션이 완료될 때까지 애니메이션에 의해 객체 value가 업데이트되는 상태를 반환한다.
  • dp로 목표값을 설정한다.

아래 해당 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")
            }
        }
    }
}

ui Theme

앱의 테마는 일반적으로 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()
    }
}

0개의 댓글