Compose Basic

박현수·2024년 9월 19일
0

Android Jetpack Compose

목록 보기
1/3
post-thumbnail

급하게 회사 플젝에서 Compose + MVI로 진행하기로 해서 작성하는 기록.

Android Developer의 Codelabs 내용.


JetPack Compose 기초

Compose 시작하기

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    ComposeApplicationTheme {
        Greeting("Android")
    }
}

onCreate() 함수는 앱의 진입점.
다른 함수를 호출하여 사용자 인터페이스를 빌드함.

Greeting() 함수는 구성 가능한 함수.
위에는 @Composable 주석이 있으며, 구성 가능한 함수는 몇가지 입력을 받아 화면에 표시되는 내용을 생성함.

@Preview(showBackground = true, name = "Text Preview")
@Composable
fun GreetingPreview() {
    ComposeApplicationTheme {
        Greeting("Android")
    }
}

이렇게 수정해도 나오는 결과는 아래와 같다.

UI 조정

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

Surface로 감싸 color을 적용하면 아래와 같이 변경된다.

텍스트 색상은 정의하지 않았는데 정의된다.
androidx.compose.material3.Surface 와 같은 Material 구성 요소들은 공통기능(텍스트에 적절한 색상 선택 등)을 처리하여 더 나은 환경을 만들도록 함.

Modifier

Surface 및 Text와 같은 대부분의 요소들은 modifier요소를 선택적으로 허용한다.
modifier는 상위 요소 레이아웃 내에서 UI 요소가 배치되고 표시되고 동작하는 방식을 UI 요소에 알려준다.

아래와 같이 modifier를 사용하여 padding을 줄 수 있다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

결과는 아래와 같다.

컴포저블 재사용

UI에 추가하는 구성요소가 많을 수록 생성되는 중첩 레벨이 더 많아지며, 가독성에 영향을 줄 수 있다.
재사용하도록 만들면 UI요소의 라이브러리를 쉽게 만들 수 있다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeApplicationTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting(name = "Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true, name = "Text Preview")
@Composable
fun GreetingPreview() {
    ComposeApplicationTheme {
        MyApp()
    }
}

이와 같이 사용하게 되면 코드 중복을 피할 수 있으므로 onCreate 콜백과 미리보기를 정리할 수 있게 된다.

열과 행 만들기

Compose의 세 가지 기본 표준 레이아웃 요소는 Column, Row, Box이다.

Column은 세로, Row는 가로, Box는 가로 세로 모두의 형태로 확인할 수 있다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(
            modifier = modifier.padding(24.dp)
        ) {
            Text(text = "Hello")
            Text(text = "$name!")
        }
    }
}

Compose와 Kotlin

구성 가능한 함수는 kotlin의 다른 함수처럼 사용가능하다.
for 루프를 사용하여 Column에 요소를 추가할 수 있다.

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(
        modifier = modifier
    ) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

미리보기에 스마트폰의 일반적인 너비인 320으로 확인하도록.

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    ComposeApplicationTheme {
        MyApp()
    }
}

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(
        modifier = modifier.padding(vertical = 4.dp)
    ) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(
            modifier = modifier.padding(24.dp).fillMaxWidth()
        ) {
            Text(text = "Hello")
            Text(text = "$name!")
        }
    }
}

Button 추가

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    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 = { /*TODO*/ }) {
                Text(text = "Show more")
            }
        }
    }
}

weight는 fillMaxWidth와 중복된다.

Compose에서의 상태

버튼을 클릭하면 화면이 커지고 작아지게 하기 위해선, 항목이 펼쳐진 상태인지를 가리키는 값을 어딘가에 저장해야 한다.
이 값을 state 라고 한다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    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(text = if (expanded) "Show less" else "Show more")
            }
        }
    }
}

boolean으로 값을 저장하고, 클릭 시 작동하지 않는다.
compose에서 이 값을 상태 변경으로 감지 하지 않기 때문에, 아무 일도 일어나지 않는다.

이 변수를 Compose에서 추적하고 있지 않으며, 값은 Greeting이 실행될 때마다 false로 재설정된다.

Composable에 내부 상태를 추가하려면 mutableStateOf 함수를 사용하면 된다.

그렇지만, Composable 내의 변수에 mutableStateOff를 할당하기만 할 수는 없다.
false 값을 가진 변경가능한 새 상태로 재설정하여 호출하게 되면 언제든 recomposition이 일어날 수 있기 때문이다.

