경험의 기록

2021.08.10 - [안드로이드/Jetpack-Compose] - [Android] Jetpack Compose 제트팩 컴포즈 사용해보기 - (1) 개념과 구조

 

[Android] Jetpack Compose 제트팩 컴포즈 사용해보기 - (1) 개념과 구조

https://android-developers.googleblog.com/2021/07/jetpack-compose-announcement.html Jetpack Compose is now 1.0: announcing Android’s modern toolkit for building native UI Posted by Anna-Chiara Bell..

hanyeop.tistory.com

에서 이어지는 글입니다.

 

이전글에서 대략적인 구조를 알아보았으니

사용법에 대해 간단히 알아보려고 한다.

 

이 글은 Google 공식문서를 기반으로 작성되었습니다.


Surface

Surface 를 사용하면 배경의 색상을 변경할 수 있다.

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

예를 들어, 어플리케이션의 배경색을 변경하기 위하여 기존의 Greeting 코드의 색상을 Purple200으로 변경해보자.

 

잘 변경된 것을 확인할 수 있다.

 

❗ 하지만 Compose 에서는 재사용성을 강조하고 있다. 재사용이 가능하게 쪼개서 관리하여야 UI를 수정하고 사용하는데에 더 유리하다.

따라서 텍스트를 생성하는 함수인 Greeting 함수에서 어플리케이션 배경색을 지정하는 것은 재사용성을 해치게 되고, 함수를 여러번 사용할 때 마다 배경색을 계속 지정해주는 것은 구조상 이상하다.

 

 

Reusability

@Composable
fun MyApp() {
    ComposeExTheme {
        Surface(color = Purple200) {
            Greeting(name = "Android")
        }
    }
}

그래서 재사용이 가능하도록 별도로 MyApp이라는 함수를 작성해주고

 

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}

메인과 Preview에서 MyApp을 사용하게 변경해준다.

하지만 이 코드에서도 MyApp이 Greeting 함수를 포함하고 있기 때문에 재사용의 취지에 어긋난다.

 

이것을 해결하기 위하여 공통의 컨테이너를 지정할 수 있는데

 

@Composable
fun MyApp(content: @Composable () -> Unit) {
    ComposeExTheme {
        Surface(color = Purple200) {
            content()
        }
    }
}

람다식을 활용하여 @Composable을 파라미터로 받도록 한다.

또한 Composable은 반환 타입을 정하지 않기 때문에 Unit으로 설정해준다.

 

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                Greeting("Android")
            }
        }
    }
}

@Composable
fun MyApp(content: @Composable () -> Unit) {
    ComposeExTheme {
        Surface(color = Purple200) {
            content()
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp {
        Greeting("Android")
    }
}

이제 Preview와 메인에서 Greeting 함수를 MyApp에 넘겨주도록 작성해주면 된다.

 

 

 

Modifiers

modifier 를 사용하면 Surface와 Text와 같은 대부분의 UI 컴포넌트의 위치를 지정할 수 있다.

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

예를들어 padding을 사용하여 패딩을 지정해줄 수 있다.

 

 

 

Column & Divider

Column 을 사용하여 아이템을 수평방향으로 배치할 수 있다.

Divider 는 수평 구분선 역할을 한다.

@Composable
fun MyScreenContent() {
    Column {
        Greeting("안드로이드")
        Divider(color = Color.Black)
        Greeting("컴포즈")
    }
}

별도로 MyScreenContent 함수를 작성해준다.

Compose는 기존의 XML처럼 코드를 복사하지 않고 Composable 함수를 호출함으로써 여러 UI를 만든다.

 

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                MyScreenContent()
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp {
        MyScreenContent()
    }
}

이제 적용하고 확인해보면

 

디자인이 변경된 것을 확인할 수 있다.

 

@Composable
fun MyScreenContent(names: List<String> = listOf("안드로이드", "컴포즈")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
    }
}

또한 Compose는 Kotlin을 기반으로 하고 있으므로

코틀린의 컬렉션, for문등을 전부 사용하여 UI를 구성할 수 있다.

 

 

State

상태 변경에 대응하는 것은 Compose의 핵심이다. Compose 앱은 Composable 함수를 호출하여 데이터를 UI로 변환하는데, 데이터가 변경되면 새 데이터로 이러한 기능을 호출하여 업데이트된 UI를 만든다.

 

