[Compose] compose의 이모저모1
해당 게시글은 compose camp codelab 1을 진행하며 알게된 점을 정리한 내용입니다.
🙋♀️ 컴포저블 재사용
- 함수가 매우 커지면 가독성에 영향을 줄 수 있다.
- 재사용 할 수 있는 작은 구성요소를 만들면 앱에서 사용하는 ui 요소의 라이브러리를 쉽게 만들 수 있다.
- 각 요소는 화면의 작은 부분을 담당하며 독립적으로 수정할 수 있다.
- 함수는 기본적으로 빈 수정자가 할당되는 수정자 매개변수를 포함하는 것이 좋다.
🙋♀️ 상태 변화와 리컴포지션
항목이 펼쳐진 상태인지에 따라 항목의 크기를 달리하는 작업을 진행하려고 한다. 우선 항목의 상태를 저장할 변수가 필요하다.
@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")
}
}
}
}
🙂 하지만 이렇게 하면 expanded 값을 상태 변경으로 감지하지 않으므로 아무 일도 일어나지 않는다.
- 따라서 컴포저블에 내부 상태를 추가하려면
mutableStateOf
함수를 사용해 해당 State를 읽는 함수를 재구성하게 된다. State
및MutableState
는 어떤 값을 보유하고 그 값이 변경될 때마다 UI 없데이트(리컴포지션)를 트리거하는 인터페이스이다.
@Composable
fun Greeting() {
val expanded = mutableStateOf(false) // Don't do this!
}
🙂 하지만 컴포저블 내의 변수에 mutableStateOf
를 할당하기만 할 수는 없다.
- 위의 경우에는 false 값을 가진 변경 가능한 새 상태로 상태를 재설정하여 컴포저블을 다시 호출할 때는 언제든지 리컴포지션이 일어날 수 있기 때문이다.
🙂 여러 리컴퍼지션간에 상태를 유지하려면 remember
를 사용하여 변경 가능한 상태를 기억해야 한다.
@Composable
fun Greeting() {
//내부 상태는 클래스의 비공개 변수로 보면 된다.
val expanded = remember { mutableStateOf(false) }
...
}
remember
는 리컴포지션을 방지하는 데 사용되므로 상태가 재설정되지 않는다.
🙂 여기서 잠깐!! remember
함수는 컴포저블이 컴포지션에 유지되는 동안에만 작동한다. 이를 해결하기 위해 rememberSaveable
을 사용한다.
val expanded by rememberSaveable { mutableStateOf(false) }
rememberSaveable
을 통해 회전과 같은 구성 변경 및 프로세스 중단에도 상태가 저장된다.=
대신by
키워드를 사용한 속성 위임을 통해 매번.value
를 입력할 필요를 없앤다.
🙋♀️ 성능 지연 목록 만들기
수천 개의 요소를 column에 배치하고자 할 때 어떻게 해야 할까?
names: List<String> = List(1000) {"$it"}
만약 이를 통해 Column을 만든다면 화면에 나타나는 것을 포함해 총 1000개의 목록이 생성되고, 이는 성능 기준에 맞지 않는다.
🙂 따라서 우리가 사용할 것은 Compose에서의 RecyclerView인 LazyColumn
과 LazyRow
이다.
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
names: List<String> = List(1000) { "$it" }
) {
LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}
- 기본적으로 LazyColumn API는 범위 내에서 items 요소를 제공하며, 여기서 로직을 개별 항목마다 적용한다.
LazyColumn
은 RecyclerView와 같이 하위 요소를 재활용하지 않는다.- 컴포저블을 방출하는 것은 Android Views를 인스턴스화하는 것보다 상대적으로 비용이 적게 들기 때문에 스크롤 할 때 새 컴포저블을 방출하고 계속 성능을 유지하게 된다.
🙋♀️ ⭐상태 호이스팅⭐
구성 가능한 함수에서 여러 함수가 읽거나 수정하는 상태는 공통의 상위 항목에 위치해야 한다.
- 이 프로세스를
상태 호이스팅
이라고 한다. - 호이스팅: 들어/끌어올린다.
장점
- 상태를 호이스팅할 수 있게 만들면 상태가 중복되지 않아 버그가 발생하는 것을 방지할 수 있다.
- 컴포저블을 재사용할 수 있고 훨씬 쉽게 테스트할 수 있다.
주의
- 컴포저블의 상위 요소에서 제어할 필요가 없는 상태는 호이스팅되면 안 된다.
온보딩 화면을 처음 한 번만 보여주고 continue 버튼을 누르면 온보딩 화면이 숨겨지는 과정을 진행해보자! 여기서 숨겨진다는 것은 단지 컴포지션에 UI 요소를 추가하지 않으면 된다.
@Composable
fun OnboardingScreen() {
// TODO: This state should be hoisted⭐
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()
}
}
- 위와 같이 하면
shouldShowOnboarding
을 다른 컴포저블 함수에서 액세스할 수 없다.
🙂 이때 상태값을 상위 요소와 공유하는 대신 상태를 호이스팅한다. 즉, 상태 값에 액세스해야 하는 공통 상위 요소로 상태 값을 이동하기만 하면 된다.
@Composable
fun MyApp() {
var shouldShowOnboarding by remember { mutableStateOf(true) }
if (shouldShowOnboarding) {
OnboardingScreen(/* TODO */)
} else {
Greetings()
}
}
shouldShowOnboarding
을 온보딩 화면과 공유해야 하지만, 직접 전달하지 않는다.- OnbarodingScreen이 상태를 변경하도록 하는 대신 사용자가 continue 버튼을 클릭했을 때 앱에 알리도록 하는 것이 더 좋다.
🙂 그렇다면 이벤트를 어떻게 전달할까? 아래로 콜백
을 전달하면 된다.
- 콜백: 다른 함수에 인수로 전달되는 함수로 이벤트가 발생하면 실행된다.
@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에 전달하는 방식으로 컴포저블의 재사용 가능성을 높이고 다른 컴포저블이 해당 상태를 변경하지 않도록 보호하고 있다.
🙂 이렇게 될 경우, 온보딩 화면의 미리보기를 어떻게 수정할 수 있을까?
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
}
}
빈 람다 표현식을 할당해 아무 작업도 하지 않는 것이 미리보기를 위해 완벽한 방식이다.
🙋♀️ 목록에 애니메이션 적용하기
애니메이션을 적용하는 방식은 정말 다양하다. compose animation 공식 문서를 통해서 보다 다양한 방식을 학습할 수 있다.
여기에서는 간단한 하나의 예시를 진행해보고자 한다.
⌨️ animateDpAsState
- 해당 컴포저블은 애니메이션이 완료될 때까지 애니메이션에 의해 객체의 value가 계속 업데이트되는 상태 객체를 반환한다.
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp
)
🙂 하지만 이렇게만 진행하면 스크롤을 한 후 다시 돌아왔을 때 해당 목록이 다시 닫혀있는 것을 확인할 수 있다. 이를 해결하기 위해서는 expanded를 rememberSaveable
함수를 사용해 상태를 기억해야 한다.
var expanded by rememberSaveable {mutableStateOf(false)}
val extraPadding by animateDpAsState(
if(expanded) 48.dp else 0.dp,
)
⌨️ animationSpec 매개변수
animateDpAsState
는 animationSpec
매개변수를 선택적으로 사용해 애니메이션을 맞춤 설정할 수 있다.
- 스프링 기반의 애니메이션의 경우, 애니메이션 도중 타겟 값이 변경될 때 속도의 연속성을 보장하므로 기간 기반 AnimationSpec 유형보다 원활하게 중단을 처리할 수 있다.
@Composable
private 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(
...
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
...
)
}
- 패딩의 경우 음수가 되지 않도록 해야 한다.
coerceAtLeast
를 통해 최소값을 정한다.
🙋♀️ 기본 테마 사용하기
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
ui/theme/Theme.kt
파일을 열면 기본 테마를 확인할 수 있다. 또한, 기본 테마에서 MaterialTheme
를 래핑하는 것을 확인할 수 있다.
MaterialTheme
은 Material 디자인 사양의 스타일 지정 원칙을 반영한 구성 가능한 함수이다.- 기본 테마로 래핑된 모든 하위 컴포저블에서는
MaterialTheme
의 세 가지 속성,colors
,typography
,shapes
를 가져올 수 있다. - 일반적으로
MaterialTheme
내부의 색상, 모양, 글꼴 스타일을 유지하는 것이 훨씬 좋다.
⌨️ 정의된 스타일 수정
copy
함수를 사용해 미리 정의된 스타일을 수정할 수 있다.
Text(
text = name,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold
)
)
📝 참고 자료
- https://developer.android.com/jetpack/compose/tutorial?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-1%23article-https%3A%2F%2Fdeveloper.android.com%2Fjetpack%2Fcompose%2Ftutorial
- https://developer.android.com/jetpack/compose/mental-model?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-1%23article-https%3A%2F%2Fdeveloper.android.com%2Fjetpack%2Fcompose%2Fmental-model#recomposition
댓글남기기