ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Jetpack Compose
    카테고리 없음 2021. 1. 31. 20:48
    반응형

    오늘은 Jetpack 라이브러리  

    Jetpack 

    - WorkManager

    - Room

    - Compose 

     

    Compose 대해서 알아보자.

     

    Jetpack Compose 안드로이드 UI 만드는 최신 툴킷이고

    • 코틀린 사용 (코틀린 API 사용) 
    • 적은 양의 코드 (XML, UI 위젯 사용하지 않아도 ) 
    • 좋은 도구를 제공 

    의 특징을 가진다. 

     

    , 현재 알파 버전이다. (Compose 1.0 is expected in 2021.)

     

    Jetpack Compose 함수를 호출해서 원하는 요소를 사용하면 Compose 컴파일러에서 나머지 작업을 완료한다.

    Jetpack Compose는 @Composable 어노테이션을 사용해서 UI를 작성하는데, 이 @Composable이 표시되어 있는 Composable한 함수들을 사용하여 앱을 만들어보자. 

    새로운 UI를 작성하기 위해서는 함수를 새로 작성해야한다. 위 어노테이션은 Compose에게 UI를 수정해 달라고 명령하는 것이다. Compose는 작성한 코드를 작은 단위로 만드는 역할을 한다. 이렇게 작성된 Composable한 함수들은 "composables" 라고 줄여 말한다. 

    재사용이 가능한 composables 을 만드는 것은 우리가 만들 앱이 사용할 UI 요소들을 가지는 라이브러리를 만드는 것을 의미한다. 각각의 composable 들은 화면 안에서 독립적으로 편집이 가능한 개체가 된다. 

    아래 튜토리얼에서는 "UI Components", "Composable functions", "composables"는 모두 같은 의미로 사용된다. 

     

    튜토리얼

    아래 튜토리얼을 따라가보자. 

    developer.android.com/codelabs/jetpack-compose-basics/#0

    https://github.com/googlecodelabs/android-compose-codelabs

     

    이 튜토리얼을 끝내면 아래 목표를 달성할 것이다. 

    • Compose가 무엇인지
    • Compose로 UI 작성하기
    • Composable 함수 상태관리하기

     

    준비물

    Android studio - 2020.3.1 Canary 5 

     

    새 프로젝트 만들기

    Empty Compose Activity 로 작성한다. (단, minSdkVersion은 21 이상 필수)

    처음 자동으로 만들어지는 파일 중 봐야할 부분은 app/build.gradle 파일이다. Compose의 의존성이 추가와 buildFeagtures { compose true } 플래그로 Android studio가 Compose를 사용하도록 되어있다. 

    android {
        ...
        kotlinOptions {
            jvmTarget = '1.8'
            useIR = true
        }
        buildFeatures {
            compose true
        }
        composeOptions {
            kotlinCompilerExtensionVersion compose_version
            kotlinCompilerVersion '1.4.21'
        }
    }
    
    dependencies {
        ...
        implementation "androidx.compose.ui:ui:$compose_version"
        implementation "androidx.compose.material:material:$compose_version"
        implementation "androidx.compose.ui:ui-tooling:$compose_version"
        ...
    }

    kotlinOptions.useIR : JVM IR(Intermediate Representation) backend 기능을 활성화한다.
    Jetpack Compose 컴파일러는 코틀린 컴파일러의 extension이다. Jetpack Compose 컴파일러가 파일을 컴파일 해서 코틀린 컴파일러 파이프라인에 빠르게 전달하는데 필요하다.
    그런데 Jetpack Compose를 사용하겠다고 build option을 활성화했으면 디폴트로 IR은 활성화되므로 따로 작성하지 않아도 된다. 

    root/build.gradle 에서 compose_version1.0.0-alpha11 으로 변경한다. (현재 alpha11까지 나왔음)

    buildscript {
        ext {
            compose_version = '1.0.0-alpha11'
        }
        ...
    }

    프로젝트 안에 theme 폴더가 미리 생성되어 있다. (자세한 내용은 맨 아래 테마부분 참고)

    MainActivity.kt 를 열어보면 아래 코드가 미리 생성되어있다.

    class MainActivity : AppCompatActivity() {
        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 안에 작성된 테마는 프로젝트의 이름이 어떻게 되어있는지에 따라 달라진다. 이 튜토리얼에서는 프로젝트의 이름이 BasicsCodelab로 되어있다고 가정한다. 그러므로 위의 코드를 사용할거면 BasicCodelabTheme ui/Theme.kt 파일의 테마 이름을 본인 프로젝트 이름인지 확인해야한다. 

     


     

    Compose를 사용해보자

    만들어진 템플릿에서 다른 클래스와 메소드들을 사용해보자.

    Composable 함수들

    Composable 함수는 @Composable 어노테이션을 가진 일반 함수이다. @Composable 이 있는 함수는 다른 함수 안에서 호출할 수도 있다. 또한 @Composable 어노테이션이 있는 코틀린 함수이다. 

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

    위의 Greeting 함수는 @Composable 어노테이션을 가진다. 이 함수는 화면에 String 을 표시하는 UI 계층의 조각이 된다. Text 는 Compose 라이브러리에서 제공하는 Composable 함수이다. 

     

    @Preview 어노테이션

    Android studio 프리뷰 기능을 사용하기 위해서는 파라미터가 없는 Composable 함수에 @Preview 어노테이션을 붙여야한다. 

    @Preview(showBackground = true, name = "DefaultPreview")
    @Composable
    fun DefaultPreview() {
        BasicsCodelabTheme {
            Greeting(name = "Android")
        }
    }
    • showBackground : 기본 배경색 없을 때 흰색으로 채워줌
    • name : Preview 상단에 뜨는 이름 

     

    파라미터가 있는 Composable 함수에 @Preview 어노테이션을 붙이면 에러가 발생한다. 


    이 프로젝트에서 Jetpack Compose와 연관된 클래스는 다음과 같다.

    • androidx.compose.* for compiler and runtime classes
    • androidx.compose.ui.* for UI toolkit and libraries

     

    MainActivity에서 Composable 함수를 이용해 Text를 작성하려면 setContent에서 XML을 이용하는 대신 Composable 함수를 호출한다. 

    class MainActivity : AppCompatActivity() {
        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")
                    }
                }
            }
        }
    }

     


     

    Declarative UI

    Declarative 의미

    더보기

    "Imperative(명령형) 프로그래밍 vs Declarative(선언형) 프로그래밍" 이냐 들어본 적이 있을 것이다. "Imperative 프로그래밍은 how를 설명하는 것이고 Declare 프로그래밍은 what을 설명하는 것이다" 라는 말도 들어본 적이 있을 것이다.  

    사전적 의미는 다음과 같다.

    Imperative (형용사) : 명령을 나타내는 
    Declare (동사) : 선언하다, 명확히 말하다. 

    일상생활에서 예를 들어보자.

    슈퍼마켓에서 집까지 가는 방법에 대한 대답?

    Imperative response : 슈퍼마켓 북쪽 출구로 나와서 주차장으로 가서 서현로를 지나 ..
    Declare response : 경기도 성남시 황새울로 333-333

    프로그래밍 코드로 보면 다음과 같다.

    // Imperative
    import storeAPI from 'some-store-api';
    
    const shoppingList = [
      {name: "bread", bought: true},
      {name: "eggs", bought: false},
      {name: "milk", bought: false},
      {name: "brownie mix", bought: true}
    ];
    
    let itemsToBuy = [];
    for (const item of shoppingList) {
      if (!item.bought) itemsToBuy.push(item);
    }
    
    let totalCost = 0;
    for (const item of shoppingList) {
      totalCost += storeAPI.getPrice(item.name);
    }
    
    // Declarative, (more) functional
    import storeAPI from 'some-store-api';
    
    const shoppingList = [
      {name: "bread", bought: true},
      {name: "eggs", bought: false},
      {name: "milk", bought: false},
      {name: "brownie mix", bought: true}
    ];
    
    const itemsToBuy = shoppingList.filter(item => !item.bought);
    
    const totalCost = shoppingList.map(
      item => storeAPI.getPrice(item.name)
    ).reduce(
      (total, itemPrice) => total + itemPrice
    );

     UI에 적용해보면,

    // Imperative Java UI
    JPanel main = new JPanel();
    JPanel toolbar = new JPanel();
    JButton button = new JButton();
    button.setIcon(…);
    button.setLabel(“Cut”);
    toolbar.add(button);
    main.add(toolbar);
    JTextArea editor = new JTextArea();
    main.add(editor);
    
    // Declarative HTML 
    <div id=“main”>
    <div id=“toolbar”>
    <button>
    <img src=“cut.png”></img>
    Cut
    </button>
    </div>
    <textarea id=“editor”></textarea>
    </div>

    즉, Declarative UI는 명확한 선언들로 작성된 UI이다. 

    Android에서 예를 들면,
    이전 XML을 이용할 때는 RecyclerView 하나를 작성하기 위해서 ViewHolder, RecyeclerView, Adapter, ... 줄줄이..
    Compose에서는 LazyColumn, Text 면 된다. 

     

    Greeting 함수에 background 색상을 변경하려면 Surface를 사용한다. 

    @Composable
    fun Greeting(name: String) {
        Surface(color = Color.Yellow) {
            Text(text = "Hello $name!")
        }
    }

    Text 컴포넌트는 Surface에 중첩되어 있는데 이 의미는 background 색상이 먼저 화면에 그려진다는 의미다. 

    새로운 코드가 작성되면 아래 버튼을 누르면, 

    새로운 변경사항을 프리뷰에서 확인할 수 있다.

     

    Modifier

    Surface와 Text와 같은 UI 컴포넌트는 Modifier 라는 파라미터를 옵셔널하게 받을 수 있다. Modifier 파라미터는 화면에 어느 위치에 배치할 것인지를 의미하고 코틀린 Object 클래스이다. 

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

    위 같이 padding modifier를 적용하면 Text 컴포넌트에 패딩값을 넣을 수 있다. 

     

    Compose reusability

    UI에 컴포넌트를 더 추가하려고 한다면 더 많은 중첩을 만들게 된다. 그렇게 되면 가독성에 영향을 끼칠 수 있다.

    그래서 재사용이 가능한 작은 컴포넌트들로 쪼개서 이를 조합해 우리 프로젝트가 사용하는 간단한 라이브러리를 구축하는 것이 좋다. 이렇게 되면 각각은 화면의 작은 부분을 담당하며 독립적으로 편집할 수도 있다.

    @Composable 어노테이션은 UI로 만들거나 다른 Composable한 함수가 호출할 때 필요하다. 따라서 일반 함수나 Composable한 함수에서 호출할 수 있고 @Composable 어노테이션은 남발되어선 안된다. 

    MainActivity.kt 에 있는 Composable 함수들은 MainActivity 클래스 밖으로 옮기고 클래스에서 바로 호출할 수 있도록 top-level 함수로 작성되어야한다. 

     

    우선 재사용이 가능하도록 수정하기 위해서는 @Composable MyApp 함수를 작성하고 Activity의 UI 로직이 담기도록 한다. 

    Greeting Composable에 작성했던 background 색상은 화면 전체에 적용되도록 할 것이므로 MyApp 함수쪽으로 옮긴다. 

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp()
            }
        }
    }
    
    @Composable
    fun MyApp() {
        BasicsCodelabTheme {
            Surface(color = Color.Yellow) {
                Greeting(name = "Android")
            }
        }
    }
    
    @Composable
    fun Greeting(name: String) {
        Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
    }
    
    @Preview
    @Composable
    fun DefaultPreview() {
        MyApp()
    }

    MyApp Composable을 다른 activity에서도 사용하도록 하고 싶다면 top-level 함수로 작성하는 것이 좋다. 그런데 현재는 MyApp()에서 Greeting()을 포함하기 때문에 재사용의 취지에 적합하지 않다. 다음 컨테이너를 새로 작성하는 방법에 대한 글을 읽어보도록 하자. 

     

    Making container functions

    공통적으로 사용할 컨테이너를 만들고 싶다면 Composable 함수를 파라미터로 받는 함수를 만들 수 있다.
    @Composable 어노테이션은 파라미터로 받는 함수에 적용될 수 있다. 또한 Composable 함수는 UI 컴포넌트를 리턴하지 않는다. 따라서 리턴값은 Unit을 사용한다.

    @Composable
    fun MyApp(content: @Composable () -> Unit) {
        BasicsCodelabTheme {
            Surface(color = Color.Yellow) {
                content()
            }
        }
    }

    위 함수의 의도는 모든 부모 컨테이너의 UI를 정의한 다음 자식 UI 컴포넌트를 정의하는 것이다.

    코틀린 문법을 따라 맨 마지막 파라미터가 함수 값을 받을 때, {} 로 표현되는 람다 표현식으로 전달할 수 있고 소괄호는 생략 가능하다.

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp {
                    Greeting("Android")
                }
            }
        }
    }
    
    @Composable
    fun Greeting(name: String) {
        Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
    }
    
    // 람다 표현식으로 값 전달 
    MyApp({ Greeting("Android") })
    
    // 소괄호 생략가능
    MyApp { Greeting("Android") }

     

    단순 궁금증

    @Composable 어노테이션은 안붙이면 어떻게 될까?

    @Composable
    fun MyApp(content: () -> Unit) {
        BasicsCodelabTheme {
            Surface(color = Color.Yellow) {
                content()
            }
        }
    }
    
    MyApp({ Greeting("Android") })

     

    린트/빌드 에러가 발생한다.

     

    메소드 레퍼런스로 넘기면 안되나?

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp(::Greeting)
            }
        }
    
        @Composable
        fun MyApp(content: @Composable () -> Unit) {
            BasicsCodelabTheme {
                Surface(color = Color.Yellow) {
                    content()
                }
            }
        }
    
        @Composable
        fun Greeting() {
          Text(text = "Hello Android", modifier = Modifier.padding(24.dp))
        }    
    }

    타입 미스매치라는 에러가 발생한다. MyApp()은 Function0 클래스로 받아야하는데 Greeting()이 KFunction0 클래스로 만들어지나보다. 

     

    Calling Composable functions multiple times using Layouts

    XML을 사용할 때 처럼 코드를 복제하지 않고 Composable 함수를 이용해 UI 컴포넌트를 재사용할 수 있다. 

    세로 방향으로 아이템들을 배열하기 위해서 Column Composable 함수를 사용한다. (LinearLayout과 비슷하다)

    @Composable
    fun MyScreenContent() {
        Column {
            Greeting("Android")
            Divider(color = Color.Black)
            Greeting("there")
        }
    }

    Divider가로 방향 구분선을 만드는 Composable 함수이다. 

    Composable 함수가 호출되면 UI 계층이 만들어지고, Composable 함수를 호출할 때마다 UI 아이템들이 만들어진다고 생각하면 된다. 

     

    Compose and Kotlin

    Composable 함수는 코틀린 함수처럼 호출할 수 있으므로 for 문을 이용해 Column에 아이템들을 추가할 수 있다. 

    @Composable
    fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
        Column {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
    }

     


    State in Compose 

    우리는 데이터를 UI로 표시해주어야 한다. Jetpack Compose는 데이터가 변경될 때마다 Composable 함수를 호출해 새로운 데이터를 UI에 반영한다. Compose는 이를 위해서 앱 데이터의 변경사항을 옵저빙하기 위한 툴(간단한 함수 호출!!)을 제공한다. 이 툴은 Composable 함수를 자동으로 호출해준다. 이러한 UI이 변경을 Recomposing 이라고 한다. Compose는 각각의 Composable에 무슨 데이터가 필요한지 확인해서 변경된 데이터의 UI만 영향이 가도록 한다. 즉 수정이 필요 없는 UI 컴포넌트는 건너뛴다는 의미이다. 

    Compose는 커스텀 코틀린 컴파일러 플러그인을 사용해서 내부적으로 데이터가 변경 되었을 때, Composable 함수가 호출되어 UI를 업데이트한다. 예를 들면 Greeting("Android") 호출했을 때, MyScreenContent Composable 함수는 "Android" 는 하드코딩된 값이므로 변경될 일이 전혀 없다는 걸 예측할 수 있기 때문에 UI 계층에 Greeting은 한 번만 추가가 되고 MyScreenContent가 Recomposing 되더라도 추가되었던 Greeting은 변경되지 않는다. 

    Composable에 내부적인 상태값을 추가하고 싶다면 mutableStateOf 함수를 이용해야한다. 이는 Composable 에 가변 메모리를 제공한다. Composable의 데이터가 변경될 때마다 상태를 유지하고 싶다면, remember 함수를 사용한다. 이 remember 함수는 @Composable 어노테이션이 있을 때만 사용 가능하다. 단, 다른 Composable 인스턴스를 한 화면에 가지고 있다면 서로 다른 버전의 상태값을 유지할 것이다. 이는 클래스 내부의 private 변수와 비슷한 개념이다. 

    Composable 함수는 자동으로 이 상태값을 구독하고 상태값이 변경되면 해당 값을 읽어 Recomposed된다.

    버튼이 클릭된 횟수를 기억하는 카운터를 만들어보자. Counter라는 Composable 함수를 정의하고 얼마나 클릭이 되었는지 화면에 표시한다. 

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
    
        Button(onClick = { count.value++ }) {
            Text("I've been clicked ${count.value} times")
        }
    }

    Remember.kt

    // Remember.kt
    @Composable
    inline fun <T> remember(
        key1: Any?,
        calculation: @ComposableContract(preventCapture = true) () -> T
    ): T {
        return currentComposer.cache(currentComposer.changed(key1), calculation)
    }

    MutableState.kt

    /** A mutable value holder where reads to the value property during the execution of a Composable function, the current RecomposeScope will be subscribed to changes of that value. 
    When the value property is written to and changed, 
    a recomposition of any subscribed RecomposeScopes will be scheduled. 
    If value is written to with the same value, no recompositions will be scheduled. */
    
    @Stable
    interface MutableState<T> : State<T> {
        override var value: T
        operator fun component1(): T
        operator fun component2(): (T) -> Unit
    }

     

    버튼이 클릭될 때마다 숫자가 증가한다.

    Button이 count.value를 읽기 때문에 Button은 이 값이 변경될 때마다 Recomposing하며 값을 화면에 표시한다. 

    여러 UI 컴포넌트가 있는 UI 계층에 버튼을 추가해보자. 

    @Composable
    fun MyScreenContent(names:List<String> = listOf("Android", "there")) {
        Column {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
            Divider(color = Color.Transparent, thickness = 32.dp)
            Counter()
        }
    }

    Preview에서 인터랙티브 모드로 변경하면 버튼을 누를 때마다 Counter가 상태를 유지하는 것을 볼 수 있다. 

     

    Source of truth

    Composable 함수에서 상태값은 반드시 외부로 드러나야한다. 그래야 Composable이 상태값을 소비하고 사용할 수 있다. 이 과정을 State hoisting(hoist : 끌어올리다) 이라고 한다.

    이 단어의 의미는 내부 상태값을 사용하는 함수에서 접근할 수 있도록 외부로 노출시키는 것이다. 따라서 Composable 함수에 파라미터로 전달한다던지 Composable 외부에서 만들어져야한다. 이는 중복 상태값을 만든 것을 방지하고 Composable을 재사용가능하게 하며 테스트가 용이하게 할 수 있다.

    단, Composable이 사용하지 않는 상태값은 외부에 드러나지 않게 한다. 예를 들면, 스크롤의 scrollerPosition는 외부에 노출되어있지만 maxPosition은 그렇지 않다. 이처럼 사용자가 특정 상태값은 신경쓰지 않을 때가 있다. 

    아래 코드에 적용을 해보면, 사용자가 Counter의 상태에 관심이 있다고 해보자. 외부에 노출되는 counterState를 정의하고 Counter에 파라미터로 넘길 수 있다. 

    @Composable
    fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
        val counterState = remember { mutableStateOf(0) }
    
        Column {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
            Divider(color = Color.Transparent, thickness = 32.dp)
            Counter(
                count = counterState.value,
                updateCount = { newCount ->
                    counterState.value = newCount
                }
            )
        }
    }
    
    @Composable
    fun Counter(count: Int, updateCount: (Int) -> Unit) {
        Button(onClick = { updateCount(count+1) }) {
            Text("I've been clicked $count times")
        }
    }

     


     

    Flexible layouts

    위에서 수직으로 아이템을 정렬하는 Column을 잠깐 사용해보았는데, 수평으로 아이템을 정렬할 수 있는 Row도 있다. Row와 Column은 weight modifier를 사용해 화면에 Layout을 변형할 수 있다. 

    Column과 weight를 함께 사용해보자. 버튼 하나는 스크린 맨 위, 다른 버튼 하나는 스크린 맨 아래에 있다고 해보자.

    1. weight를 사용하는 Column을 만들고 아이템들을 감싸는 형태를 만든다. 외부 컬럼 크기가 조정되고 난 뒤 아이템 크기들이 채워지기 때문에 수직으로 Column이 꽉 찬 뒤 아이템들이 그 크기를 따르게된다. 

    2. Counter 값을 외부에 둔다.

    @Composable
    fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
        val counterState = remember { mutableStateOf(0) }
    
        Column(modifier = Modifier.fillMaxHeight()) {
            Column(modifier = Modifier.weight(1f)) {
                for (name in names) {
                    Greeting(name = name)
                    Divider(color = Color.Black)
                }
            }
            Counter(
                count = counterState.value,
                updateCount = { newCount ->
                    counterState.value = newCount
                }
            )
        }
    }

    weight modifier는 fillMaxHeight()와 함께 사용해야하고 fillMaxHeight()는 스크린에서 Column이 최대 높이를 가지도록 하는 것이다.

     

    다른 예제로 Button에 background 컬러 값을 입힐 수 있다.

    @Composable
    fun Counter(count: Int, updateCount: (Int) -> Unit) {
        Button(
            onClick = { updateCount(count+1) },
            colors = ButtonDefaults.buttonColors(
                backgroundColor = if (count > 5) Color.Green else Color.White
            )
        ) {
            Text("I've been clicked $count times")
        }
    }

    프리뷰에서 확인하면 다음과 같다. 버튼의 색상을 처음에는 하얀색으로 6번째 클릭부터는 초록색으로 표시한다. 

     

    이제 실전으로 1000개의 아이템을 관리해보자. 

    @Composable
    fun NameList(names: List<String>, modifier: Modifier = Modifier) {
       LazyColumn(modifier = modifier) {
           items(items = names) { name ->
               Greeting(name = name)
               Divider(color = Color.Black)
           }
       }
    }

     

    MyScreenContent 함수 이름을 NameList로 변경하고 디폴트 파라미터 값도 변경한다. 리스트 파라미터는 1000개의 아이템으로 넘겨보자.

    names: List<String> = List(1000) { "Hello Android #$it" }

    Column은 기본적으로 스크롤이 불가능하다. 따라서 아래 두가지 설정을 해야한다.

    ScrollableCoulumn은 verticalScroll modifier를 이용한 Column이다. 모든 아이템을 한 번에 렌더링하며 ScrollView와 동일하다고 볼 수 있다. 

    LazyColumn은 오직 화면에만 보이는 아이템들만 그린다. 이는 RecyclerView와 동일한 형태이다.

    따라서 NameList는 1000개의 아이템을 가지고 있기 때문에 LazyColumn을 사용하고 있으며 이 Composable 함수는 모든 item들에 접근할 수 있게 해준다. 

    여기서 LazyColumn은 RecyclerView 처럼 자식 뷰들을 따로 재활용하진 않는다. 화면에 아이템이 변경될 때마다 Composable을 화면에 그리게 되는데 이는 Android View를 새로 인스턴스를 생성하는 것 보다 상대적으로 비용이 적게 들기 때문이다. 

     

    전체 코드는 다음과 같다. 

    class MainActivity : AppCompatActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContent {
               MyApp {
                   MyScreenContent()
               }
           }
       }
    }
    
    @Composable
    fun MyApp(content: @Composable () -> Unit) {
       BasicsCodelabTheme {
           Surface(color = Color.Yellow) {
               content()
           }
       }
    }
    
    @Composable
    fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
       val counterState = remember { mutableStateOf(0) }
    
       Column(modifier = Modifier.fillMaxHeight()) {
           NameList(names, Modifier.weight(1f))
           Counter(
               count = counterState.value,
               updateCount = { newCount ->
                   counterState.value = newCount
               }
           )
       }
    }
    
    @Composable
    fun NameList(names: List<String>, modifier: Modifier = Modifier) {
       LazyColumn(modifier = modifier) {
           items(items = names) { name ->
               Greeting(name = name)
               Divider(color = Color.Black)
           }
       }
    }
    
    @Composable
    fun Greeting(name: String) {
       Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
    }
    
    @Composable
    fun Counter(count: Int, updateCount: (Int) -> Unit) {
       Button(
           onClick = { updateCount(count + 1) },
           colors = ButtonDefaults.buttonColors(
               backgroundColor = if (count > 5) Color.Green else Color.White
           )
       ) {
           Text("I've been clicked $count times")
       }
    }
    
    @Preview("MyScreen preview")
    @Composable
    fun DefaultPreview() {
       MyApp {
           MyScreenContent()
       }
    }

     


    Animating your list

    클릭했을 때 버튼의 배경색을 변경할 것인데 바로 바뀌는 것이 아닌 애니메이션을 적용해보자.  

    이 것을 구현하기 위해서는 animateColorAsState() API를 사용하면 된다. 그리고 Greeting Composable 안에 isSelected 상태값을 저장하도록 한다. 

    // by delegator 쪽 import
    
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.setValue
    import androidx.compose.runtime.mutableStateOf
    
    @Composable
    fun Greeting(name: String) {
       var isSelected by remember { mutableStateOf(false) }
       val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
    
       Text(
           text = "Hello $name!",
           modifier = Modifier
               .padding(24.dp)
               .background(color = backgroundColor)
               .clickable(onClick = { isSelected = !isSelected })
       )
    }

    animateColorAsState()는 Color를 파라미터로 받고 받아온 파라미터들 중 애니메이션 전환시 필요한 중간 색상을 자동으로 설정한다. 그리고 Text Composable을 이용해서 애니메이션으로 변경된 background 색상을 전달한다. 

    전체 코드는 다음과 같다.

    class MainActivity : AppCompatActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContent {
               MyApp {
                   MyScreenContent()
               }
           }
       }
    }
    
    @Composable
    fun MyApp(content: @Composable () -> Unit) {
       BasicsCodelabTheme {
           Surface(color = Color.Yellow) {
               content()
           }
       }
    }
    
    @Composable
    fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
       val counterState = remember { mutableStateOf(0) }
    
       Column(modifier = Modifier.fillMaxHeight()) {
           NameList(names, Modifier.weight(1f))
           Counter(
               count = counterState.value,
               updateCount = { newCount ->
                   counterState.value = newCount
               }
           )
       }
    }
    
    @Composable
    fun NameList(names: List<String>, modifier: Modifier = Modifier) {
       LazyColumn(modifier = modifier) {
           items(items = names) { name ->
               Greeting(name = name)
               Divider(color = Color.Black)
           }
       }
    }
    
    @Composable
    fun Greeting(name: String) {
       var isSelected by remember { mutableStateOf(false) }
       val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
    
       Text(
           text = "Hello $name!",
           modifier = Modifier
               .padding(24.dp)
               .background(color = backgroundColor)
               .clickable(onClick = { isSelected = !isSelected })
       )
    }
    
    @Composable
    fun Counter(count: Int, updateCount: (Int) -> Unit) {
       Button(
           onClick = { updateCount(count + 1) },
           colors = ButtonDefaults.buttonColors(
               backgroundColor = if (count > 5) Color.Green else Color.White
           )
       ) {
           Text("I've been clicked $count times")
       }
    }
    
    @Preview("MyScreen preview")
    @Composable
    fun DefaultPreview() {
       MyApp {
           MyScreenContent()
       }
    }

    (CodeLab에서 deprecate된 animateAsState를 사용해서 animateColorAsState로 변경)


    Theming your app

    예제의 Activity에서 BasicCodelabTheme를 기본으로 사용하고 있었다. app/package/java/ui/theme/Theme.kt 파일을 열어보면, BasicCodelabTheme이 MaterialTheme을 사용하는 것을 볼 수 있다. MaterialTheme은 Composable 함수로 안드로이드 디자인 가이드인 Material design을 따르고 있다. 

    Type.kt에 선언한 typography 변수와  

    // Type.kt
    import androidx.compose.material.Typography
    import androidx.compose.ui.text.TextStyle
    import androidx.compose.ui.text.font.FontFamily
    import androidx.compose.ui.text.font.FontWeight
    import androidx.compose.ui.unit.sp
    
    // Set of Material typography styles to start with
    val typography = Typography(
                    body1 = TextStyle(
                    fontFamily = FontFamily.Default,
                    fontWeight = FontWeight.Normal,
                    fontSize = 16.sp
            )
    )
    
    
    // androidx.compose.material.Typography.kt
    constructor(
            defaultFontFamily: FontFamily = FontFamily.Default,
            h1: TextStyle = TextStyle(
                fontWeight = FontWeight.Light,
                fontSize = 96.sp,
                letterSpacing = (-1.5).sp
            ),
            h2: TextStyle = TextStyle(
                fontWeight = FontWeight.Light,
                fontSize = 60.sp,
                letterSpacing = (-0.5).sp
            ),
            h3: TextStyle = TextStyle(
                fontWeight = FontWeight.Normal,
                fontSize = 48.sp,
                letterSpacing = 0.sp
            ),
            ...
    

    Theme.kt에 선언한 Dark/Light 컬러값들을 이용해 MeterialTheme()를 선언하고 있다. 

    // Theme.kt
    import androidx.compose.foundation.isSystemInDarkTheme
    import androidx.compose.material.MaterialTheme
    import androidx.compose.material.darkColors
    import androidx.compose.material.lightColors
    import androidx.compose.runtime.Composable
    
    private val DarkColorPalette = darkColors(
            primary = purple200,
            primaryVariant = purple700,
            secondary = teal200
    )
    
    private val LightColorPalette = lightColors(
            primary = purple500,
            primaryVariant = purple700,
            secondary = teal200
    )
    
    @Composable
    fun BasicCodelabTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
        val colors = if (darkTheme) {
            DarkColorPalette
        } else {
            LightColorPalette
        }
    
        MaterialTheme(
                colors = colors,
                typography = typography,
                shapes = shapes,
                content = content
        )
    }

    Activity에서는 Greeting을 사용할 때 BasicCodelabTheme로 감싸서 사용하기 때문에 Greeting에 MaterialTheme이 적용된 것이다. 

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                BasicsCodelabTheme {
                    Greeting(name = "Android")
                }
            }
        }
    }

    Greeting의 Text에 직접 typography를 적용할 수도 있다. 

    @Composable
    fun Greeting(name: String) {
        Text (
            text = "Hello $name!",
            modifier = Modifier.padding(24.dp),
            style = MaterialTheme.typography.h1
        )
    }

    적용한 화면은 다음과 같다.

     

    Create your app's theme

    Theme.kt에 있는 것 처럼 재사용이 가능한 Composable을 이용해서 직접 테마를 만들 수 있다. 

    import androidx.compose.foundation.isSystemInDarkTheme
    import androidx.compose.runtime.Composable
    
    @Composable
    fun BasicsCodelabTheme(
        darkTheme: Boolean = isSystemInDarkTheme(),
        content: @Composable () -> Unit
    ) {
    
        // TODO 
    }

    primary color 등 값을 지정한다.

    private val DarkColors = darkColors(
        primary = purple200,
        primaryVariant = purple700,
        secondary = teal200
    )
    
    private val LightColors = lightColors(
        primary = purple500,
        primaryVariant = purple700,
        secondary = teal200
    )
    
    @Composable
    fun BasicsCodelabTheme(
        darkTheme: Boolean = isSystemInDarkTheme(),
        content: @Composable () -> Unit
    ) {
        val colors = if (darkTheme) {
            DarkColors
        } else {
            LightColors
        }
    
        MaterialTheme(colors = colors) {
            content()
        }
    }
    • primary : (빈도수를 어떻게 측정하는지는 잘 모르겠지만) 화면에 자주 표시되는 컴포넌트에 대한 색상이다.
    • primaryVariant : primary의 변형색상
    • secondary : primary보다 노출 빈도수 적은 컴포넌트 색상

    참조 : material.io/design/color/the-color-system.html#color-theme-creation 

     


    튜토리얼 따라하다보니 불편한 점

    1. 아직 Composable 함수명이 익숙하지 않다.

    `LinearLayout` vertical -> `Column`, `LinearLayout` horizontal -> `Row`

     

    2. 뷰 트리를 하나하나 확인하기 불편하다.

    ex) 내가 작성한 컬럼이 스크린에서 어느정도 크기를 차지하고 있는지 프리뷰에서 마우스로 찍어서 보기가 힘들다. 마우스 포인터가 움직이긴하지만 뷰가 복잡해지면 찾아보기 힘들 것 같다. 

     

    3. Layout inspector에 뷰 트리가 아무것도 안나온다.

    DecorationView의 canvas에 한 번에 다 그리는 것 같다. 

     

    4. CustomView 만들기가 귀찮아졌다.

    CustomView를 만들려면 AndroidView 라는 Composable 을 이용해야한다. 람다로 View클래스를 파라미터로 받아 원하는 뷰를 그릴 수 있다고 한다. 

    @Composable
    fun CustomView() {
        val selectedItem = remember { mutableStateOf(0) }
    
        // Adds view to Compose
        AndroidView(
            modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
            viewBlock = { context ->
                // Creates custom view
                CustomView(context).apply {
                    // Sets up listeners for View -> Compose communication
                    myView.setOnClickListener {
                        selectedItem.value = 1
                    }
                }
            },
            update = { view ->
                // View's been inflated or state read in this block has been updated
                // Add logic here if necessary
    
                // As selectedItem is read here, AndroidView will recompose
                // whenever the state changes
                // Example of Compose -> View communication
                view.coordinator.selectedItem = selectedItem.value
            }
        )
    }
    
    @Composable
    fun ContentExample() {
        Column(Modifier.fillMaxSize()) {
            Text("Look at this CustomView!")
            CustomView()
        }
    }

     

    custom view in jetpack compose - https://developer.android.com/jetpack/compose/interop#views-in-compose 

     

    반응형
Designed by Tistory.