Compose는 기존의 옵저버 패턴과 같이 앱 데이터의 변경 사항을 관찰하기 위한 도구를 제공하여 자동으로 기능을 호출한다. 이를 recomposing 이라고 한다. 또한 Compose는 데이터가 변경된 구성 요소만 재구성하고 영향을 받지 않은 구성 요소를 건너뛸 수 있도록 개별 구성 가능에 필요한 데이터를 확인한다.

 

즉, MyScreenContent Composable 함수에서 아까 하드코딩된 Greeting("Android")을 호출하면 그 값은 변경되지 않을 것이므로 UI 트리에 한 번 추가되고 recomposed 되더라도 변경되지 않게 된다.

 

@Composable
fun Counter() {

    val count = remember { mutableStateOf(0) }

    Button(onClick = { count.value++ }) {
        Text("${count.value} 번 클릭하셨네요!")
    }
}

예를 들어 버튼을 만들어 버튼 클릭 회수를 보여주는 Counter 함수를 만들 수 있다.

 

mutableStateOf 를 사용하여 가변 메모리를 사용할 수 있다.

또한 remember를 사용하면 데이터가 변경될 때의 상태를 유지할 수 있다. 화면의 다른 위치에 컴포저블의 여러 객체들은 자체 버전의 상태를 갖게 되는 것이다. 마치 기존의 private 변수와 유사하다.

 

Composable은 자동으로 이 값을 옵저빙하여 UI를 갱신한다.

 

@Composable
fun MyScreenContent(names: List<String> = listOf("안드로이드", "컴포즈")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }

        Divider(color = Color.Transparent, thickness = 32.dp)
        Counter()
    }
}

이제 MyScreenContent 함수로 돌아와 Divider의 색을 Transparent(투명) 으로 지정해주고 두께를 32dp로 추가해준 후
Counter 함수를 호출해주면

 

클릭 횟수에 따라 UI가 자동으로 갱신 되는 것을 확인할 수 있다.

 

 

 

Source of truth

Composable 함수에서 State는 함수를 사용하거나 제어하기 위한 유일한 방법이기 때문에 외부에 노출되어야 한다. 이 프로세스를 State hoisting이라고 한다.

 

Source of truth 라는 거창한 이름을 썼지만 쉽게 말해서, State hoisting은 내부 상태를 호출한 함수에 의해 제어 가능하게 만드는 방법이다.

이로 인해 재사용성이 올라가고 테스트가 용이해진다.

 

Composable이 사용하지 않는 값은 외부에 노출되지 않도록 하는 것이 좋다. 예를 들어 scrollerPosition 상태는 노출되지만 maxPosition 는 노출되지 않는다.

 

@Composable
fun MyScreenContent(names: List<String> = listOf("안드로이드", "컴포즈")) {
    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("$count 번 클릭하셨네요!")
    }
}

기존의 Counter 함수에서 관리되던 Count값을 MyScreenContent 함수가 관리하도록 하고, 람다식을 활용하여 호출한 함수의 값을 변경해준다. 이렇게 하면 Counter 함수는 오직 버튼 클릭시 숫자를 증가시키고 그 텍스트를 출력시키는 역할만을 하게 된다.

 

 

실행하게 되면 아까 작성했던 것과 동일한 기능을 하는 것을 확인할 수 있다.

 

 

Weight Modifier

새로운 텍스트를 생성할 때 마다 기존의 LinearLayout처럼 위에서 부터 하나씩 쌓아지는 것을 확인할 수 있다.

여기서 특정 아이템의 위치를 지정해주고 싶다면 Weight를 사용하면 된다.

 

@Composable
fun MyScreenContent(names: List<String> = listOf("안드로이드", "컴포즈")) {
    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를 지정해주기 위해서는 전체를 감싸는 Column의 Modifier.fillMaxHeight() 를 사용해야만 한다.

Modifier.fillMaxHeight() 는 Column이 최대의 높이를 가지도록 해준다.

 

그 후 텍스트들을 별도의 Column으로 묶어 weight를 1f로 지정해준다.

 

이제 배경이 어플리케이션을 전부 채우게 되고 상단에는 텍스트가 위치하며 하단에는 버튼이 위치한 레이아웃으로 변경되었다.

 

 

여러 아이템에 weight를 주어 이런식으로도 사용할 수 있다.

 

Button 색상 변경

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button( onClick = { updateCount(count+1) },
            colors = ButtonDefaults.buttonColors(
            backgroundColor = if (count > 5) Color.Cyan else Color.White)
    ) {
        Text("$count 번 클릭하셨네요!")
    }
}

