Google Compose Camp에서 배운 내용을 기록합니다!
Compose Camp 진행하면서 실습한 코드는 REPO로!
Pathway 1-1. Compose 기본사항
첫 번째 코드랩에서는 다음과 같은 주제로 Compose 기본사항에 대해 알아본다.
'Hello world!' 텍스트를 display 해보자!
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello world!")
}
}
}
❗ 여기서 잠깐 ❗
: Composable functions은 오직 다른 composable functions으로부터 호출될 수 있다!!
: Jetpack Compose는 Kotlin 컴파일러 플러그인을 사용하여 Composable functions를 앱의 UI element로 변환한다.
EX) Text() == text label을 display
composable function을 만들려면 @Composabe annotation을 추가하면 된다.
// ...
import androidx.compose.runtime.Composable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Android")
}
}
}
@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}
// ...
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}
@Preview
@Composable
fun PreviewMessageCard() {
MessageCard("Android")
}
따라서 MessageCard function은 파라미터가 있으므로 바로 preview할 수 없다.
대신 PreviewMessageCard라는 이름의 두 번째 function을 만들고 적절한 parameter와 MessageCard를 호출하게 한다.
Project를 rebuild 해보면 app 자체는 변화하지 않는다. PreviewMessageCard function은 아무데도 호출되지 않기 때문이다. 하지만 Android Studio는 preview window를 추가한다.
이 window는 @Peview annotion이 표시된 composable functions에 의해 생성되는 UI elements의 preview를 보여준다. 언제든 previews를 업데이트하려면 preview window 창 위쪽의 refesh button을 클릭하면 된다.
UI elements는 계층적(hierarchical)이며, elements들은 다른 elements에 포함될 수 있다. Compose에서는 다른 composable 함수로 부터 composable 함수를 호출해서 UI 계층을 build 할 수 있다.
더 많은 Jetpack Compose의 기능(capabilities)를 알아보기 위해, 우리는 간단한 messaging screen을 만들어 볼 것이다! 근데.. 약간의 애니메이션 확장이 가능한 메세지의 리스트를 곁들인..
먼저 메세지의 author 이름과 message content를 보여주면서 메세지 구성을 풍부하게 만들어봅시다
// ...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard(Message("Android", "Jetpack Compose"))
}
}
}
data class Message(val author: String, val body: String)
@Composable
fun MessageCard(msg: Message) {
Text(text = msg.author)
Text(text = msg.body)
}
@Preview
@Composable
fun PreviewMessageCard() {
MessageCard(
msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!")
)
}
// ...
import androidx.compose.foundation.layout.Column
@Composable
fun MessageCard(msg: Message) {
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
sender의 profile 사진을 추가하여 message card를 보완해보자
// ...
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.res.painterResource
@Composable
fun MessageCard(msg: Message) {
Row {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
)
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
}
❗ 여기서 잠깐 ❗
: Moifiers는 composable의 크기, 레이아웃, 모양(appearance)를 바꾸거나 high-level interactions를 추가할 수 있다(e.g. element가 clickable하게 만드는 거!)
: 이러한 modifiers들을 연결해서 더 복잡한 composables를 만들 수 있다.
// ...
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 40 dp
.size(40.dp)
// Clip image to be shaped as a circle
.clip(CircleShape)
)
// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(text = msg.author)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(4.dp))
Text(text = msg.body)
}
}
}
Compose는 Material Design 원칙을 지원하도록 빌드되었다. 많은 compose UI elements는 Metrial Design을 즉시 사용하도록 구현되었다. Material Design widgets으로 style을 적용해보자!
이제 메시지 디자인에 레이아웃이 생겼지만, 아직 디자인은 그렇게 좋아 보이지 않는다!!
Material Design styling을 사용하여 MessageCard composable의 디자인을 개선해 보겠다!!
// ...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTutorialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
MessageCard(Message("Android", "Jetpack Compose"))
}
}
}
}
}
@Preview
@Composable
fun PreviewMessageCard() {
ComposeTutorialTheme {
Surface {
MessageCard(
msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!")
)
}
}
}
❗ 여기서 잠깐 ❗
: Material Design은 Color, Typography, Shape의 세 가지 핵심 요소를 중심으로 이루어진다.
: Empty Compose Activity template은 MeterialTheme을 customize할 수 있도록 default theme을 생성한다.
: 만약 프로젝트 이름을 ComposeTutorial이 아닌 다른 이름으로 지었다면, ui.theme subpackage 안의 Themme.kt file에서 우리의 custom theme를 찾을 수 있다.
이제 하나씩 추가해보자!
// ...
import androidx.compose.foundation.border
import androidx.compose.material3.MaterialTheme
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.height(4.dp))
Text(text = msg.body)
}
}
}
// ...
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = msg.body,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
Shape를 마지막으로 추가해보자!!
// ...
import androidx.compose.material3.Surface
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
어떻게 Compose로 lists를 쉽게 생성하고 애니메이션을 추가하여 재밌게 만들 수 있는지 배워보자!
Chat에 메세지 하나 이상을 추가하여 대화하는 것으로 만들어보자! 우리는 nultiple messages를 보여줄 수 있는 Conversation function이 필요할 것이다!
이 경우 Compose의 LazyColumn과 LazyRow를 사용해볼 것이다.
이 composable들은 화면에 표시되는 elements들만 rendering하므로, 긴 list에 매우 효율적으로 설계되었다.
// ...
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@Composable
fun Conversation(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
MessageCard(message)
}
}
}
@Preview
@Composable
fun PreviewConversation() {
ComposeTutorialTheme {
Conversation(SampleData.conversationSample)
}
}
이제 애니메이션을 적용해보자!! 우리는 긴 message를 확장하는 기능을 추가하고 콘텐츠 크기와 배경 색상 모두에 애니메이션 효과를 적용하는 기능을 추가해 볼 것이다.
이 로컬 UI 상태를 저장하려면 메시지가 확장되었는지 추적해야 한다. 이 상태 변경을 추적하려면 remember와 mutableStateOf 함수를 사용해야 한다.
❗ 여기서 잠깐 ❗
: Composable 함수는 remember를 사용해서 메모리에 local state를 저장하고 mutableStateOf에 전달 된 값의 변화를 추적한다.
: state를 이용하는 Composables은 값이 업데이트될 때마다 자동으로 그려준다. 이를 recomposition(재구성)이라고 부른다.
: Compose의 상태 API인 remember와 mutableStateOf를 사용함으로서, 어떤 state의 변화를 자동으로 UI로 update한다.
// ...
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTutorialTheme {
Conversation(SampleData.conversationSample)
}
}
}
}
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Surface(
shape = MaterialTheme.shapes.medium,
shadowElevation = 1.dp,
) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// surfaceColor will be updated gradually from one color to the other
val surfaceColor by animateColorAsState(
if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
)
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Surface(
shape = MaterialTheme.shapes.medium,
shadowElevation = 1.dp,
// surfaceColor color will be changing gradually from primary to surface
color = surfaceColor,
// animateContentSize will change the Surface size gradually
modifier = Modifier.animateContentSize().padding(1.dp)
) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
번역본이 있긴 하지만!! 찬찬히 기록하며 복습하려고 원문 보면서 기록했다!! 끄ㅡ으으읕!!!