이러한 recomposition 간에 상태를 유지하려면 remeber을 사용하여 변경 가능한 상태를 기억해야한다.

remember는 recomposition을 방지하는데 사용되므로, state가 재설정되지 않는다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    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(text = if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

항목 펼치기

extraPadding은 간단한 계산을 수행하므로, recomposition에 대비하여 이 값을 기억할 필요가 없다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 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.value = !expanded.value }) {
                Text(text = if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

상태 호이스팅

구성 가능한 함수에서 여러 함수가 읽거나 수정하는 상태는 공통의 상위 항목에 위치해야 한다.
이 프로세스를 상태 호이스팅이라고 한다. (State Hoisting, Hoisting: 들어올린다 or 끌어 올린다.)

상태를 호이스팅할 수 있게 만들면 상태가 중복되지 않고 버그가 발생하는 것을 방지할 수 있으며, Composable을 재사용할 수 있고 훨씬 쉽게 테스트할 수 있다.
반대로, Composable의 상위 요소에서 제어할 필요가 없는 상태는 호이스팅 되면 안된다.

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Welcome to the Basics Codelab")
        Button(
            onClick = { shouldShowOnboarding = false },
            modifier = Modifier.padding(vertical = 24.dp)
        ) {
            Text(text = "Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun OnboardingPreview() {
    ComposeApplicationTheme {
        OnboardingScreen()
    }
}

OnboardingScreen이라는 새 Composable과 Preview 추가했다.

화면 중앙에 콘텐츠가 표시되도록 Column을 구성한다.

shouldShowOnboarding에 사용된 by는 매번 .value를 입력할 필요가 없도록 해주는 속성 위임이다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeApplicationTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(
    modifier: Modifier = Modifier
) {
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier = modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false})
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    modifier: Modifier = Modifier,
    onContinueClicked: () -> Unit
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Welcome to the Basics Codelab")
        Button(
            onClick = onContinueClicked,
            modifier = Modifier.padding(vertical = 24.dp)
        ) {
            Text(text = "Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(
        modifier = modifier.padding(vertical = 4.dp)
    ) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    var expanded by remember { mutableStateOf(false) }
    val extraPadding = 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(text = if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun OnboardingPreview() {
    ComposeApplicationTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    ComposeApplicationTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun MyAppPreview() {
    ComposeApplicationTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

성능 지연 목록 만들기

스크롤이 가능한 열을 표시하기 위해 LazyColumn을 사용한다.
LazyColumn은 화면에 보이는 항목만 렌더링하므로 항목이 많은 목록을 렌더링할 때 성능이 향상된다.

참고: LazyColumn과 LazyRow는 RecyclerView와 동일하다.

@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은 RecyclerView와 같이 하위 요소를 재사용하지 않는다. -> 속도 훨씬 빠름

상태 유지

온보딩 화면 유지

기기에서 앱을 실행하고 버튼을 클릭한 다음 회전하면 온보딩 화면이 다시 표시된다.
remember함수는 컴포저블이 컴포지션에 유지되는 동안에만 작동된다.
기기회전 시 전체 활동이 다시 시작되므로 모든 상태 손실된다.

-> remember 대신에 rememberSaveable을 사용하면 된다.

목록 항목의 펼쳐진 상태 유지

목록 항목을 펼친 다음 항목이 보이지 않을 때까지 목록을 스크롤할거나, 기기를 회전한 다음 펼쳐진 항목으로 되돌아가면 항목이 초기 상태로 돌아온 것을 확인할 수 있다.

해결방법: rememberSaveable 사용하기

목록에 애니메이션 적용

Compose에서는 여러가지 방법으로 UI에 애니메이션을 지정할 수 있다.

animateDpAsState 컴포저블을 사용한다.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    var expanded by rememberSaveable { mutableStateOf(false) }
    val extraPadding by animateDpAsState(targetValue = 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(text = if (expanded) "Show less" else "Show more")
            }
        }
    }
}

스프링 기반의 애니메이션 추가

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    var expanded by rememberSaveable { mutableStateOf(false) }
    val extraPadding by animateDpAsState(
        targetValue = 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(text = if (expanded) "Show less" else "Show more")
            }
        }
    }
}

앱의 스타일 지정 및 테마 설정

모든 하위 컴포저블에서 MaterialTheme의 세가지 속성인 colorScheme, typography, shapes를 사져올 수 있다.

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }

0개의 댓글