안드로이드

컴포즈 스터디 1차 시작 튜토리얼 정리

강한 맷돼지 2023. 12. 21. 21:03

Compose 기본사항 시작

앞으로 다룰 내용들은 Android 개발자를 위한 jetpack compose 문서를 학습한 내용이다.

일간 시작점인 compose 기본사항의 항목을 따라가보자

1. 시작 듀토리얼

1.구성 가능한 함수

compose, composable 등등 이제 새로운 용어가 튀어나오니 얼탱이 없는 번역도 같이 튀어나올것이다.

기존에 activity가 활동, fragment 가 조각이였던것 처럼 이제 compose는 구성가능한 이라는 말을쓴다. 구성가능한이라니 그래도 기존것들보다는 선녀인것 같다.

 

이제 내용을 보자면 compose는 composable함수들을 중심으로 만들어졌다고 한다. 이러한 함수를 사용하면

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

 

라는데 솔직히 뭐라는건지 지금은 모르겠고(번역도 번역이고 난 컴포즈에 생소하고 ...) 대충 지금까지 줏어들은 "컴포즈는 선언형이다", "컴포즈는 함수형이다." 이런 특징들을 나타내리라 예상한다.

 

일단 그래서 코루틴에서 suspend 붙여서 코루틴 표식을 남기듯 @Composable 낙인을 찍어서 함수를 컴포저블 하게 만든다고 한다.

텍스트 요소 추가

이제 실제 텍스트 요소를 넣어보는 예시를 보여준다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent{
            Text("멧돼지다 임마")
        }
    }
}

이렇게 하면 화면에 텍스트를 넣을 수 있는것인데 Text라는것 자체가 컴포저블 함수이고(기본제공 -> 텍스트 뷰 같은거지 )

 

setContent블록을 만들고 그안에서 호출하면 된다.

setContent블록은 컴포저블 함수가 호출되는 액티비티의 레이아웃을 정의한다고 한다.(내 ConstraintLayout 돌려내 엉엉엉)

 

컴포저블 함수는 코루틴 마냥 다른 컴포저블 함수에서만 호출할 수 있다고 한다.

 

컴포즈는 코틀린 컴파일러 플러그인을 통해서 함수를 UI로 변경한다고 한다. 컴포즈 코틀린 컴파일러 플러그인을 구글링해보니 KSP,KAPT와 비교하는걸보니 코드제너레이션을 돕는 도구인것 같다.(지금은 컴포즈 둘러보기니까 가볍게 간다 여기서 이거에 빠지면 못나온다 또)

구성 가능한 함수 정의

이제 위에는 함수분리 없이 그냥 하나의 함수에 때려박는 느낌이었고 이제 함수분리를 해볼때가 되었다.

함수를 @Composable 어노테이션을 달아서 함수분리를 시켜서 재사용가능하게 만든다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PigMessage("멧돼지다 임마")
        }
    }
}
​
@Composable
fun PigMessage(pigSound: String) {
    Text("멧돼지가 말했다: $pigSound")
}
​

이렇게 하면된다 그리고 매개변수를 통해서 값을 받아올 수 있는것이다. 근데 의문점이 컴포즈 함수는 컨벤션이 파스칼 케이스인지 공식문서 함수명이 파스칼 케이스로 되어있다.

어쩃든 이렇게 사용해보면 잘 만들어진다.

Android 스튜디오에서 함수 미리보기

@Preview 어노테이션을 이용하면 미리보기를 볼 수 있다고 한다. 근데 이런 컴포저블 함수는 매개변수가 없어야만 한다고 한다.

그래서 위에서 본 함수와 같은 형태는 미리볼 수 없다는데 그래서 미리보기용 함수를 만들고 있다.

그럼 Tools 마냥 미리보기용 상태값을 다 넣어주고 아마 미리보기를 따로 구성해야 할것 같은데 굉장히 불편하지 않은가 싶다. -> 뭔가 해결책이 있겠지

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PigMessage("멧돼지다 임마")
        }
    }
}
​
@Composable
fun PigMessage(pigSound: String) {
    Text("멧돼지가 말했다: $pigSound")
}
@Preview
@Composable
fun PreviewPigMessage() {
    PigMessage("히히하하호호후후")
}

그래서 이렇게 프리뷰 함수를 따로 만드는 형태를 보여주고 있다.

-> 이렇게 하면 미리보기가 잘보인다.

 

만약 업데이트가 안된다면 상단의 새로고침 버튼을 통해서 최신화 시킬수 있다는데 지가알아서 업데이트 되어서 딱히 그럴 필요가 없다.

2.레이아웃

컴포즈에도 당연히 계층 구조가 있다. 컴포즈의 경우 컴포저블 함수에서 다른 컴포저블 함수를 호출하는 형태로 계층구조를 형성한다.

여러 텍스트 추가

레이아웃을 보여주기 위해서 좀더 뭔가를 추가해서 화면을 빌드한다고 한다. 일단 텍스트를 더 추가해주고 내부적으로 받는 인자도 데이터 클래스로 변경해서 구성해보자.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PigMessage(PigSound("멧돼지가 말했다.","나는 돼지가 아니라 멧돼지다"))
        }
    }
}
​
data class PigSound(val prefix: String, val content: String)
​
@Composable
fun PigMessage(pigSound: PigSound) {
    Text(pigSound.prefix)
    Text(pigSound.content)
}
​
@Preview
@Composable
fun PreviewPigMessage() {
    PigMessage(
        PigSound(
            "돼지가 말하였다",
            "나는 여전히 배가고프다."
        )
    )
}

이렇게 구성해보았는데

이런식으로 글자가 겹치게 되어 문제가 있다고 한다. 이는 정렬 방법이 정의되어 있지 않아서 이다.

 

열 사용

Column '열'이다.

행과열 항상 헷갈리는데 들을때마다 0.1초 스턴걸린다.

어쩃든 요소를 수직으로 정렬하려면 Column을 사용하면 된다.

 

Row는 '행'이다.

요소를 수평으로 정렬하려면 Row를 사용하면 된다.

 

Box를 사용하면 요소를 쌓을 수 있다고 한다. 구글링해본 결과 FrameLayout과 비슷하게 생각하면 될것 같고 공간을 만들기위한 기본요소이다. 참고할만한 글

@Composable
fun PigMessage(pigSound: PigSound) {
    Column {
        Text(pigSound.prefix)
        Text(pigSound.content)
    }
}

 

함수내부에 요소들을 Column으로 감싸면 되는데

 

이런식으로 수직으로 쭉 나열된다.

css 같은데 나 이방식 싫어하는데 ㅠㅠㅠ

 

이미지 요소 추가

이제 이미지를 Image라는 컴포저블 함수를 통해 추가할 수 있는데(대략 이미지뷰) 이미지를 추가하고 column과 row를 통하여 위치가 겹치지 않도록 배치 해보는 예시가 있다.

@Composable
fun PigMessage(pigSound: PigSound) {
    Row {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지"
        )
        Column {
            Text(pigSound.prefix)
            Text(pigSound.content)
        }
    }
}

대략적으로 이런식으로 Row Column안에 적절히 배치하면된다. 익숙해지면 괜찮을것 같다.

 

 

레이아웃 구성

지금까지의 예시들은 너무 못생기고 위치도 이상하고 그래서 이것들의 크기, 레이아웃, 모양, 클릭 가능한 상호작용 등을 추가할 수 있도록 Modifier(수정자라고 해석되어있음)을 이용하여 방금 나온것들을 조정한다.

@Composable
fun PigMessage(pigSound: PigSound) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))
        Column {
            Text(pigSound.prefix)
            Spacer(modifier = Modifier.height(4.dp))
            Text(pigSound.content)
        }
    }
}

 

이런 코드를 통해서 이미지의 패딩 마진 등등을 조정할수 있는데 중간에 위치를 띄워놓는 방식으로 margin을 통한 방법이아닌 Spacer라는 빈공간을 만드는 무언가를 집어넣는 형태를 취해서 신기하다. 어쩃든 요런 형태로 작업하면 된다고 한다.

 

 

3.Material Design

마테리얼이라니 흑흑흑 마테리얼 못생김 흑흑흑 컴포즈의 많은 UI 요소가 마테리얼 디자인을 즉시 사용가능하도록 구현한다고 한다. 한마디로 우리가 쓰던 익숙한 마테리얼이 깜뽀즈에도 녹아있다는거겠지

Material Design 사용

Material Design 스타일 지정을 통해서 디자인을 개선 해본다고 한다. 여기서 보여주는 방법이 ThemeSurface를 이용하는데 이 두가지를 함수에 래핑을 통해서 개선해 본다고 한다.

 

여기서 막간 용어정리

 

Surface는 뭘까? Box랑 비교가 많이 되는데 한마디로 마테리얼 디자인을 적용해놔서 일반적으로 디자인 적인 요소들을 지원하는 레이아웃 비스무리한것 이라고 생각하면 될 것 같다. 참고할만한 글 글에 나와있는것을 보면 Box와의 차이점을 다음과 같이 설명한다.

- Layering and Alignment:
Box is primarily used to stack its children, allowing layering of composables on top of each other. On the other hand, Surface doesn’t stack its children. Instead, it acts as a single-child layout composable.

- Material Design Styles:
Surface automatically applies Material Design styles such as elevation, shape, and color. It also adapts to light/dark  theme modes. Box, in contrast, does not provide these features out of the box.

- Usage:
While Box is mainly used for alignment and layering, Surface is typically used as a container for other composables, where you wish to apply Material Design aesthetics.

 

이를 대충 요약해보자면 Box는 자식들을 겹칠수 있는 반면에 Surface의 경우 자식을 겹쳐 놓을 수 없다고 한다.

또한 기본적으로 Surface는 마테리얼 을 따르기 때문에 elevation,shape,color,다크 테마 등 박스가 기본적으로 제공하지 않는(직접 커스텀해야하는) 디자인적 요소들을 기본적으로 지원한다고 한다.

그래서 Box의 주용도는 레이어링과 정렬에 있고 Surface는 머터리얼을 적용하고 싶은 경우에 다른 컴포저블을 담는 용도로 쓰인다고 한다.

 

그래서 코드를 살펴보면 이렇게 만들어놓았다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTutorialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    PigMessage(PigSound("멧돼지가 말했다.", "나는 돼지가 아니라 멧돼지다"))
                }
            }
        }
    }
}
​
@Preview
@Composable
fun PreviewPigMessage() {
    ComposeTutorialTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            PigMessage(
                PigSound(
                    "돼지가 말하였다",
                    "나는 여전히 배가고프다."
                )
            )
        }
    }
}

이렇게 하면 테마에 맞춰서 앱전체에서 일관성있게 관리할 수 있는 장점이 있다고 한다.

Material Design은 Color,Typography,Shape 이 세가지 요소를 중심으로 이루어 진다고 한다.

이제 이것들을 하나씩 적용해나가 본다고 한다.

색상

MaterialTheme.colors를 이용해서 래핑한 테마의 색상을 지정할 수 있다고 한다.

-> 안드로이드 문서 예시에는 MaterialTheme.colors로 되어있는데 이상하게 실제 쳐보면 colors가 자동완성이 안되고 비스무리한게 colorScheme라는게 있었는데 구글링해보니 Material2 는 colors Material3는 colorScheme라고 한다.

 

그래서 내가 만든 예시에는 colorScheme를 사용했다.

@Composable
fun PigMessage(pigSound: PigSound) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))
        Column {
            Text(
                text = pigSound.prefix,
                color = MaterialTheme.colorScheme.secondary
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(pigSound.content)
        }
    }
}

 

 

그래서 적용해보았더니 테두색 잘나오고 한다. -> 이걸 래핑한 테마를 따른다니까 테마를 잘활용하면 편하게 코딩할 수 있겠다는 생각이든다. 물론(xml에서 테마 활용안한 내 잘못이긴함)

 

 

서체

마찬가지 개념으로 글씨체도 MaterialTheme를 이용해서 적용할 수 있으며 예시를 보면 바로 알 수 있다.

마찬가지로 구글 예시랑 좀 다른데 이또한 Material3라서 그런것 같다.

@Composable
fun PigMessage(pigSound: PigSound) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))
        Column {
            Text(
                text = pigSound.prefix,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(
                text = pigSound.content,
                style = MaterialTheme.typography.bodySmall
            )
        }
    }
}

 

이런식으로 서체 크기가 달라진다.

 

도형

Shape를 사용해서 모양을 잡을 수 있는데 예시에서는 Surface로 텍스트를 감싸고 그거에 모양을 주는데 그 모양 이름이 midium이라 뭔지 직감적으로 이해가 안된다.

이런건 나중에 알아보고 무슨 말풍선 같이 생긴 도형이 씌워진다.

 

그리고 shadowElevation을 줘서 대충 경계선도 보여준다 이정도 예시이다.

또한 사이즈를 딱히 안설정해주면 wrapContent처럼 내부 원소에 맞춰서 사이즈가 맞아진다. 그리고 텍스트에 패딩도 줘서 일단 예쁘게 만들었다.

@Composable
fun PigMessage(pigSound: PigSound) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))
        Column {
            Text(
                text = pigSound.prefix,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(4.dp))
            Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 3.dp) {
                Text(
                    text = pigSound.content,
                    modifier = Modifier.padding(all = 4.dp),
                    style = MaterialTheme.typography.bodySmall
                )
            }
        }
    }
}

 

이렇게 도형을 씌워놓을수 있다.

 

어두운 테마 사용

 

마테리얼을 통해서 기본적으로 다크모드를 지원한다고 한다. 그래서 Material Design 색상,텍스트,배경을 사용하면 다크모드를 알아서 지원한다고 한다.

미리보기또한 함수에 어노테이션을 추가해서 다크모드 미리보기도 볼 수 있다고 한다.

@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "DARK MODE"
)
@Composable
fun PreviewPigMessage() {
    ComposeTutorialTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            PigMessage(
                PigSound(
                    "돼지가 말하였다",
                    "나는 여전히 배가고프다."
                )
            )
        }
    }
}

 

이렇게 결과물이 나온다.

다크모드와 라이트모드 관련된 색상은 IDE가 알아서 Theme.kt파일을 만들고 거기에 정의되어있다.

 

 

이런게 지가 알아서 만들어진다 ㅋㅋㅋ

그리고 내부에 값들이 정의되어있다.(컬러값 이런것들)

 

4. 목록 및 애니메이션

목록이라는데 리사이클러뷰 이야기하는것 같다. 리사이클러뷰아 애니메이션으로 재미를 더한다고 하는데 재미없다 너만 재미있지 안드로이드 놈들아!!!

메시지 목록 만들기

리사이클러부를 컴포즈로 구현하는 모습을 보여준다. 일단 리사이블러뷰 자체도 함수화 시켜서 사용한다.(선택사항이겠지만) LazyColumn 혹은 LazyRow를 이용해서 만드는데 화면에 표시되는 부분만 렌더링 해서 리소스를 아낀다고 한다.

어쩃든 이제 예시를 보고 사용법을 봐보자

@Composable
fun Conversation(pigSounds: List<PigSound>) {
    LazyColumn {
        items(pigSounds) { pigSound ->
            PigMessage(pigSound = pigSound)
        }
    }
}
​
@Preview(name ="Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "DARK MODE"
)
@Composable
fun PreviewConversation() {
    ComposeTutorialTheme {
        Conversation(pigSounds = SampleData.conversationSample)
    }
}

자 살펴보면 내부적으로 LazyColumn안에 하위요소로 items가 있는데 items는 List를 매개변수로 받고 람다를 통해 그 리스트의 요소를 수신하며 이제 이 지정해놓은 함수가 요소요소 마다 매번 호출되는것이다.

