[Compose] 튜토리얼

6 분 소요

해당 게시글은 jetpack compose tutorial 공식 페이지를 통해 학습한 내용을 정리한 글입니다.


🙋‍♀️ Jetpack Compose: 선언형 UI 프레임워크

  • 더 적은 수의 코드, 강력한 도구, 직관적인 Kotlin API로 안드로이드에서의 UI 개발을 간소화하고 가속화한다.
  • kotlin 컴파일러 플러그인을 사용해 구성 가능한 함수앱의 ui 요소로 반환한다.
  • 기존 view와의 호환성으로 MapView처럼 아직 Compose로 구축되지 않은 콘텐츠를 표시할 때 유용하다.

    🌟🌟State -> UI🌟🌟

    • 상태UI로 변경하기 때문에 모델과 UI 상태를 동기화하는 데 편리하다.
    • UI는 변경할 수 없고, 한 번 생성하면 업데이트가 불가능하다.
      • 앱의 상태가 바뀌면 새로운 상태를 새로운 표현으로 변환한다.
      • UI를 재생성하는 것이다.
      • 변경되지 않은 요소에 대한 작업은 건너뛴다.


🙋‍♀️ 구성 가능한 함수: Composable

  • jetpack compose는 composable 함수를 중심으로 빌드된다.

    ui의 구성 과정(요소 초기화, 상위 요소에 연결 등)에 집중하기보다는 앱 모양을 설명하고 데이터 종속 항목을 제공하여 프로그래매틱 방식으로 앱의 ui를 정의할 수 있다.

    ⌨️ 1. composable 함수 생성

    • @Composable 주석을 함수 이름 위에 추가한다.
      • 컴포즈 컴파일러가 해당 함수가 데이터를 ui로 변환하기 위한 함수라는 것을 인식한다.
      • 해당 함수는 원하는 화면 상태를 설명하므로 아무것도 반환할 필요가 없다.
      @Composable
      fun MessageCard(name: String){
        Text(text = "Hello $name!")
      }
      

    ⌨️ 2. composable 함수의 특징

    • 컴포저블 함수는 다른 컴포저블 함수에서만 호출할 수 있다.
      • setContent는 컴포저블 함수가 호출되는 활동의 레이아웃을 정의한다.
        setContent {
        MessageCard("Android")
        }
        
    • 모든 컴포저블은 새 데이터가 입력될 때마다 ⭐리컴포저블(재구성)⭐된다.
      • onClick 같은 이벤트가 발생해 앱로직에 전달하여 앱의 상태가 변경된다.
      • compose 프레임워크는 변경된 구성요소만 지능적으로 재구성할 수 있다.
    • 정보를 전달할 때는 무조건 매개변수로 컴포저블에 전달해야 한다.
    • Kotlin으로 작성되기 때문에 동적(반복문 등)으로 콘텐츠를 생성할 수 있다.
      @Composable
      fun Greeting(names: List<String>) {
          for (name in names) {
              Text("Hello $name")
          }
      }
      

    ⌨️ 3. composable 함수 미리보기

    • @Preview 주석을 사용해 앱을 빌드해 기기, 애뮬레이터 설치 없이 구성 가능한 함수를 미리 볼 수 있다.
    • 단, 매개변수를 사용하지 않는 구성 가능한 함수에 사용할 수 있다.
    @Composable
    fun MessageCard(name: String){
      Text(text = "Hello $name!")
    }
    
    @Preview
    @Composable
    fun PreviewMessageCard(){ // 매개변수 없음
      MessageCard("Android")
    }
    


🙋‍♀️ compose에서의 레이아웃 생성

  • ui 요소의 계층 구조는 다른 구성 가능한 함수로부터 구성 가능한 함수를 호출하여 빌드한다.

    예를 들어 메시지 목록이 포함된 메시지 화면을 빌드하고자 할 때 우리는 다음과 같이 화면을 구성할 수 있다.

    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!")
      )
    }
    
    • MessageCard 컴포저블 내에 다른 Text 컴포저블을 추가해 레이아웃을 구성한다.
    • 이렇게 할 경우, 정렬에 대한 정보가 제공되지 않았기 때문에 author, body가 겹치게 표시된다.
      • 따라서 Column/Row/Box 함수를 사용해 요소들을 수직/수평/쌓아서(겹겹) 정렬할 수 있다.
        @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)
                }
                
            }
                
        }      
        