Button의 colors의 속성을 변경하여

조건부로 색상을 변경할 수도 있다.

기본 색상을 화이트로 바꿨으며 클릭 회수가 5를 초과하면 다른 색으로 변경하는 등으로 구현할 수 있다.

 

 

LazyColumn

LazyColumn은 기존의 RecyclerView와 동일한 기능을 한다.

그러나 LazyColumn은 RecyclerView처럼 뷰를 재활용하지 않는다. 스크롤할 때 새로운 Composable을 내보내고 그것이 기존의 방법인 View를 인스턴스화하는 것에 비해 상대적으로 효율적이기 때문이다.

 

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

기존의 텍스트 Column을 생성하는 코드를 별도의 NameList 함수로 작성하여

LazyColumn을 사용한다.

 

@Composable
fun MyScreenContent(names: List<String> = List(100) { "안드로이드 #$it" }) {
    val counterState = remember { mutableStateOf(0) }

    Column(modifier = Modifier.fillMaxHeight()) {
        NameList(names, Modifier.weight(1f))
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }
}

이제 names 리스트를 100개 정도 지정해주고

NameList 함수에 전달해준다.

 

평소에 사용하던 RecyclerView 와 같은 화면에 텍스트가 나열된 것을 확인할 수 있으며 스크롤 할 수 있다.

 

 

 

Animating

UI 변경 시의 애니메이션을 사용하기 위해서는 animateColorAsState 를 사용하면 된다.

 

@Composable
fun Greeting(name: String) {
    var isSelected by remember { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

    Text(text = "안녕하세요 $name!",
        modifier = Modifier
        .padding(24.dp)
        .background(color = backgroundColor)
        .clickable(onClick = { isSelected = !isSelected }))
}

remember를 사용하여 isSelected 값을 저장하고

animateColorAsState 를 사용하여 텍스트가 클릭된 상태라면 빨간색으로, 아니라면 투명색으로 배경을 변경시켜준다.

 

animateColorAsState 가 중간색상을 자동으로 생성하여 애니메이션을 만들어준다.

 

하지만 isSelected 상태가 Greeting 컴포저블에서 hoisting 되기 때문에 NameList는 항목이 선택되었는지 여부를 추적하지 않는다. 항목이 화면 밖으로 스크롤되면 상태가 false로 설정되어 색이 다시 투명하게 변경된다. 

따라서 변경사항을 유지하려면 해당 isSelected 상태를 NameList 수준에서 끌어올려야 한다.

 

 

TextStyle

@Composable
fun Greeting(name: String) {
    var isSelected by remember { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

    Text(text = "안녕하세요 $name!",
        modifier = Modifier
        .padding(24.dp)
        .background(color = backgroundColor)
        .clickable(onClick = { isSelected = !isSelected }),
    style = MaterialTheme.typography.h1)
}

테마의 속성을 따르지않고 텍스트의 style 속성을 변경하여 텍스트 스타일을 직접 변경해줄 수 있다.

 

 

 

 


구글의 Codelab 을 참고하여 Compose의 간단한 사용법을 알아보았다.

아직 익숙하지 않아서인지 Composable 이 많아지면 굉장히 헷갈릴거 같은 기분이 든다.

 

하지만 UI와 기능을 한번에 코딩하여 옵저빙하여 상태를 갱신할 수 있다는 것은 분명 엄청난 장점으로 느껴진다.

 

https://github.com/HanYeop/Jetpack-Compose/tree/master/ComposeEx

 

GitHub - HanYeop/Jetpack-Compose: Jetpack Compose 사용해보기

Jetpack Compose 사용해보기. Contribute to HanYeop/Jetpack-Compose development by creating an account on GitHub.

github.com

 

 

 

참고

https://developer.android.com/courses/pathways/compose

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading