Android Programming 기초 (2)

2023. 12. 6. 20:03Soft_Ware/Android

안드로이드 스튜디오 개발자 페이지를 기반으로 작성됨.

https://developer.android.com/courses/pathways/android-basics-compose-unit-1-pathway-1?hl=ko

 

Kotlin 프로그래밍 소개  |  Android Basics Compose - First Android app  |  Android Developers

Kotlin에서 Android 앱 빌드를 준비하기 위해 Kotlin의 입문 프로그래밍 개념을 알아봅니다.

developer.android.com

 

이번 블로그는 지난 Android Programming 기초 (1)에 이어서, 텍스트를 활용한 앱 빌드를 복습한 뒤에 텍스트 뿐만 아니라 이미지 컴포넌트를 앱에 추가하는 방법을 살펴보고, 정렬을 가능하게 하는 composable 함수를 활용하는 방법에 대해서 소개해보고자 한다.

 

이전 블로그는 다음을 참조하면 될 것이다.

https://heavy-rainy.tistory.com/66

 

  • 텍스트 Composable 함수를 활용한 간단한 앱 빌드
  • 이미지 Composable 함수를 활용한 간단한 앱 빌드
  • 정렬을 연습하기 위한 간단한 프로젝트

 

텍스트 Composable 함수를 활용한 간단한 앱 빌드

 

이전 블로그에서는 단순히  Hello Android! 에서 텍스트를 변경하고 Surface 함수를 씌워 배경색을 바꾸는 작업을 진행했었다.

 

이번 블로그에서는 텍스트 함수에 조금 더 초점을 두어 생일축하 메세지 앱을 만들어보자.

 

우선, 새로운 프로젝트를 생성하고 적절한 프로젝트 이름을 지어주자. 필자는 "Happy Birthday"라고 명명하였다.

 

처음 프로젝트를 생성하면, MainActivity.kt에는 MainActivity class와 greeting composable 함수, greeting composable preview 함수가 정의되어 있을 것이다.

 

의미 있는 이름을 짓기 위해서 greeting composable 함수를 삭제하고 greeting composable preview 함수의 이름을 수정해보자.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HappyBirthdayTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                // composable 함수 호출 부분 삭제
                }
            }
        }
    }
}

// greeting composable 함수 정의 삭제

@Preview(showBackground = true)
@Composable
fun BirthdayCardPreview(){ // function 이름 변경
    HappyBirthdayTheme { 
    }
}

 

위와 같이 확인할 수 있다.

 

프로젝트 이름에 맞게 생일 축하 메세지를 보내는 composable 함수를 추가해보도록 하겠다.

 

@Composable
fun GreetingText(message: String, modifier: Modifier = Modifier) {
    Text(
        text = message
    )
}

@Preview(showBackground = true)
@Composable
fun BirthdayCardPreview() {
    HappyBirthdayTheme {
        GreetingText(message = "Happy Birthday Sam!")
    }
}

 

Composable 함수 이름은 GreetingText로 명명하였고, Preview하는 부분에 해당 함수가 호출된 것을 확인할 수 있다. setContent 함수에도 마찬가지로 해당 함수를 호출시켜주면 되겠다.

실행 결과 아래와 같은 Text를 얻어낼 수 있다.

 

이제 텍스트 글꼴의 크기를 변경해보고자 한다. 글꼴의 크기는 확장 가능한 픽셀인 SP를 기준으로 설정이 되는데, 이것은 이전 블로그에서 설명한 레이아웃에서 사용하는 픽셀단위인 DP와 크기가 동일하긴 하나, 사용자가 선택한 휴대전화 폰트 크기에 따라서 크기가 조절된다. 

Text(
    text = message,
    fontSize = 100.sp,
    textAlign = TextAlign.Center
)

 

