From 5f14bf4deaf0d73124023b9cc3f30cc1385f64cb Mon Sep 17 00:00:00 2001 From: roel Date: Sun, 29 Dec 2024 18:01:06 +0900 Subject: [PATCH] [Feat/#2] Migrate from java.time to kotlinx.datetime for broader compatibility --- app/build.gradle.kts | 3 +- .../com/dongchyeon/calendar/MainActivity.kt | 9 ++- calendar/build.gradle.kts | 3 +- .../com/dongchyeon/calendar/CalendarEvent.kt | 2 +- .../calendar/model/DateYearMonth.kt | 39 ++++++++++++ .../com/dongchyeon/calendar/ui/Calendar.kt | 63 ++++++++++--------- .../calendar/ui/HorizontalCalendar.kt | 43 ++++++++----- .../calendar/util/FormatYearMonth.kt | 17 +++++ .../com/dongchyeon/calendar/util/TimeExt.kt | 55 ++++++++++++++++ gradle/libs.versions.toml | 2 + 10 files changed, 181 insertions(+), 55 deletions(-) create mode 100644 calendar/src/main/java/com/dongchyeon/calendar/model/DateYearMonth.kt create mode 100644 calendar/src/main/java/com/dongchyeon/calendar/util/FormatYearMonth.kt create mode 100644 calendar/src/main/java/com/dongchyeon/calendar/util/TimeExt.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96d7800..18bf212 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,7 +30,6 @@ android { } } compileOptions { - isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } @@ -52,7 +51,6 @@ android { dependencies { implementation(project(":calendar")) - coreLibraryDesugaring(libs.desugar.jdk.libs) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -62,6 +60,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.kotlinx.datetime) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/dongchyeon/calendar/MainActivity.kt b/app/src/main/java/com/dongchyeon/calendar/MainActivity.kt index 6ae9e4d..dad47ab 100644 --- a/app/src/main/java/com/dongchyeon/calendar/MainActivity.kt +++ b/app/src/main/java/com/dongchyeon/calendar/MainActivity.kt @@ -18,7 +18,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.dongchyeon.calendar.ui.Calendar import com.dongchyeon.calendar.ui.theme.DongChyeonCalendarTheme -import java.time.LocalDate +import com.dongchyeon.calendar.util.now +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.plus class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -34,12 +37,12 @@ class MainActivity : ComponentActivity() { val events = listOf( CalendarEvent( - date = LocalDate.now().plusDays(1), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(2), + date = LocalDate.now().plus(DatePeriod(days = 2)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), diff --git a/calendar/build.gradle.kts b/calendar/build.gradle.kts index 7d595e8..757e27d 100644 --- a/calendar/build.gradle.kts +++ b/calendar/build.gradle.kts @@ -27,7 +27,6 @@ android { } } compileOptions { - isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } @@ -43,7 +42,6 @@ android { } dependencies { - coreLibraryDesugaring(libs.desugar.jdk.libs) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -56,6 +54,7 @@ dependencies { implementation(libs.material) implementation(libs.coil.compose) implementation(libs.coil.network) + implementation(libs.kotlinx.datetime) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/calendar/src/main/java/com/dongchyeon/calendar/CalendarEvent.kt b/calendar/src/main/java/com/dongchyeon/calendar/CalendarEvent.kt index 301a9d4..0864064 100644 --- a/calendar/src/main/java/com/dongchyeon/calendar/CalendarEvent.kt +++ b/calendar/src/main/java/com/dongchyeon/calendar/CalendarEvent.kt @@ -2,7 +2,7 @@ package com.dongchyeon.calendar import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Shape -import java.time.LocalDate +import kotlinx.datetime.LocalDate @Immutable data class CalendarEvent( diff --git a/calendar/src/main/java/com/dongchyeon/calendar/model/DateYearMonth.kt b/calendar/src/main/java/com/dongchyeon/calendar/model/DateYearMonth.kt new file mode 100644 index 0000000..b65d03d --- /dev/null +++ b/calendar/src/main/java/com/dongchyeon/calendar/model/DateYearMonth.kt @@ -0,0 +1,39 @@ +package com.dongchyeon.calendar.model + +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.plus + +data class DateYearMonth( + val year: Int, + val month: Int +) { + override fun toString(): String = "$year-${month.toString().padStart(2, '0')}" + + internal fun atDay(day: Int): LocalDate { + return LocalDate(year, month, day) + } + + internal fun atEndOfMonth(): LocalDate { + val firstDay = LocalDate(year, month, 1) + val firstDayNextMonth = firstDay.plus(DatePeriod(months = 1)) + return firstDayNextMonth.minus(DatePeriod(days = 1)) + } + + internal fun lengthOfMonth(): Int { + val firstDayThisMonth = LocalDate(year, month, 1) + val firstDayNextMonth = firstDayThisMonth.plus(DatePeriod(months = 1)) + val lastDayThisMonth = firstDayNextMonth.minus(DatePeriod(days = 1)) + return lastDayThisMonth.dayOfMonth + } + + companion object { + internal fun from(localDate: LocalDate): DateYearMonth { + return DateYearMonth( + year = localDate.year, + month = localDate.monthNumber + ) + } + } +} \ No newline at end of file diff --git a/calendar/src/main/java/com/dongchyeon/calendar/ui/Calendar.kt b/calendar/src/main/java/com/dongchyeon/calendar/ui/Calendar.kt index 3cd7d63..741fd2b 100644 --- a/calendar/src/main/java/com/dongchyeon/calendar/ui/Calendar.kt +++ b/calendar/src/main/java/com/dongchyeon/calendar/ui/Calendar.kt @@ -1,5 +1,8 @@ package com.dongchyeon.calendar.ui +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -35,21 +38,27 @@ import com.dongchyeon.calendar.CalendarIndicatorConfig import com.dongchyeon.calendar.CalendarLanguage import com.dongchyeon.calendar.CalendarWeekHeaderConfig import com.dongchyeon.calendar.R +import com.dongchyeon.calendar.model.DateYearMonth import com.dongchyeon.calendar.theme.CalendarTheme import com.dongchyeon.calendar.ui.component.DayBackgroundImage +import com.dongchyeon.calendar.util.formatYearMonthInEnglish +import com.dongchyeon.calendar.util.formatYearMonthInKorean +import com.dongchyeon.calendar.util.getNumberWeeks +import com.dongchyeon.calendar.util.minusMonths import com.dongchyeon.calendar.util.noRippleClickable -import java.time.DayOfWeek -import java.time.LocalDate -import java.time.YearMonth -import java.time.format.DateTimeFormatter -import java.time.temporal.TemporalAdjusters -import java.time.temporal.WeekFields +import com.dongchyeon.calendar.util.now +import com.dongchyeon.calendar.util.plusMonths +import com.dongchyeon.calendar.util.previousOrSame +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.plus @Composable fun Calendar( modifier: Modifier = Modifier, events: List = emptyList(), - currentYearMonth: YearMonth = YearMonth.from(LocalDate.now()), + currentYearMonth: DateYearMonth = DateYearMonth.from(LocalDate.now()), selectedDate: LocalDate, headerConfig: CalendarHeaderConfig = CalendarHeaderConfig.default(), weekHeaderConfig: CalendarWeekHeaderConfig = CalendarWeekHeaderConfig.default(), @@ -109,19 +118,17 @@ fun MonthSelector( modifier: Modifier = Modifier, config: CalendarHeaderConfig, calendarLanguage: CalendarLanguage, - yearMonth: YearMonth, + yearMonth: DateYearMonth, isTablet: Boolean, onPrevClick: () -> Unit, onNextClick: () -> Unit ) { val formattedMonth = when (calendarLanguage) { CalendarLanguage.EN -> { - val locale = java.util.Locale.ENGLISH - val formatter = DateTimeFormatter.ofPattern("MMMM yyyy", locale) - yearMonth.atDay(1).format(formatter) // e.g., "February 2024" + formatYearMonthInEnglish(yearMonth) } CalendarLanguage.KO -> { - "${yearMonth.year}년 ${yearMonth.monthValue}월" // e.g., "2024년 12월" + formatYearMonthInKorean(yearMonth) } } @@ -203,21 +210,22 @@ fun WeekHeader( } } +@SuppressLint("NewApi") @Composable fun Week( modifier: Modifier = Modifier, itemWidth: Dp, events: List, weekNumber: Long, - currentMonth: YearMonth, + currentMonth: DateYearMonth, selectedDate: LocalDate, dayConfig: CalendarDayConfig, indicatorConfig: CalendarIndicatorConfig, isTablet: Boolean, onDayClicked: (LocalDate) -> Unit ) { - val beginningWeek = currentMonth.atDay(1).plusWeeks(weekNumber) - var currentDay = beginningWeek.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)) + val beginningWeek = currentMonth.atDay(1).plus(DatePeriod(days = (weekNumber * 7).toInt())) + var currentDay = beginningWeek.previousOrSame(DayOfWeek.SUNDAY) Row( modifier = modifier.fillMaxWidth(), @@ -225,7 +233,7 @@ fun Week( horizontalArrangement = Arrangement.SpaceBetween ) { for (i in 0..6) { - if (currentDay.month == currentMonth.month) { + if (currentDay.monthNumber == currentMonth.month) { val matchedEvent = events.find { it.date.year == currentDay.year && it.date.month == currentDay.month && @@ -245,7 +253,7 @@ fun Week( } else { Box(modifier = Modifier.size(itemWidth)) } - currentDay = currentDay.plusDays(1) + currentDay = currentDay.plus(DatePeriod(days = 1)) } } } @@ -311,13 +319,6 @@ private fun Day( } } -fun YearMonth.getNumberWeeks(weekFields: WeekFields = WeekFields.SUNDAY_START): Int { - val firstWeekNumber = this.atDay(1)[weekFields.weekOfMonth()] - val lastWeekNumber = this.atEndOfMonth()[weekFields.weekOfMonth()] - - return lastWeekNumber - firstWeekNumber + 1 -} - @Preview @Composable fun PreviewCalendar() { @@ -326,37 +327,37 @@ fun PreviewCalendar() { val events = listOf( CalendarEvent( - date = LocalDate.now().plusDays(1), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(2), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), CalendarEvent( - date = LocalDate.now().plusDays(3), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(4), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), CalendarEvent( - date = LocalDate.now().plusDays(5), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(6), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), CalendarEvent( - date = LocalDate.now().plusDays(7), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), diff --git a/calendar/src/main/java/com/dongchyeon/calendar/ui/HorizontalCalendar.kt b/calendar/src/main/java/com/dongchyeon/calendar/ui/HorizontalCalendar.kt index d4635e5..50f397b 100644 --- a/calendar/src/main/java/com/dongchyeon/calendar/ui/HorizontalCalendar.kt +++ b/calendar/src/main/java/com/dongchyeon/calendar/ui/HorizontalCalendar.kt @@ -1,5 +1,8 @@ package com.dongchyeon.calendar.ui +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -39,18 +42,27 @@ import com.dongchyeon.calendar.CalendarIndicatorConfig import com.dongchyeon.calendar.CalendarLanguage import com.dongchyeon.calendar.CalendarWeekHeaderConfig import com.dongchyeon.calendar.R +import com.dongchyeon.calendar.model.DateYearMonth import com.dongchyeon.calendar.theme.CalendarTheme import com.dongchyeon.calendar.ui.component.DayBackgroundImage +import com.dongchyeon.calendar.util.formatYearMonthInEnglish +import com.dongchyeon.calendar.util.formatYearMonthInKorean +import com.dongchyeon.calendar.util.minusMonths import com.dongchyeon.calendar.util.noRippleClickable -import java.time.LocalDate +import com.dongchyeon.calendar.util.now +import com.dongchyeon.calendar.util.plusMonths +import kotlinx.datetime.DatePeriod import java.time.YearMonth import java.time.format.DateTimeFormatter +import kotlinx.datetime.LocalDate +import kotlinx.datetime.format +import kotlinx.datetime.plus @Composable fun HorizontalCalendar( modifier: Modifier = Modifier, events: List = emptyList(), - currentYearMonth: YearMonth = YearMonth.from(LocalDate.now()), + currentYearMonth: DateYearMonth = DateYearMonth.from(LocalDate.now()), selectedDate: LocalDate = LocalDate.now(), headerConfig: CalendarHeaderConfig = CalendarHeaderConfig.default(), weekHeaderConfig: CalendarWeekHeaderConfig = CalendarWeekHeaderConfig.default(), @@ -75,7 +87,7 @@ fun HorizontalCalendar( val days = mutableListOf() for (i: Int in 1..selectedYearMonth.lengthOfMonth()) { days.add(currentDay) - currentDay = currentDay.plusDays(1) + currentDay = currentDay.plus(DatePeriod(days = 1)) } HorizontalCalendarHeader( @@ -96,7 +108,7 @@ fun HorizontalCalendar( items(days) { today -> val matchedEvent = events.find { it.date.year == today.year && - it.date.monthValue == today.monthValue && + it.date.monthNumber == today.monthNumber && it.date.dayOfMonth == today.dayOfMonth } @@ -127,7 +139,7 @@ fun HorizontalCalendar( @Composable private fun HorizontalCalendarHeader( modifier: Modifier = Modifier, - yearMonth: YearMonth, + yearMonth: DateYearMonth, config: CalendarHeaderConfig, calendarLanguage: CalendarLanguage, isTablet: Boolean, @@ -136,12 +148,10 @@ private fun HorizontalCalendarHeader( ) { val formattedMonth = when (calendarLanguage) { CalendarLanguage.EN -> { - val locale = java.util.Locale.ENGLISH - val formatter = DateTimeFormatter.ofPattern("MMMM yyyy", locale) - yearMonth.atDay(1).format(formatter) // e.g., "February 2024" + formatYearMonthInEnglish(yearMonth) } CalendarLanguage.KO -> { - "${yearMonth.year}년 ${yearMonth.monthValue}월" // e.g., "2024년 12월" + formatYearMonthInKorean(yearMonth) } } @@ -187,6 +197,7 @@ private fun HorizontalCalendarHeader( } } +@SuppressLint("NewApi") @Composable private fun Day( modifier: Modifier = Modifier, @@ -288,37 +299,37 @@ fun PreviewHorizontalCalendar() { val events = listOf( CalendarEvent( - date = LocalDate.now().plusDays(1), + date = LocalDate.now().plus(DatePeriod(days = 1)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(2), + date = LocalDate.now().plus(DatePeriod(days = 2)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), CalendarEvent( - date = LocalDate.now().plusDays(3), + date = LocalDate.now().plus(DatePeriod(days = 3)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(4), + date = LocalDate.now().plus(DatePeriod(days = 4)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), CalendarEvent( - date = LocalDate.now().plusDays(5), + date = LocalDate.now().plus(DatePeriod(days = 5)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), CalendarEvent( - date = LocalDate.now().plusDays(6), + date = LocalDate.now().plus(DatePeriod(days = 6)), imgUrl = "https://picsum.photos/200/300", imgShape = RoundedCornerShape(8.dp) ), CalendarEvent( - date = LocalDate.now().plusDays(7), + date = LocalDate.now().plus(DatePeriod(days = 7)), imgUrl = "https://picsum.photos/200/300", imgShape = CircleShape ), diff --git a/calendar/src/main/java/com/dongchyeon/calendar/util/FormatYearMonth.kt b/calendar/src/main/java/com/dongchyeon/calendar/util/FormatYearMonth.kt new file mode 100644 index 0000000..dbc838d --- /dev/null +++ b/calendar/src/main/java/com/dongchyeon/calendar/util/FormatYearMonth.kt @@ -0,0 +1,17 @@ +package com.dongchyeon.calendar.util + +import com.dongchyeon.calendar.model.DateYearMonth +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +internal fun formatYearMonthInEnglish(yearMonth: DateYearMonth): String { + val calendar = Calendar.getInstance(Locale.ENGLISH) + calendar.set(yearMonth.year, yearMonth.month - 1, 1) + val sdf = SimpleDateFormat("MMMM yyyy", Locale.ENGLISH) + return sdf.format(calendar.time) +} + +internal fun formatYearMonthInKorean(yearMonth: DateYearMonth): String { + return "${yearMonth.year}년 ${yearMonth.month}월" +} \ No newline at end of file diff --git a/calendar/src/main/java/com/dongchyeon/calendar/util/TimeExt.kt b/calendar/src/main/java/com/dongchyeon/calendar/util/TimeExt.kt new file mode 100644 index 0000000..c770a8e --- /dev/null +++ b/calendar/src/main/java/com/dongchyeon/calendar/util/TimeExt.kt @@ -0,0 +1,55 @@ +package com.dongchyeon.calendar.util + +import android.annotation.SuppressLint +import com.dongchyeon.calendar.model.DateYearMonth +import kotlinx.datetime.Clock +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.toLocalDateTime + +fun LocalDate.Companion.now(): LocalDate { + return Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date +} + +internal fun LocalDate.previousOrSame(target: DayOfWeek): LocalDate { + val currentOrdinal = this.dayOfWeek.ordinal + val targetOrdinal = target.ordinal + + val diff = (7 + currentOrdinal - targetOrdinal) % 7 + + return this.minus(DatePeriod(days = diff)) +} + +internal fun DateYearMonth.plusMonths(months: Int): DateYearMonth { + val totalMonths = this.year * 12 + (this.month - 1) + val newTotal = totalMonths + months + val newYear = newTotal / 12 + val newMonth = (newTotal % 12) + 1 + return DateYearMonth(newYear, newMonth) +} + +internal fun DateYearMonth.minusMonths(months: Int): DateYearMonth { + return this.plusMonths(-months) +} +@SuppressLint("NewApi") +internal fun LocalDate.weekOfMonth(startDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY): Int { + val offset = (7 + this.dayOfWeek.ordinal - startDayOfWeek.ordinal) % 7 + return ((this.dayOfMonth + offset - 1) / 7) + 1 +} +@SuppressLint("NewApi") +internal fun DateYearMonth.getNumberWeeks( + startDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY +): Int { + val firstDay = this.atDay(1) + val lastDay = this.atEndOfMonth() + + val firstWeekNumber = firstDay.weekOfMonth(startDayOfWeek) + val lastWeekNumber = lastDay.weekOfMonth(startDayOfWeek) + + return lastWeekNumber - firstWeekNumber + 1 +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cc77b5..8b50b6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ coil = "3.0.3" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" +kotlinxDatetime = "0.6.1" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" composeBom = "2024.04.01" @@ -30,6 +31,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }