본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] Compose에서 Calendar 사용하기, LazyColumn Data 구성하기 (3)

by immgga 2023. 10. 16.

이전 포스팅에서는 Compose에서 Bottom navigation bar를 만드는 것으로 마무리 지었는데

이번 포스팅에서는 필자가 Plan Screen의 UI를 만들면서 있었던 일에 대해 설명해 보겠다.

 

UI 설계는 어떻게 하였는가?

UI 설계는 기존 Planner의 UI를 참고해서 만들었다.

기존 planner

위에는 달력을 달고 아래에는 선택된 날짜의 일정들을 보여주는 방식으로 구현해 보겠다.

 

1. 달력 만들기

UI를 구성하려면 먼저 달력을 만들어야 한다.

필자가 Compose에서 제공하는 달력이 있나 찾아보았지만 아직은 관련 달력 라이브러리가 제공되지는 않는 듯했다.

그런데 사용할 만한 라이브러리는 찾을 수 있었다.

https://github.com/kizitonwose/Calendar

 

GitHub - kizitonwose/Calendar: A highly customizable calendar view and compose library for Android.

A highly customizable calendar view and compose library for Android. - GitHub - kizitonwose/Calendar: A highly customizable calendar view and compose library for Android.

github.com

https://github.com/boguszpawlowski/ComposeCalendar

 

GitHub - boguszpawlowski/ComposeCalendar: A Jetpack Compose library for handling calendar component rendering.

A Jetpack Compose library for handling calendar component rendering. - GitHub - boguszpawlowski/ComposeCalendar: A Jetpack Compose library for handling calendar component rendering.

github.com

그중 2번째 라이브러리는 UI 구현 완료 후 찾은 거라서 추가적으로 공부하고 시간 나면 적용해 볼 생각이다.

 

일단 필자는 XML에서 사용하는 CalendarView를 Compose의 AndroidView를 이용해서 구현했다.

우선 날짜가 선택되었을 때 변경될 State를 만들어 주었다.

val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.KOREA)
val date = Date()

var yearState by remember { mutableStateOf(formatter.format(date).split("-").first()) }
var monthState by remember { mutableStateOf(formatter.format(date).split("-")[1]) }
var dayState by remember { mutableStateOf(formatter.format(date).split("-").last()) }

Date()를 SimpleDateFormat으로 yyyy-MM-dd 형식으로 바꾸고 split을 이용해 날짜 정보의 초기 상태를 생성해 주었다.
Date()는 현재 날짜를 알 수 있게 해주는 역할이다.

 

다음으로는 AndroidView를 이용해서 CalendarView를 사용하는 코드이다.

AndroidView(
    modifier = Modifier.fillMaxWidth(),
    factory = { CalendarView(it) }
) { calendarView ->
    val selectedDate = "${yearState}-${monthState}-${dayState}"
    calendarView.date = formatter.parse(selectedDate)!!.time

    calendarView.setOnDateChangeListener { _, year, month, day ->
        yearState = year.toString()
        monthState = (month + 1).toString()
        dayState = day.toString()
    }
}

위의 코드에서 방금 생성한 날짜 State들을 사용하였는 데 사용한 이유를 설명하자면,

필자는 LazyColumn을 이용해서 달력이 Scroll 되도록 만들었는데, 달력이 Scroll 되어서 폰 화면에서 사라지고 나서 나시 폰 화면에 나타나면 날짜가 초기 상태(오늘 날짜)로 변경되어 있기 때문이었다.

말로 풀면 이해가 어려울 듯해서 간단하게 영상으로 봐보자.

 

문제 영상

Compose LazyColumn의 특징으로 인해서 화면에 보일 때만 view를 build 한다고 알고 있기에 이러한 문제가 생긴다고 판단하고, State로 저장해서 Calendar가 화면을 벗어났을 때에도 변경된 날짜가 제대로 적용되게 해 주었다.

calendarview.date를 이용해 선택된 날짜의 정보를 변경할 수 있다.

val selectedDate = "${yearState}-${monthState}-${dayState}"
calendarView.date = formatter.parse(selectedDate)!!.time

참고로 calendarview.date를 사용하려면 자료형이 Long Type이어야 하는데, 그래서 parse를 이용해 String을 Date로 변경해 주고, time을 이용해 Long Type으로 변경해 주는 방식으로 날짜를 선택해 주었다.

 

그렇게 해서 scroll 해서 화면을 넘어가고 난 후에 다시 화면에 보일 때, Calendar가 초기 상태로 돌아가던 문제를 해결하였다.

 

성공 영상

달력 생성까지 끝냈으니 이제는 그날의 일정을 보여주는 LazyColumn을 만들어 보자.

 

2. LazyColumn 구현

영상을 봤으면 알다시피 화면에는 달력, 날짜를 보여주고 Icon이 있는 header와 일정 리스트들로 화면에 구성되었다.

그리고 scroll 하면 달력은 scroll 돼서 사라지고 header 부분은 화면 맨 위까지만 scroll 되고 화면에서 사라지지는 않았다.

