Start Compose
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
BasicsCodelabTheme {
Greeting("Android")
}
}
- setContent 내에서 사용되는 앱 테마는 프로젝트 이름에 따라 지정된다.
- setContent 안에 XML 대신 레이아웃을 구성할 Composable 함수가 들어갈 수 있다.
- @Composable을 사용하면 Composable function으로 만들 수 있다.
- TextView 대신 사용할 수 있는 Text 같은 Composable function을 넣을 수 있다.
- showBackground : 기본 배경색 없을 때 흰색으로 채움
Tweaking th UI
@Composable
fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Text(text = "Hello $name!")
}
}
- surface는 뷰 그룹의 역할을 한다.
- Text는 Surface 내부에 중첩되어 있는데, 중첩될 때 백그라운드 색상이 먼저 화면에 그려진다.
- 결과를 보면 텍스트(Hello Android)의 색이 따로 지정해 주지 않았는데 흰색으로 변경되어 있다.
- 이유는 Surface는 텍스트에 적절한 색상을 선택하는 것과 같이 더 나은 경험을 제공하도록 제작되었기 때문이다.
Modifiers
@Composable
fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
}
}
- Surface 및 Text와 같은 대부분의 Compose UI 요소는 선택적으로 modifier 매개변수를 허용한다.
- modifier는 UI 요소에서 화면에 어느 위치에 배치할 것인지를 의미한다.
Reusing Composables
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
BasicsCodelabTheme {
MyApp()
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
BasicsCodelabTheme {
MyApp()
}
}
@Composable
private fun MyApp() {
Surface(color = MaterialTheme.colors.background) {
Greeting(name = "Android")
}
}
- UI에 구성 요소를 추가할수록 중첩이 쌓인다.
- 재사용 가능한 요소를 만들어 사용하는 것이 좋다.
- 예제에서 MyApp() Composable을 재사용하여 코드 중복을 방지할 수 있다.
- Composable을 다른 액티비티에서도 사용하도록 하려면 top level 함수로 작성하는 것이 좋다.
- 현재는 MyApp()에서 Greeting() 함수를 포함하기 때문에 재사용의 취지에 적합하지 않다.
Creating Columns and Rows
@Composable
fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = "Hello $name!")
Text(text = name)
}
}
}
- Column, Row, Box를 사용해서 배열을 정할 수 있다.
- Column은 Linear Layout의 vertical, Row는 Linear Layout의 horizontal, Box는 Frame Layout과 비슷하다.
@Composable
private fun MyApp(names: List<String> = listOf("World", "Compose")) {
Column {
for (name in names) {
Greeting(name = name)
}
}
}
- Composable 함수는 코틀린 함수처럼 호출할 수 있다.
@Composable
fun Greeting(name: String) {
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Column(modifier = Modifier
.fillMaxWidth()
.padding(24.dp)) {
Text(text = "Hello $name!")
Text(text = name)
}
}
}
@Composable
private fun MyApp(names: List<String> = listOf("World", "Compose")) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}
- modifier를 사용하여 구성 요소의 크기를 지정할 수 있다.
@Composable
fun Greeting(name: String) {
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(
modifier = Modifier
.weight(1f)
) {
Text(text = "Hello $name!")
Text(text = name)
}
OutlinedButton(onClick = { /*TODO*/ }) {
Text(text = "Show more")
}
}
}
}
- 컴포즈는 Material Desing Button의 spec에 따라 여러 타입의 버튼을 제공한다. (Button, OutlinedButton, TextButton)
- 예제에서는 Text를 감싸는 OutlinedButton을 사용했다.
State in Compose
- 사용자 변경 사항에 반응하는 상호 작용 요소를 추가해보자.
- Compose는 데이터가 변경될 때마다 Composable 함수를 호출해 새로운 데이터를 UI에 반영한다.
- 코틀린 컴파일러 플러그인을 이용해 내부적으로 데이터가 변경되었을 때 Compose는 변경사항을 옵저빙 하여 업데이트한다.
@Composable
fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
Surface(
color = MaterialTheme.colors.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 $name!")
Text(text = name)
}
OutlinedButton(onClick = {
expanded.value = !expanded.value
}) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
- 버튼을 클릭 가능하게 만드는 방법을 추가하기 전에 각 항목이 확장되었는지 여부를 나타내는 값(상태)을 저장해야 한다.
- mutableStateOf 함수를 사용해 Compose가 해당 상태를 재구성을 추적할 수 있도록 구성해야 한다.
- State 및 MutableState는 일부 값을 보유하고 해당 값이 변경될 때마다 UI 업데이트(재구성)를 트리거하는 인터페이스이다.
- 또한, 재구성에서 상태를 유지하려면 remember 함수를 사용한다.
- state에 의존하고 간단한 계산을 수행하기 때문에 extraPadding에 remember를 사용하지 않아도 된다.
State hoisting
- Composable 함수에서 읽거나 수정한 상태는 공통 조상에 있어야 한다. 이를 hoisting이라 부른다.
- state hoisting은 Composable 함수가 재호출 될 때 재구성의 여부를 결정하는 상태를 함수 내부가 아닌 외부로 노출시키기 위함이다.
- Composable 상태를 저장하지 않는 방식으로 만들기 위해 호출자에게 상태를 이동시킨다.
- 상태를 hoist 가능하게 만들면 상태 복제 및 버그 도입을 방지하고 Composable 재사용에 용이하다.
- 반대로 Composable의 부모에 의해 제어될 필요가 없는 상태는 hoisting 돼서는 안 된다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
BasicsCodelabTheme {
MyApp()
}
}
}
}
}
@Composable
fun MyApp() {
// shouldShowOnboarding으로 state hoisting을 한다.
var shouldShowOnboarding by remember { mutableStateOf(true) }
if (shouldShowOnboarding) {
// 버튼을 클릭했을 때 shouldShowOnboarding의 값을 false로 바꿔준다.
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")
}
}
}
}
@Composable
private fun Greetings(names: List<String> = listOf("World", "Compose")) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {})
}
}
@Composable
private fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
Surface(
color = MaterialTheme.colors.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)
}
OutlinedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
BasicsCodelabTheme {
Greetings()
}
}
Creating a Performant lazy list
- 이전 예제에서 2 개의 텍스트를 생성하여 보여주었다. 하지만, 1000 개의 텍스트를 생성한다면 성능에 좋지 않다.
- LazyColumn, LazyRow를 사용해 개선하자. 이것은 RecyclerView와 동일한 역할을 한다.
- 리사이클 러뷰는 성능을 위해 스크롤 시 화면에서 사라지는 뷰를 재활용하는데, LazyColumn은 재활용하지 않는다.
- 그럼에도 불구하고 성능이 좋다. Composable은 안드로이드 뷰에 비해 비용이 저렴하기 때문이다.
@Composable
private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}
Persisting State
Animating your list
- Compose에는 애니메이션 효과를 줄 수 있는 다양한 방법이 있다.
- 예제에서는 animateDpAsState(애니메이션이 끝날 때까지 계속해서 값이 업데이트되는 State 객체를 반환)와 animationSpec(animateDpAsState에서 선택적으로 사용할 수 있는 매개변수)을 사용했다.
- spring은 애니메이션을 보다 자연스럽게 만들기 위한 속성이다.
@Composable
private fun Greeting(name: String) {
val expanded = rememberSaveable { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded.value) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Surface(
color = MaterialTheme.colors.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)
}
OutlinedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
Styling and theming your app
- MaterialTheme은 Material 디자인 스타일링 원칙을 반영해 여러 스타일을 제공한다.