🙋‍♀️ 수정자: Modifier

  • 수정자를 통해서 상위 요소 레이아웃 내에서 ui 요소가 배치되고, 표시되고, 동작하는 방식을 ui 요소에 지정할 수 있다.
    • composable의 크기, 레이아웃 모양 변경, 요소 클릭 여부 등의 상위 수준 상호작용을 추가할 수 있다.
  • 상위 요소 modifier를 사용하며 하위 요소에서 추가 방식을 지정할 수 있다.
    @Composable
    fun OnboardingScreen(
      onContinueClicked: () -> Unit,
      modifier: Modifier = Modifier
      ) {
      Column(
          modifier = modifier.fillMaxSize(), //상위 modifier를 가져와 추가 방식 지정
          verticalArrangement = Arrangement.Center,
          horizontalAlignment = Alignment.CenterHorizontally
      ) {
          Text(text = "Welcome to the Basics Codelab!")
          Button(
              modifier = Modifier.padding(vertical = 24.dp), //새로운 Modifier 지정
              onClick = onContinueClicked
          ) {
              Text(text = "Continue")
          }
      }
    }
    


🙋‍♀️ Material Design 사용

  • Compose는 Material Design 원칙을 지원하도록 빌드되었다.

    ⌨️ Material Design의 세 핵심 요소

    • color
    • typography
    • shape

    ⌨️ Material Design: color

    • MaterialTheme.colorScheme을 사용해 래핑된 요소의 색상을 지정할 수 있다.
             Column {
             Text(
                 text = msg.author,
                 color = MaterialTheme.colorScheme.secondary // 색상으로 스타일 지정
             )
      
             Spacer(modifier = Modifier.height(4.dp))
             Text(text = msg.body)
         }
      

    ⌨️ Material Design: typography

    • MaterialTheme.typography를 사용해 래핑된 요소의 서체를 지정할 수 있다.
             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 // 서체 지정
             )
         }
      

    ⌨️ Material Design: shape

    • MaterialTheme.shapes를 사용해 래핑된 요소의 모양을 지정할 수 있다.
                //Surface의 모양 지정
                 Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
                 Text(
                     text = msg.body,
                     modifier = Modifier.padding(all = 4.dp),
                     style = MaterialTheme.typography.body2
                 )
             }
      

⌨️ + 어두운 테마 사용

  • Material Design 지원 덕분에 compose는 기본적으로 어두운 테마를 처리할 수 있다.