위와 같이 fontSize라는 매개변수를 지정하여 텍스트의 크기를 조절할 수 있는데 이 때 매개변수 단위를 맞춰줘야 하므로 .sp라는 확장자를 적용해준다. 이렇게 하면 기존 Int type에서 sp타입으로 값이 변환된다.  아마 처음에 sp가 빨간색 텍스트로 뜰 텐데, 이것은 패키지가 없는 것이라서 Alt + Enter를 통해 Import 시켜주면 된다. 추가적으로 textAlign을 통해 텍스트를 가운데 정렬 해주었다.

 

 

실행 결과 위와 같이 텍스트가 커짐에 따라서 겹치는 현상이 생긴다.

이유는 Text compsoable함수에서 각 라인에 대한 높이가 default로 정해져 있을텐데 그 값보다 텍스트가 너무 크기 때문에 겹쳐져서 보이는 것이다. 아래와 같이 각 라인에 대한 높이를 지정하는 lineHeight를 적절히 조절해주면 원하는 결과가 생성될 것이다.

 

Text(
    text = message,
    fontSize = 100.sp,
    lineHeight = 116.sp,
    textAlign = TextAlign.Center
)

 

 

이 상태에서 생일 축하 메세지를 누가 보냈는지 다른 텍스트를 표시해보자.

방법은 아래와 같이 GreetingText 함수에 또 다른 Text composable함수를 추가해주고 매개변수를 추가해주면 된다.

Emma 라는 사람이 보냈다라는 것을 표시하기 위해서 from Emma라는 텍스트를 추가하는 코드를 살펴보자.

@Composable
fun GreetingText(message: String, from: String, modifier: Modifier = Modifier) {
    // from이라는 매개변수 추가
    Text(
        text = message,
        fontSize = 100.sp,
        lineHeight = 116.sp,
        textAlign = TextAlign.Center
    )
    // 새로운Text composable 함수 추가 
    Text(
        text = from,
        fontSize = 36.sp,
    )
}

@Preview(showBackground = true)
@Composable
fun BirthdayCardPreview() {
    HappyBirthdayTheme {
        // GreetingText의 from 인수를 전달
        GreetingText(message = "Happy Birthday Sam!", from = "From Emma")
    }
}

 

결과를 확인해보면 뭔가 어색하다. 텍스트가 일단 겹쳐보이기도 하고 누가 보냈는지 적을 때에는 통상 우하단에 적기 때문이다.  단순히 Text composable함수 두 개를 이어서 놓았기 때문에 이전에 작성한 Happy Birthday Sam! 위에 From Emma가 겹쳐서 쌓인 것이다.

 

우리가 원하는 결과를 얻기 위해 텍스트를 정렬하는 방법에 대해서 알아보자.

 

UI 계층 구조

기본적으로 UI 계층은 포함에 기반을 한다. 하나의 구성 요소에 여러개 혹은 하나의 구성요소를 포함할 수도 있다. 어떤 UI 요소는 하위 요소가 되고 어떤 UI 요소는 상위 요소가 될 수 있는데, 우리는 상위 요소 역할을 할 수 있는 즉 다른 UI요소를 포함시킬 수 있는 Box, Column, Row composable 함수에 대해서 살펴보도록 하자.

 

위 세가지의 composable 함수 중에서 Box가 가장 기본적인 상위 요소 역할을 하는 함수이고 Column, Row는 각각 세로, 가로로 배치할 때 사용되는 composable 함수이다. 다음과 같은 예를 살펴보자.

 

 Column{
     Text("Row1 Text")
     Text("Row2 Text")
 }

 

이렇게 코드를 작성하면 "Row1 Text"와 "Row2 Text"가 세로로 정렬되어 배치될 것이다.

반대로 Row composable 함수를 활용하면 가로로 정렬되어 배치된다.

 

이러한 쓰임에서 살짝 이해가 되지 않는 부분이 있을 수 있는데, 함수를 호출할 때 보통 괄호를 통해 인수를 전달하는데, 위 코드의 Column은 괄호가 없고 중괄호만 있는 것을 확인할 수 있다.

 

