안드로이드 연구소

안드로이드 Jetpack Compose(컴포즈) 제일 쉽게 배우기/예제/강의 (ToDo만들기) 본문

안드로이드 연구소/Jetpack Compose

안드로이드 Jetpack Compose(컴포즈) 제일 쉽게 배우기/예제/강의 (ToDo만들기)

안드로이드 연구원 2023. 9. 1. 16:14

안녕하세요. 안드로이드 연구원입니다.

처음 블로그를 할 때만 해도 완전 봄이었는데

이제 선선한 가을 날씨로 바뀐 것 같네요.

 

지난 Jetpack Compose를 공부하겠다는 4라운드 프로젝트는 계획대로 된게 하나도 없네요ㅎㅎ;;

(이전 라운드들은 계획한대로 안된게 없었는데)

 

제가 코로나에도 걸리고

실력 검증을 위한 기업에 지원했던 기업에서 코테를 오랜시간 쓰기도 했고

무엇보다 컴포즈가 만만한 녀석이 아니었습니다. 

 

컴포즈를 단순히 xml을 대체하고 컴포넌트들을 배우면 되겠지라고 생각을 했는데

우리가 사용한 엑티비티에서 사용하는 문법이 전부 바뀌게 되더라구요.

 

우선 앞으로 보여드릴 컨텐츠는 아래와 같습니다.

1. ToDo 어플리케이션 만들면서 기초적인 컴포즈 설명

2. ViewModel과 LiveData 사용하기 

 

추후에 컴포즈를 최적하는 파트와

컴포즈로 클린아키텍처로 만드는 파트가 있을 예정입니다.(그건 그때가서 자세한 일정으로~)

 

오늘 첫번째 ToDo어플리케이션을 만들면서 컴포즈를 배워보도록하겠습니다. 


1. 첫번째 Compose 파일 만들기

새로운 파일을 만들면 Empty Activity가 있는데

저 폴더를 누르면 컴포즈 파일을 만드실 수 있습니다.

이 부분은 안드로이드 버전마다 다르게 나옵니다.(제 안드로이드 버전은 Android Studio Giraffe | 2022.3.1)

만들고서 build.gradle(app)에 가면 dependencies에 머테리얼 버전을 확인하실 수 있습니다.

implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material")

위에는 머테리얼3, 아래는 그냥 기존의 머테리얼1

최신 버전의 안드로이드 스튜디오는 머테리얼3밖에 만들어지지 않습니다.

 

이걸 얘기하는 이유는 이 버전에 따라 진짜 조금씩 코드 스타일이 다릅니다.

그럼 저는 최신 버전인 머테리얼3으로 진행해보도록 하겠습니다.

 

 

2. 컴포즈 엑티비티 기본 구조

이곳은 기존 메인엑티비티와 같은 듯 다른 듯합니다.

onCreate 생명주기안에 컴포즈 함수들이 있을 호출하면 됩니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TodoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    
                    [이곳에서 아래에서 만든 컴포즈 함수를 호출하는 곳]
                    
                }
            }
        }
    }
}

 

아래에 @Composable로 어노테이션된 함수에서 Compose로 UI를 그리면 됩니다.

@Composable
fun TopLevel(){
	
}

 

마지막으로 @Preview에서 위에서 그린 컴포즈 함수를 연결시키면

오른쪽에서 split으로 UI가 어떤 모습인지 바로 보실 수 있습니다. 

@Preview(showBackground = true)
@Composable
fun Preview() {
    TodoTheme {

    }
}

 

 

3. 컴포즈 컴포넌트 구조

기본적으로 onCreate에 만들어지는 Surface를 두고 컴포넌트에 대해 설명해보겠습니다.

Surface는 Text, Buttom, Image, Raw, Column와 같은 컴포넌트입니다.

- 기존 xml에서는 TextView, Button, ImageView같은 녀석입니다.

- 또 html에서는 <Text>, <Image>같은 태그들이고요.

여기서 Surface는 다른 컴포넌트들을 감싸는 역할을 합니다.

Surface

 

 

Surface안에 다른 컴포넌트를 생성한다면 아래처럼 사용할 수 있습니다.

{ } 중괄호 안에 다른 컴포넌트를 호출하고 여러개를 사용할 수 있습니다.

Surface {
    Text (text = "Hello")
    Text (text = "android lab")
}

 

만약 Surface의 디자인적인 요소를 바꾼다면 아래처럼 사용합니다.

( ) 소괄호 안에서 modifier를 사용해서 아래와 같이 호출합니다.

html의 css같은 부분이네요.

아래는 화면 크기만큼 꽉채워서 보여주는 내용입니다.

Surface(
    modifier = Modifier.fillMaxSize(),
) {
    Text (text = "Hello")
    Text (text = "android lab")
}

 

이외에도 다양한 컴포넌트도 modifier사용방법들,