이렇게 화면을 구성하려면 LazyColumn의 item을 사용하는 방법을 알아야 한다.

 

이전에 LazyColumn을 처음 배웠을 때 사용했던 것은 items(list) {} 였을 것이다.

사실은 items 말고도 다양한 아이템 구성 함수들을 지원한다.

 

그중에서 필자는 item이랑 stickyHeader 함수를 사용했다.

 

LazyColumn은 말 그대로 Column의 일종이기 때문에 개발자가 원하는 순서대로 item들을 배치해서 LazyColumn을 구성할 수 있다.

필자의 경우에는 calendar -> header -> item의 구성으로 이루어져 있다.

이 글을 보시는 분들도 LazyColumn에서 items 말고도 다른 item들이 더 필요하면 사용해서 LazyColumn을 custom 할 수 있다.

item은 그냥 평범한(?) 단일 item을 뜻한다.

stickyHeader는 스크롤해도 사라지지 않는 item을 원할 때 사용할 수 있다.(이해가 힘들면 위에 올려져 있는 영상을 참고)

items는 모두가 알고 있다고 생각하기에 설명을 하지는 않겠다.

 

item과 stickyHeader, items에서는 Component를 하나만 사용할 수 있는가? 그건 아니다.

자신이 원하는 Component를 마음껏 사용할 수 있다.

나중에 code를 이용해 자세히 설명해 주도록 하겠다.

 

이제 서론은 여기까지 하고 바로 코드를 통해 확인해 보자.

필자의 경우에는 calendar -> header -> item 순서이기 때문에, item -> stickyHeader -> items 순서대로 구현해 주겠다.

 

item 부분 코드

item {
    AndroidView(
        modifier = Modifier.fillMaxWidth(),
        factory = { CalendarView(it) }
    ) { calendarView ->
        val selectedDate = "${yearState}-${monthState}-${dayState}"
        calendarView.date = formatter.parse(selectedDate)!!.time

        calendarView.setOnDateChangeListener { _, year, month, day ->
            yearState = year.toString()
            monthState = (month + 1).toString()
            dayState = day.toString()
        }
    }
}

이전에 설명한 calendar 코드를 item 안에 넣어 주면 끝이다.

 

stickyHeader 코드

stickyHeader {
    ScheduleHeader(month = monthState, day = dayState) {

    }
}

stickyHeader도 item이랑 사용법이 똑같다.

ScheduleHeader는 필자가 만든 Composable Component이기에 stickyHeader 안에 여러분들이 원하는 header를 구성해 주시면 되겠다.

 

items 코드

items(list) {
    ScheduleItem(
        checked = checkBoxList[it - 1],
        onCheckBoxClick = {

        }
    )

    Divider(
        modifier = Modifier
            .height(1.dp)
            .padding(horizontal = 15.dp)
    )
}

items 부분에는 list를 이용해 ScheduleItem이랑 Divider를 반복해 주었다.

Items에서도 위에서 언급했다시피 Component를 여러 개 사용할 수 있다.

items안에 list 만큼 ScheduleItem이랑 Divider가 계속 만들어질 것이다.

 

최종 LazyColumn 코드

// change firebase date
val list = listOf(1,2,3,4,5,6,7,8,9,10)
val checkBoxList = listOf(true, false, false, false, false, false, false, false, false, true)

LazyColumn(modifier = Modifier.fillMaxSize()) {
    item {
        AndroidView(
            modifier = Modifier.fillMaxWidth(),
            factory = { CalendarView(it) }
        ) { calendarView ->
            val selectedDate = "${yearState}-${monthState}-${dayState}"
            calendarView.date = formatter.parse(selectedDate)!!.time

            calendarView.setOnDateChangeListener { _, year, month, day ->
                yearState = year.toString()
                monthState = (month + 1).toString()
                dayState = day.toString()
            }
        }
    }

    stickyHeader {
        ScheduleHeader(month = monthState, day = dayState) {

        }
    }

    items(list) {
        ScheduleItem(
            checked = checkBoxList[it - 1],
            onCheckBoxClick = {

            }
        )

        Divider(
            modifier = Modifier
                .height(1.dp)
                .padding(horizontal = 15.dp)
        )
    }
}

최종 구현 영상

최종 구현 영상

 

정리

  • 이번 포스팅에서는 필자가 Plan Screen을 만들면서 있었던 일을 글로 남겨보았다.
  • stickyHeader는 아직 정식으로 적용된 함수가 아니기 때문에 @OptIn(ExperimentalFoundationApi::class)을 사용해야 제대로 동작한다.
  • 최종 영상에서 위의 top bar는 공책 느낌이 나도록 만들어보자는 아이디어가 번뜩였기 때문에 바로 만들어서 적용해 보았는데 생각보다 잘 어울렸다.
  • calendarView는 자신이 마음대로 custom 하기에는 한계가 많아서 좀 아쉬웠다. 그래도 막상 만들고 나니까 썩 나쁘지 는 생각보다 잘 어울려서(필자 기준) 놀랐다.

포스팅의 코드에 오류가 있으면 댓글로 알려주세요.

728x90

댓글