이것은 Kotlin의 문법인데, 마지막 매개변수가 함수일 때에는 함수의 본문을 포함하는 매개변수를 전달할 수 있는 특수한 문법과 후행 람다 문법을 함께 활용한 것이다. 함수 본문을 괄호 안에 인자 이름과 함께 전달하는 것이 아니라 중괄호를 통해 함수의 본문만 전달하는 형태이다. 즉, 함수 이전의 인자는 모두 생략되고 함수 인자의 이름까지 생략되어 바로 중괄호가 튀어나오는 것이다. Column 위에 마우스를 올려두면 다음과 같은 설명이 나오고, 확인해보면 우리는 content의 composable함수를 인수로 전달한 형태이다.

 

 

 

후행 람다 문법을 활용하지 않고 람다 표현식만 사용하는 경우 코드가 아래와 같아지기 때문에 번거러울 수 있다.

 

Row(
    content = {
        Text("Some Text")
        Text("Another Text")
    }
)

 

람다 표현식 및 후행 람다 표현식은 아직 정확하게는 숙지하지 않아도 되고 필자는 구글링을 통해 이해하는데 도움을 받을 수 있었다.

 

이제, 상위 요소가 가능한 composable함수인 Box, Column, Row를 통해 이전에 만든 생일 축하 카드 앱의 정렬을 시켜보도록 하자.

 

Text 함수에 커서를 갖다 대고 Alt + Enter를 누르고, Surround with widget → Surround with Column 을 통해 두 Text 함수를 Column으로 씌워보자. 그러면 두 개의 Text가 겹치지 않고 세로를 기준으로 쌓이는 것을 확인할 수 있다.

 

최종적으로 우리가 원하는 우 하단을 만들기 위해서는 Modifier를 활용해야 한다. Text함수의 인수로 modifier를 지정해줄 수 있는데, 다음과 같이 Padding과 함께 align이라는 메서드를 활용해주면 해당 텍스트를 원하는 위치로 옮길 수 있다. 최종적인 코드와 실행 화면은 아래와 같다.

 