@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES, // dark mode 미리보기
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
   ComposeTutorialTheme {
    Surface {
      MessageCard(
        msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}
  • 하나의 구성 가능한 함수에 대해 여러 주석을 추가하였다. 위에서는 하나의 구성 가능한 함수에 대해 두 개의 Preview가 생성된다.


🙋‍♀️ 목록 및 애니메이션

  • LazyColumnLazyRow는 컴포즈에서의 RecyclerView에 해당한다.
    • 화면에 표시되는 요소만 렌더링하므로 긴 목록을 매우 효율적으로 설계할 수 있다.
    • 하위 요소로 items가 있어 리스트를 매개변수로 받게 되고, 람다를 통해 원하는 작업을 항목마다 적용할 수 있다.
    @Composable
    fun Conversation(messages: List<Message>) {
        LazyColumn{
            items(messages) { message ->
                MessageCard(msg = message)
            }
        }
    }
    


🙋‍♀️ ⭐상태 변경 추적⭐

  • compose는 기본적으로 state를 따라 ui를 선언적으로 그리는 ui 툴킷이다.
  • 상태가 변경되면 composable은 재구성이 되므로 변경되는 상태를 잘 추적해야 한다.
  • 상태 변경을 추적하기 위해 remember, mutableStateOf와 같은 Compose의 상태 API를 사용한다.

    ⌨️ remember

    • remember의 경우 컴포저블이므로 다른 컴포저블 안에 존재해야 한다.
    • 컴포저블을 다시 실행하거나 리컴포지션을 호출하더라도 유지하고 싶은 값이 있을 경우 remember를 사용한다.
      • 이전 실행에서 얻은 값기억할 수 있게 된다.

    ⌨️ remember와 mutableStateOf

    • 구성 가능한 함수는 remember를 사용하여 메모리에 로컬 상태를 저장하고 mutableStateOf에 전달된 값의 변경사항을 추적할 수 있다.
    • 이 상태를 사용하는 컴포저블 및 하위 요소는 값이 업데이트되면 자동으로 다시 그려진다.(재구성)
    @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
                    )
                }
            }
        }
    }
    
    • 대화 상자 클릭 여부의 상태 변경을 추적해 그에 따라 text와 maxLine을 달리한다.
    • animateColorAsState 함수를 통해 isExpanded 상태에 따라 배경 색을 달리한다.
    • animateContentSize를 통해 자식 사이즈에 따라 Surface의 사이즈를 달리한다.


🙋‍♀️ Compose에서 프로그맹할 때 알아야 할 사항들

☝️ 구성 가능한 함수는 순서와 관계없이 실행할 수 있습니다.

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}
  • StartScreen이 일부 전역 변수를 설정하고, MiddleScreen()이 해당 변경사항을 활용하도록 할 수 없다.
  • 이러한 각 함수는 독립적이어야 한다.

✌️ 컴포저블 함수는 동시에 실행할 수 있다.

  • 컴포저블 함수를 동시에 실행하여 재구성을 최적화할 수 있다.
  • 다중 코어를 활용한다.
  • 백그라운드 스레드 풀 내에서 커포저블 함수가 실행될 수 있음을 의미한다.
    @Composable
    @Deprecated("Example with bug")
    fun ListWithBug(myList: List<String>) {
      var items = 0
    
      Row(horizontalArrangement = Arrangement.SpaceBetween) {
          Column {
              for (item in myList) {
                  Text("Item: $item")
                  items++ // Avoid! Side-effect of the column recomposing.
              }
          }
          Text("Count: $items")
      }
    }
    
  • 함수가 로컬 변수를 쓰는 경우 이 코드는 스레드로부터 안전하지 않거나 적절하지 않다.
  • items는 모든 재구성에서 수정된다. 애니메이션의 모든 프레임에서 또는 목록이 업데이트될 때 수정되면서 UI에 잘못된 개수가 표시될 수 있다. 따라서 이와 같은 쓰기는 Compose에서 지원되지 않는다.

👌 재구성은 최대한 많은 수의 컴포저블 함수 및 람다를 건너뛴다.

✋ 재구성은 낙관적이며 취소될 수 있다.

  • 컴포즈는 컴포저블의 매개변수가 변경되었을 수 있다고 생각할 때마다 재구성이 시작된다.
  • 즉, 컴포즈는 매개변수가 다시 변경되기 전에 재구성을 완료할 것으로 예상한다.
  • 만약 재구성 완료 이전에 매개젼수가 변경되면 컴포즈는 재구성을 취소하고 새 매개변수를 사용해 재구성을 다시 시작할 수 있다.

🤚 컴포저블 함수는 애니메이션의 모든 프레임에서와 같은 빈도로 매우 자주 실행될 수 있다.

  • 구성 가능한 함수에 데이터가 필요하다면 데이터의 매개변수를 정의해야 한다.
  • 비용이 많이 드는 작업을 구성 외부의 다른 스레드로 이동하고 mutableStateOf 또는 LiveData를 사용하여 Compose에 데이터를 전달할 수 있다.


📝 참고 자료

댓글남기기