이 포스트는 안드로이드 공식 Codelab 내용을 기반으로 작성되었습니다.
위와 같이 애니매이션으로 항목이 펼쳐지늠 목록과 온보딩 화면이 포함된 앱을 만드는 것이 이번 포스트의 목표이다.
[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 에서 솔루션 코드를 클로닝해올 수 있다.
먼저 Greeting
에 다른 배경 색상을 설정해보자. Text
컴포저블을 Surface
로 래핑하면된다. 이때 Surface
는 MaterialTheme.colorScheme.primary
색상을 사용한다.
@Composable
private fun Greeting(name: String) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text (text = "Hello $name!")
}
}
"Hello Android!" Text
컴포저블의 배경색이 바뀌어서 적용된 것을 확인할 수 있다.
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 만큼 적용된 것을 확인할 수 있다.
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 함수를 재사용하여 코드 중복을 피할 수 있다.
...
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()
}
}
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
의 위치도 변경해주었다.
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")
}
}
}
}
위 코드에서 사용된 ElevatedButton
은 Material Design
버튼 사양의 하나이다.
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")
}
}
}
}