그냥 감이 오지않는가?

 

그래서 예시를 만들어보면 이렇게 나온다.

 

확장중 메시지에 애니메이션 적용

이제 상태관리 리컴포지션 이런 이야기가 나오는데 이건 컴포즈의 핵심이기도하고 다음 강의에 바로 나오니 그때 집중해서 공부해보는게 나을것 같다.

 

어쩃든 키워드로 remembermutableStateOf를 잘 이용해야 한다고 나와있다. 뭐 이것에 대해 대충 예습한바로는 이걸 통해서 업데이트 하지말고 고정하는 하는 상태값과 ui업데이트 를 위해 업데이트 하는 상태값을 골라내는 용도로 알고있다.

 

그냥 일단 사용법을 보면 어떻게 사용해야하는지 감이오긴한다. 사용하는예시를 살펴보자

@Composable
fun PigMessage(pigSound: PigSound) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))
​
        var isExpanded by remember { mutableStateOf(false) }
​
        Column (modifier = Modifier.clickable { isExpanded = !isExpanded }){
            Text(
                text = pigSound.prefix,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(4.dp))
            Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 3.dp) {
                Text(
                    text = pigSound.content,
                    modifier = Modifier.padding(all = 4.dp),
                    maxLines = if(isExpanded)Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.bodySmall
                )
            }
        }
    }
}

이코드 보면 이제 원래는 uistate같이 가공해서 넘겨줬던 데이터들을 그냥 직접 함수에서 관리하는데 이걸 뭘 업데이트하고 뭘 상태 저장시킬지 결정하기위해 remember랑 mutableStateOf를 쓰는거다.

 

뭐 나쁘지는 않긴한데 함수분리를 아무리 잘해도 함수 자체가 좀만 뭐해도 거대화 되어서 코드 가독성 극악일것같은데 왤케 좋아하는지 모르겠다. 어쩃든 좀 플젝에서 써보고 판단해야겠다.

 

이거 하면 클릭했을때 메시지창이 커졌다 작아졌다 한다. 이거는 문서에서 직접확인하자 (내가 gif를 뜰수는 없자나) 근데 사실 애니메이션이 부드럽게 슈루룩 하고 늘어났다 줄었다 하는게 아니라 그냥 비저블 꺼버린것 마냥 툭하고 사라진다. (애니메이션 맞아?) -> animateCOntentSize이거 적용하면 부드럽게 내려옴

 

일단 좀더 자세히 이야기해보면 clickable 수정자를 이용해서 클릭이벤트를 처리한다고 하는데 Modifier.clickable{}이런게 있고 이거에 람다에 클릭시 뭘해줄지 정의해줄 수 있다. -> 그럼 이제 뭐 여기다 오만잡것 다 넣을수 있는것 같다.

 

예시에서는 메시지창의 팽창여부를 나타내는 상태값이 들어가있는데 이를 변경하는 로직을 넣고 그 상태 값에 맞춰서 모든 행위들을 조절하는 형태로 작성되어있다.

 

여기서는 animateColorAsState함수를 사용하고 animateContentSize를 이용해서 부드럽게 애니메이션으로 변경되게 하는데 실제적으로 사용할 때 찾아보고 쓰는게 좋을것 같다. 지금해봐야 기억도 안남

@Composable
fun PigMessage(pigSound: PigSound) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "대략 돼지이미지",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))
​
        var isExpanded by remember { mutableStateOf(false) }
        val surfaceColor by animateColorAsState(
            if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary
        )
​
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = pigSound.prefix,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(4.dp))
            Surface(
                shape = MaterialTheme.shapes.medium,
                color = surfaceColor,
                shadowElevation = 3.dp,
                modifier = Modifier.animateContentSize().padding(1.dp)
            ) {
                Text(
                    text = pigSound.content,
                    modifier = Modifier.padding(all = 4.dp),
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.bodySmall
                )
            }
        }
    }
}

 

어쩃든 이런식으로 하면 부드러운 애니메이션까지 완료,

듀토리얼이 끝난다.