@Composable
fun GreetingText(message: String, from: String, modifier: Modifier = Modifier) {
    Column{ // Column 추가
        Text(
            text = message,
            fontSize = 100.sp,
            lineHeight = 116.sp,
            textAlign = TextAlign.Center
        )
        Text(
            text = from,
            fontSize = 36.sp,
            // modifier를 활용한 텍스트 정렬 및 패딩 추가
            modifier = Modifier
                .padding(16.dp)
                .align(alignment = Alignment.End)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun BirthdayCardPreview() {
    HappyBirthdayTheme {
        // GreetingText의 from 인수를 전달
        GreetingText(message = "Happy Birthday Sam!", from = "From Emma")
    }
}

 

 

 

근데 이렇게 하고 앱에 대해서 직접 실행을 시키게 되면, 텍스트가 상단에 위치할 것이다. 이 경우 Column 함수의 매개변수로 verticalArrangement 부분을 조정해주면 된다. 또한 실제 스마트폰 기기나 에뮬레이터 기기와 동일한 크기로 Column 요소를 확장하기 위해서 Modifier에 fillMaxsize()라는 메서드를 추가해줘도 좋다.

Column(
    // 세로 정렬
    verticalArrangement = Arrangement.Center,
    // 빌드하는 기기와 동일한 크기로 요소를 확장시켜줌
    modifier = Modifier.fillMaxSize()
) {
    ...
}

 

 

이미지 Composable 함수를 활용한 간단한 앱 빌드

 

이번에는 이전에 만든 프로젝트를 기반으로 배경을 더해보고자 한다.

이미지는 링크에서 다운로드 받을 수 있다.

 

이후 Android Studio에서 다운로드 받은 이미지를 업로드 하는 과정에 대해서 살펴보자.

 Android Studio에서 View → Tool Windows → Resource Manger를 클릭한다.

 

 

Resource Manger 창을 누르게 되면 현재 가지고 있는 resource가 보일 것이고 우리는 다운로드 받은 이미지를 업로드하기 위해서 좌상단의 + 버튼을 눌러 Import drawables를 클릭하자. 이후 다운로드 받은 경로를 찾아 androidparty를 찾아 클릭한다. 여기까지 따라왔다면 다음과 같은 Import drawables 대화 상자가 나타날 것이다.

 

 

이제, Qualifier type을 이미지이기 때문에 Density 로 설정을 해주고 value는 No density로 지정해주자.

Resource type에 따라서 위 타입들을 지정해줄 수 있고 다른 Resource를 다룰 기회가 있다면 좀 더 자세히 설명하기로 한다.  No density는 모든 밀도에 대한 resource를 의미하게 되고 현재 화면에 관계 없이 시스템에서 resource 크기를 조정하지 않는다고 한다. 한마디로 value에 따라 resource의 dpi를 조정해주는 셈이고 nodpi는 조정하지 않겠다는 의미이다.

 

이후 Next를 클릭하고 이미지가 배치될 디렉토리 구조를 확인한 뒤, Import를 클릭해주면 완료된다.

이제 좌측의 android 파일 구조로 설정하고 res → drawable 을 클릭해보면 우리가 추가한 이미지가 정상적으로 위치하는 것을 확인할 수 있을 것이다.

 

이제 이미지를 추가했으니 코드를 통해 앱에 적용하는 작업을 진행해보도록 하자.

이미지도 텍스트와 마찬가지로 composable 함수를 통해 적용시켜줄 수 있는데, 이전에 작업했던 함수는 greetingText로 우리의 의도는 텍스트를 표시하고자 했던 것이다. 따라서 이미지를 표시할 수 있는 또 다른 composable 함수를 만들어주는 것이 적절하다.

 

그 이전에 Jetpack Compose에서 resoure에 접근하는 방법에 대해서 소개하고자 한다.

 

우선, 프로젝트 안에서 resource는 그룹화 되어 있다. 좌측 파일 구조의 res 디렉토리 안에 사용할 resource들이 그룹핑되어 있고 drawable, mipmap, values로 나누어져 있을 것이다. 다음 세가지 분류는 필자가 가지고 있는 resource의 예시이다. 

  • drwable 디렉토리에는 이미지 resource
  • mipmap 디렉토리에는 런처 아이콘
  • values 디렉토리에는 문자열 resource

 

이러한 resource에 액세스 하는 방법은 프로젝트에서 제공하는 R class 를 활용하는 것이다.

Android에서 자동적으로 생성되는 class로, 프로젝트 내 모든 resource에 대한 ID를 제공하게 된다.

우리가 추가한 이미지에 대한 resource ID는 다음과 같다.

R.drawable.androidparty

 

 

※ 리터럴 문자열을 resource로 변환하여 사용하는 방법

더보기

앱에서 사용하는 Text안의 문자열을 재사용하거나 코드의 가독성 혹은 간결성을 위해서 String을 resource로 바꾸는 방법에 대해서 살펴보고자 한다. 정적인 리터럴 문자열을 사용하는 것보다 유지, 보수도 조금 더 편하고 코드의 간결성 측면에서도 이점이 더 있는 것 같다.

 

우선 prinln이나 변수 정의에 있는 string에 커서를 갖다 대자.

이후 Alt + Enter를 눌러 Extract string resource tap을 클릭해보자.

그러면, Extract Resource 대화 상자가 열릴텐데, 여기서 Resource Name과 Resource Value를 지정하여 resource를 만들어줄 수 있다.

 

 

위와 같은 형태이다.

 

OK를 누르고 좌측 파일구조에서 res → values → string.xml에 들어가보면 우리가 추가한 string resource가 보일 것이다.

 

 

 

필자의 경우 변수 정의에 예시를 적용해보았는데 리터럴 문자열 대신 아래와 같이 stringResource에 Resource ID를 인수로 전달해 해당 string을 대입할 수 있었다.

 

 

이제 학습한 resource ID를 기반으로 특정 image 변수에 resource를 대입해주는 코드와 함께 Image composable 함수를 추가하는 방법에 대해서 코드로 살펴보자.

 

@Composable
fun GreetingImage(modifier: Modifier = Modifier) {
    // 사용할 resource를 painterResource를 통해 변수화
    val image = painterResource(R.drawable.androidparty)
    
    // UI를 구성하는 Image composable 함수 호출
    Image(
        painter = image,
        // Image 등 content를 설명하는 contentDescription에 대한 설정
        // 우리는 단순히 장식용이므로 null 값을 대입.
        contentDescription = null
    )
}

 

 

이제, 앞서 적용했던 텍스트를 이미지 위에 나타내도록 해보자.

UI 요소들을 서로 쌓기 위해서 Box 레이아웃을 사용한다. 이것이 그룹핑해서 처리하기도 추후에 용이하기에 이런식으로 처리하는 것 같고 현재 프로젝트에서는 Box 레이아웃으로 wrapping 하든 안하든 결과는 동일하다.

 

@Composable
fun GreetingImage(message: String, from: String, modifier: Modifier = Modifier) {
    // GreetingText를 위한 매개변수 추가
    val image = painterResource(R.drawable.androidparty)
    Box { // Box 레이아웃 추가
        Image(
            painter = image,
            contentDescription = null
        )
        // 기존 구현했던 GreetingText composable함수 추가
        GreetingText(
            message = message,
            from = from,
            modifier = Modifier
                .fillMaxSize()
                .padding(8.dp)
        )
    }
}

 

Composable 함수라는 표현과 레이아웃을 번갈아가며 표현하고 있는데 여기서는 동일한 의미로 받아들여도 될 것 같다.

 

하지만, 실행해보면 아래와 같이 이미지의 높이가 화면보다 작기 때문에 하단부에 공백이 생기는 것을 확인할 수 있다.

 

잘 안보이지만 하단부를 잘 보면 공백이 있는 것을 알 수 있다.

여튼, 이 경우에 Image composable의 매개변수인 ContentScale을 조정해서 해결할 수 있다. 

ContentScale.Crop을 활용하여 이미지의 너비와 높이가 화면의 크기와 상응하거나 더 커지도록 조정해준다.

        Image(
            painter = image,
            contentDescription = null,
            contentScale = ContentScale.Crop
        )

 

정상적으로 하단부의 공백이 채워진 것을 확인할 수 있다.

마지막으로 장식용이긴 하나, 앱의 대비를 좀 더 개선하기 위해 이미지의 불투명도를 조정하는 방법에 대해서 소개한다.

Image composable에서 alpha라는 매개변수를 조정해주면 되는데 이 값은 float 값으로 지정해줄 수 있다.

코드와 결과는 아래와 같다.

        Image(
            painter = image,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            alpha = 0.5F
        )

 

결과를 살펴보면 이미지가 조금 더 투명해진 것을 알 수 있다.

alpha 값은 작을수록 투명도가 높아지고 1일 때 원본 이미지와 동일하다.

 

정렬을 연습하기 위한 간단한 프로젝트

 

Android Developer 강의를 따라가다 보면 연습으로 텍스트와 이미지 composable 함수를 활용한 여러가지 연습용 프로젝트를 진행해보라고 권장하는데, 가장 난해했던 것을 한번 소개해보기로 한다.

 

Compose 사분면이라는 과제인데, 4사분면으로 나누어 텍스트를 작성해보는 그런 연습 프로젝트이다.

 

 

최종적으로 위의 모양을 만들어야 한다고 한다.

 

새롭게 프로젝트를 만들어보자.

Quadrant Practice 이름을 가진 Android project를 생성해서 진행을 해보자.

 

생성하자마자 Greeting composable 함수를 삭제하고 새로운 이름을 가진 함수를 생성하자.

Composable 함수 Naming 관례에 따라서 명사로 지어주었다. 

이제 4분면 중에서 1,2 사분면을 먼저 채우고 Column을 통해 3,4 사분면을 채워보고자 한다. 

 

각 사분면에 채울 텍스트를 반복해서 작성해주기엔 비효율적이므로 제목, 내용, 배경 색상을 인자로 받는 composable 함수를 다음과 같이 새로 만들어서 정의를 해주었다.

 

@Composable
fun ComposableFunctionInfo(title: String, description: String,
                           colorCode: Long, modifier : Modifier = Modifier) {
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = modifier
            .fillMaxHeight()
            .background(color = Color(colorCode))
            .padding(16.dp)
    ) {
        Text(
            text = title,
            // 글씨체를 Bold체로 설정
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        Text(
            text = description,
            // TextAlign.Justify 를 적용하면 각 라인의 첫 번째 단어와 끝 단어를 정렬해줌.
            textAlign = TextAlign.Justify
        )
    }
}

 

1,2 사분면은 Row를 통해 화면을 채울 수 있다. TextAlign.Justify는 주석에 쓰인것과 같이 텍스트의 각 줄에 첫 단어와 끝 단어의 위치가 정렬되도록 해주는 값이다.

 

여기서 각 사분면의 텍스트 길이 때문에 단순히 Row, Column으로 배치하면 하나의 요소만 배치되고 나머지는 생략되는 현상이 생긴다.

 

위와 같이 Text composable 오른쪽에는 Image composable이, Row composable 오른쪽에는 Column composable이 있어야 하는데도 말이다.

여기서 사용할 수 있는 메서드가 Modifier의 weight라는 메서드이다.  이것은 Row나 Column에서 차지하는 비율을 설정하는 메서드로 2개 이상의 요소에 대해서 비율을 지정해주면 된다. Float 형태로 지정해줘야 해서 예를 들어 1f, 1f이면 1:1의 비율이고 0.3f: 0.7f이면 3:7의 비율이 되겠다.

 

이렇게 Row에 대해서, Column에 대해서 weight method를 통해 위 앱의 형태처럼 UI를 구성할 수 있고 코드와 결과는 아래와 같다.

@Composable
fun QuadrantUI(modifier: Modifier = Modifier) {
    Column {
        Row(
            modifier = Modifier.weight(0.5f)
        ) {
            ComposableFunctionInfo(
                title = stringResource(R.string.text_composable_title),
                description = stringResource(R.string.text_composable_desc),
                colorCode = 0xFFEADDFF,
                modifier = Modifier.weight(0.5f)
            )
            ComposableFunctionInfo(
                title = stringResource(R.string.image_composable_title),
                description = stringResource(R.string.image_composable_desc),
                colorCode = 0xFFD0BCFF,
                modifier = Modifier.weight(0.5f)
            )
        }
        Row(
            modifier = Modifier.weight(0.5f)
        ) {
            ComposableFunctionInfo(
                title = stringResource(R.string.row_composable_title),
                description = stringResource(R.string.row_composable_desc),
                colorCode = 0xFFB69DF8,
                modifier = Modifier.weight(0.5f)
            )
            ComposableFunctionInfo(
                title = stringResource(R.string.column_composable_title),
                description = stringResource(R.string.column_composable_desc),
                colorCode = 0xFFF6EDFF,
                modifier = Modifier.weight(0.5f)
            )
        }
    }
}

@Composable
fun ComposableFunctionInfo(title: String, description: String,
                           colorCode: Long, modifier : Modifier = Modifier) {
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = modifier
            .fillMaxHeight()
            .background(color = Color(colorCode))
            .padding(16.dp)
    ) {
        Text(
            text = title,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        Text(
            text = description,
            textAlign = TextAlign.Justify
        )
    }
}


@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    QuadrantPracticeTheme {
        QuadrantUI()
    }
}

 

전체 코드는 https://github.com/seof622/kotlin_android_practice/tree/master/practice3 를 참조하면 될 것이다.