modifier이외에도 컴포넌트 내부 설정 방법들이 있지만

이건 사용해보면서 익히는 방법이 최고라고 생각합니다.

(설명해주는 강의를 들었지만 다 날라가버린지 오래)

 

 

4. Todo 앱 만들기(1) - 상단 입력창 만들기

TopLevel()이라는 컴포즈 함수를 따로 분리해서

해당 함수에서 UI를 그려보도록 하겠습니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TodoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    TopLevel()
                }
            }
        }
    }
}

 

아래처럼 Preview로 연결해주면 UI가 그려지는 것도 바로 확인할 수 있습니다.

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

 

그렇다면 이제 TopLevel() 컴포즈 함수를 보겠습니다.

너무 복잡해보이는 것 같지만 컴포넌트 하나하나를 보면 이해하기

// 최상위
@Composable
fun TopLevel(){
    // (0)OutlinedTextField에서 넣은 데이터
    val (text, setText) = remember { mutableStateOf("") }

    // (1)세로 컬럼 
    Column {
    	// (2)상단 입력창
        Row(modifier = Modifier.padding(8.dp)) {
        
            // (3)입력창
            OutlinedTextField(
            	value = text, 
                onValueChange = {}, 
                modifier = Modifier.weight(1f)
            ) 
            
            // (4)빈공간
            Spacer(modifier = Modifier.size(8.dp))
            
            // (5)버튼
            Button(onClick = {  }) { 
                Text("입력")
            }
        }
    }

}

(1)Column은 내부 컴포넌트들을 세로로 나열시킬는 컨테이너같은 역할을 합니다.

제일 바깥에 설정을 해서 Row컴포넌트와 그 이후에 놓일 컴포넌트를 세로로 나열하겠다는거죠.

 

(2)Row는 컬럼과 반대로 가로로 나열시킵니다.

여기서 OutlinedTextField, Spacer, Button 세가지 컴포넌트를 가로로 정렬시킵니다.

-OutLinedTextField: 입력창(like; EditText)

-Spacer: 빈공간

-Button: 버튼

 

(3)그리고 OutLinedTextField의

onValueChage와 위의 setText와 연결하여 데이터 변경된 값이 넣어지도록 하고

value와 위 text를 연결하여 UI가 변경되도록 합니다.

val (text, setText) = remember { mutableStateOf("") }

OutlinedTextField(
    value = text, 
    onValueChange = setText, 
    modifier = Modifier.weight(1f)
)

 

완성하여 이런 UI가 짜잔.

처음에 이거 되면 기분좋음. 벌써 컴포즈 마스터한거 같음

 

 

5. Todo 앱 만들기(2) - 상단 입력창 버튼

이제 OutLinedTextField 입력창에서 값을 넣고

[입력]버튼을 누르면 데이터가 저장이 되도록 해보겠습니다.

// 최상위
@Composable
fun TopLevel(){
    // OutlinedTextField에서 넣은 데이터
    val (text, setText) = remember { mutableStateOf("") }
  
    // (1)LazyColumn의 리스트 아이템으로 보여줄 데이터
    val toDoList = remember { mutableStateListOf<TodoData>() }

    // (2)입력 로직
    val onSubmit: (String) -> Unit = { text ->
        val key = (toDoList.lastOrNull()?.key ?:0) + 1
        toDoList.add(TodoData(key, text))
        setText("")
    }

    // 세로 컬럼 
    Column {
    	// 상단 입력창
        Row(modifier = Modifier.padding(8.dp)) {
            // 입력창
            OutlinedTextField(
            	value = text, 
                onValueChange = setText, 
                modifier = Modifier.weight(1f)
            ) 
            
            // 빈공간
            Spacer(modifier = Modifier.size(8.dp))
            
            // (3)버튼의 onClick에 2번 연결
            Button(onClick = { onSubmit(text) }) 
                Text("입력")
            }
        }
    }

}

 

(1) 입력창에서 받은 text를 차곡차곡 아래 리스트에 담아줄겁니다.

val toDoList = remember { mutableStateListOf<TodoData>() }

TodoData 데이터 클래스는 따로 파일을 생성해서 만들어주면 더욱 깔끔합니다.

 

data class TodoData (
    val key: Int,
    val text: String,
    val done: Boolean = false
)

 

(2) 이제 가장 중요한 전송 이벤트를 담당할 onSubmit을 만들어주는데

String값을 파라미터로 받고, key값을 내부에서 생성해주어서

toDoList에 넣어줍니다. (done값은 false로 초기화해나서 넣을 필요없음)

그리고 마지막으로 setText값을 초기화해서 입력창에 넣은 값을 지워줍니다.

val onSubmit: (String) -> Unit = { text ->
    val key = (toDoList.lastOrNull()?.key ?:0) + 1
    toDoList.add(TodoData(key, text))
    setText("")
}

 

