[Android] Jetpack Compose - 3. Compose 기초 앱 만들어보기 (1)

문승연·2023년 9월 4일
0
post-thumbnail

이 포스트는 안드로이드 공식 Codelab 내용을 기반으로 작성되었습니다.

오늘의 목표 결과물

위와 같이 애니매이션으로 항목이 펼쳐지늠 목록과 온보딩 화면이 포함된 앱을 만드는 것이 이번 포스트의 목표이다.

1. 새 프로젝트 생성


[File] -> [New] -> [New Project] 로 이동한 후 [Empty Compose Activity] 를 선택한다. 안드로이드 최신 버전일 경우 Compose 세팅이 디폴트로 설정되어있기 때문에 [Empty Activity] 로 선택해도 [Empty Compose Activity] 와 같다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

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

프로젝트를 새로 생성하면 MainActivity 에 Compose로 "Hello, Android" 를 출력하는 코드가 작성되어 있는 것을 확인할 수 있다.

기본 프로젝트를 실행하면 위와 같은 결과를 볼 수 있다.

솔루션 코드 다운로드

git clone https://github.com/android/codelab-android-compose

위와 같이 GitHub 에서 솔루션 코드를 클로닝해올 수 있다.

3. UI 조정

먼저 Greeting 에 다른 배경 색상을 설정해보자. Text 컴포저블을 Surface로 래핑하면된다. 이때 SurfaceMaterialTheme.colorScheme.primary 색상을 사용한다.

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

"Hello Android!" Text 컴포저블의 배경색이 바뀌어서 적용된 것을 확인할 수 있다.

수정자 (modifier)

modifier 매개변수를 이용하여 padding을 적용할 수 있다.

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

Text 컴보저블에 padding 이 24dp 만큼 적용된 것을 확인할 수 있다.

4. Composable 재사용

UI에 추가하는 구성요소가 많아질수록 생성되는 중첩 레벨이 필연적으로 더 많아진다. 이런 과정이 반복되면 함수가 복잡해지게되고 가독성에 영향을 준다.

재사용할 수 있는 구성요소를 만들면 앱에서 사용하는 UI 요소의 라이브러리를 쉽게 만들 수 있고 각 요소는 화면의 작은 부분을 담당하여 독립적으로 수정할 수 있게 된다.

함수는 기본적으로 빈 modifier 가 할당되는 modifier 매개변수를 포함하는 것이 바람직하다. 이렇게 하면 호출 사이트가 Composable 함수 외부에서 레이아웃 동작을 조정할 수 있다.

인사말이 포함된 MyApp 이라는 Composable을 생성한다.

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

이제 이 MyApp Composable 함수를 재사용하여 코드 중복을 피할 수 있다.

MainActivity.kt

...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
private 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)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

5. 열과 행 만들기 (Column, Row, Box)

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

위 셋 모두 Composable 함수이므로 같은 Composable 함수 내부에 배치할 수 있다.

이 중 Column 을 사용하여 레이아웃을 변경해보자.

@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!")
        }
    }
}

Column 이 추가되면서 padding 의 위치도 변경해주었다.

Compose와 Kotlin

Composable 함수는 Kotlin의 다른 함수처럼 사용할 수도 있다. 이는 UI가 표시되는 방식에 영향을 주는 구문을 추가할 수 있으므로 매우 강력한 UI를 제작하는데 도움을 준다.

예를 들어, for 루프를 사용하여 Column 에 요소를 추가할 수 있다.

@Composable
private fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Column(modifier) {
            for (name in names) {
                Greeting(name = name)
            }
        }
    }
}

Greeting 함수의 각 Text 에 대해 크기 제약사항을 추가하지 않았기 때문에 각 행이 최소한의 공간만 차지하고 있다.

기본 크기를 320dp로 설정하고 각 Column이 너비를 전부 차지하도록 설정해보자

@Composable
private 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) {
    Surface (
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello,")
            Text(text = "$name!")
        }
    }
}

버튼 추가

다음 단계에서는 Greeting 에 누르면 펼쳐지는 기능을 포함하는 Button을 추가하도록 하자.

아래와 같은 레이아웃을 구현하는 것이 목표이다.

@Composable
fun Greeting(name: String) {
    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("Show more")
            }
        }

    }
}

위 코드에서 사용된 ElevatedButtonMaterial Design 버튼 사양의 하나이다.

6. Compose에서의 상태

Button 을 추가했으니 이번에는 Button 을 통한 상호작용을 추가해보도록 하자.

위 화면처럼 버튼이 클릭할 때마다 변하는 레이아웃을 구현하기 위해서는 먼저 각 항목이 펼쳐진 상태인지 가리키는 값을 어딘가에 저장해야한다. 이를 상태 라고 부른다.

버튼은 각 Greeting 컴포저블에 존재하므로 해당 상태Greeting 에 위치해야한다.

@Composable
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")
            }
        }

    }
}

Greeting 함수에 expanded Boolean 값을 추가하고 버튼이 클릭될때마다 true, false 값이 바뀌도록 구현했다. 하지만 위 코드는 정상적으로 동작하지 않는다.

Compose에서 이 expanded 값을 상태 변경으로 감지하지 않기 때문이다.

위처럼 특정 변수가 변경될때 UI가 바뀌는 것을 재구성(Recomposition)이라고 하는데 재구성을 위해서는 Compose가 해당 변수를 추적하도록 해야한다.

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

val expanded = mutableStateOf(false)

하지만 위 코드는 컴파일 에러를 호출한다. 이유는 컴포저블 내 변수에 mutableStateOf 를 할당하기만 할 수는 없기 때문이다.

재구성(Recomposition)이 일어날 때 상태를 유지하려면 remember 를 사용하여 변경된 상태를 기억 해야한다.

val expanded = remember {
    mutableStateOf(false)
}

상태 변경 및 상태 변경사항에 반응

ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

이 단계까지 진행했을 때 레이아웃은 아래와 같다.

항목 펼치기

이제 실제로 버튼 상호작용이 일어났을 때 텍스트 변경 뿐만 아니라 항목을 펼쳐보도록 하자.

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

    }
}

profile
"비몽(Bemong)"이라는 앱을 개발 및 운영 중인 안드로이드 개발자입니다.

0개의 댓글