(3) 마지막으로 [입력]버튼 onClick에 위에서 만들어준 onSubmit(text)값을 연결해줍니다.

Button(onClick = { onSubmit(text) }) 
    Text("입력")
}

그렇다면 이제 버튼을 누르면 toDoList에 데이터가 쌓이겠죠?

그렇다면 입력창 부분은 완성~~

 

 

* ToDoInput함수로 컴포즈 분리하기(필수x)

이렇게 마무리 할 수 도 있지만 여기서 입력창을 깔끔하게 함수로 분리해서 만들어보겠습니다.

// 최상위
@Composable
fun TopLevel(){
    val (text, setText) = remember { mutableStateOf("") }
    val toDoList = remember { mutableStateListOf<TodoData>() }

    // 입력 로직
    val onSubmit: (String) -> Unit = { text ->
        val key = (toDoList.lastOrNull()?.key ?:0) + 1
        toDoList.add(TodoData(key, text))
        setText("")
    }

    // 컬럼
    Column {
        TodoInput(text, setText, onSubmit)
    }
}

// 상단 입력창
@Composable
fun TodoInput(text: String, onTextChange: (String)->Unit, onSubmit: (String)->Unit){
    Row(modifier = Modifier.padding(8.dp)) {
        OutlinedTextField(value = text, onValueChange = onTextChange, modifier = Modifier.weight(1f)) // 입력창
        Spacer(modifier = Modifier.size(8.dp)) // 빈공간
        Button(onClick = { onSubmit(text) }) {// 버튼
            Text("입력")
        }
    }
}

깔끔 (출처: https://jjalbang.today/view/%EC%A1%B0%EC%84%B8%ED%98%B8/7699)

 

6. Todo 앱 만들기(3) - 리스트 만들기

이제 toDoList데이터를 출력하는 일만 남았네요.

이럴때 필요한 것은 Recyclerview겠죠.

하지만 컴포즈에서는 이거 안쓴다고한답니다.(ㄷㄷ)

 

대신에 컴포즈에서는 LazyColumn 컴포넌트를 사용합니다.

아래처럼 items안에서 위에서 만들어놓은 toDoList를 받아서

데이터를 ToDo()에서 데이터 바인딩해줍니다.(Recyclerview의 onViewBinding같은 느낌?)

val toDoList = remember { mutableStateListOf<TodoData>() }

Column {
    TodoInput(text, setText, onSubmit)
    
    LazyColumn{
        items(toDoList, key = { it.key }){ toDoData ->
            ToDo(todoData = toDoData)
        }
    }
}

 

ToDo()는 Recyclerview의 CreateViewHolder정도 되겠네요.

사실 위와 전혀 다르지 않은 컴포즈 컴포넌트 만드는 것 뿐입니다.

가로 컨테이너 Row안에 체크박스와 텍스트가 하나씩 각 각 만든다.

 

그리고 CheckBox는 checked에 toDoList의 값 중 하나인

toDoData의 done값을 연결하여 체크표시를 해준다.

 

Text는 toDoData의 text와 연결해서

출력된 글짜를 표시한다. 

@Composable
fun ToDo(todoData: TodoData){
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.padding(8.dp)
    ){
        Checkbox(
            checked = todoData.done,
            onCheckedChange = { checked -> }
        )
        
        Text(
            text = todoData.text,
            modifier = Modifier.weight(1f)
        )
    }
}

 

CheckBox의 체크 여부에 따라 

toDoList의 done값이 바뀌게끔 해야겠네요.

onToggle에서 done값을 바꾼 newData를 변경해줍니다.

val onToggle: (Int, Boolean) -> Unit = { key, checked ->
    val index = toDoList.indexOfFirst { it.key == key }
    val newData = toDoList[index].copy(done = checked)
    toDoList[index] = newData
}

 

그리고 CheckBox의 onCheckedChange에서

위에서 만들어준 onToggle을 연결해줍니다.

@Composable
fun ToDo(
    todoData: TodoData,
    onToggle: (key: Int, checked: Boolean) -> Unit = { _, _-> }
){
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.padding(8.dp)
    ){
        Checkbox(
            checked = todoData.done,
            onCheckedChange = { checked -> onToggle(todoData.key, checked)}
        )

        Text(
            text = todoData.text,
            modifier = Modifier.weight(1f)
        )
    }
}

 

이렇게 하면 아래처럼 간단한 Todo 애플리케이션을 구현할 수 있습니다.


원래 강의에서는 생성한 아이템을 수정과 삭제하는 기능이 있지만

저것까지 한다면 너무 복잡할 듯 하네요.

혹시 해당 기능이 필요한 분이 있다면 댓글에 남겨주시면 파일을 전달해드리겠습니다.

 

그렇다면 컴포즈로 간단하게 만들어본 Todo 애플리케이션은 이렇게 종료해보겠습니다.

감사합니다.

Comments