diff --git a/README.md b/README.md index ef5ec7b..da3e232 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,24 @@ This is a Kotlin Multiplatform project targeting Android, iOS, Desktop. you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. -Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… \ No newline at end of file +Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… + +## Recommendation + +Why you should use standard formatters, because every locale uses different style for date/time, like spacing, 12/24 format, etc... + +This result is from same code with different locales (US, DE, FR, CS): + +US: uses 12h format, `MM dd, yyyy`, comma and space as date separator. +![US](./imgs/image_us.png) + +DE: uses 24h format, `dd.MM.yyyy`, dot as date separator. +![DE](./imgs/image_de.png) + +FR: uses 24h format, `dd MMM yyyy`, space separator for date. +![FR](./imgs/image_fr.png) + +CS: uses 24h format, `dd. MM. yyyy`, dot and space as date separator. +![CS](./imgs/image_cs.png) + +Please respect regional settings and use standard formatters if you return output for users :). \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5215e64..894c455 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -81,12 +81,16 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + + isCoreLibraryDesugaringEnabled = true } buildFeatures { compose = true } dependencies { debugImplementation(compose.uiTooling) + + coreLibraryDesugaring(libs.desugar.jdk.libs) } } diff --git a/composeApp/src/androidMain/kotlin/DateTimeAdapter.android.kt b/composeApp/src/androidMain/kotlin/DateTimeAdapter.android.kt new file mode 100644 index 0000000..aa6d20b --- /dev/null +++ b/composeApp/src/androidMain/kotlin/DateTimeAdapter.android.kt @@ -0,0 +1,30 @@ +import kotlinx.datetime.* +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +/** + * You should use standard date and time formatting patterns for the platform you are targeting. + * The reason is that every Locale uses different patterns for formatting dates and times. + * Standard FormatStyle.[SHORT|MEDIUM|LONG] solve this problem for you. + * For example 12/24-hour format, dividers, order of day/month/year in output, etc... + * + * Try to change Locale of your system from US/FR/DE and see how the output changes. + * + * We can create our domain FormatStyle of course and propagate expected format from common + * module and not use only MEDIUM format here... + */ +actual fun getDateTimeAdapter(): DateTimeAdapter = object : DateTimeAdapter { + override fun formatLocalDateTime(dateTime: LocalDateTime): String { + return dateTime.toJavaLocalDateTime().format( + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM) + ) + } + + override fun formatLocalTime(time: LocalTime): String = time.toJavaLocalTime().format( + DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM) + ) + + override fun formatLocalDate(date: LocalDate): String = date.toJavaLocalDate().format( + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index ef659cd..70af1a1 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -22,16 +22,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.UtcOffset -import kotlinx.datetime.format -import kotlinx.datetime.format.char -import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.ui.tooling.preview.Preview -import kotlin.time.Duration.Companion.hours data class City( val name: String, @@ -58,7 +52,7 @@ fun App() { ) } LaunchedEffect(true) { - while(true) { + while (true) { cityTimes = cities.map { val now = Clock.System.now() it to now.toLocalDateTime(it.timeZone) @@ -87,31 +81,12 @@ fun App() { horizontalAlignment = Alignment.End ) { Text( - text = dateTime - .format( - LocalDateTime.Format { - hour() - char(':') - minute() - char(':') - second() - } - ), + text = getDateTimeAdapter().formatLocalTime(dateTime.time), fontSize = 30.sp, fontWeight = FontWeight.Light ) Text( - text = dateTime - .format( - LocalDateTime.Format { - dayOfMonth() - char('/') - monthNumber() - char('/') - year() - } - ), - fontSize = 16.sp, + text = getDateTimeAdapter().formatLocalDate(dateTime.date), fontWeight = FontWeight.Light, textAlign = TextAlign.End ) diff --git a/composeApp/src/commonMain/kotlin/DateTimeAdapter.kt b/composeApp/src/commonMain/kotlin/DateTimeAdapter.kt new file mode 100644 index 0000000..d69108a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/DateTimeAdapter.kt @@ -0,0 +1,11 @@ +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime + +interface DateTimeAdapter { + fun formatLocalDateTime(dateTime: LocalDateTime): String + fun formatLocalTime(time: LocalTime): String + fun formatLocalDate(date: LocalDate): String +} + +expect fun getDateTimeAdapter(): DateTimeAdapter \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/DateTimeAdapter.desktop.kt b/composeApp/src/desktopMain/kotlin/DateTimeAdapter.desktop.kt new file mode 100644 index 0000000..92213d3 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/DateTimeAdapter.desktop.kt @@ -0,0 +1,17 @@ +import kotlinx.datetime.* +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +actual fun getDateTimeAdapter(): DateTimeAdapter = object : DateTimeAdapter { + override fun formatLocalDateTime(dateTime: LocalDateTime): String = dateTime.toJavaLocalDateTime().format( + DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM) + ) + + override fun formatLocalTime(time: LocalTime): String = time.toJavaLocalTime().format( + DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM) + ) + + override fun formatLocalDate(date: LocalDate): String = date.toJavaLocalDate().format( + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + ) +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/DateTimeAdapter.ios.kt b/composeApp/src/iosMain/kotlin/DateTimeAdapter.ios.kt new file mode 100644 index 0000000..ea2cc01 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/DateTimeAdapter.ios.kt @@ -0,0 +1,20 @@ +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime + +/** + * Use iOS DateFormatter: https://stackoverflow.com/a/41769435/4024146" + */ +actual fun getDateTimeAdapter(): DateTimeAdapter = object : DateTimeAdapter { + override fun formatLocalDateTime(dateTime: LocalDateTime): String { + TODO("Not yet implemented") + } + + override fun formatLocalTime(time: LocalTime): String { + TODO("Not yet implemented") + } + + override fun formatLocalDate(date: LocalDate): String { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdca37b..5030116 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" compose-plugin = "1.6.11" +desugar_jdk_libs = "2.0.4" junit = "4.13.2" kotlin = "2.0.0" datetime = "0.6.0" @@ -28,6 +29,9 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime"} +# Desugar for support Java Time API on older Android versions +desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } diff --git a/imgs/image_cs.png b/imgs/image_cs.png new file mode 100644 index 0000000..c8c8084 Binary files /dev/null and b/imgs/image_cs.png differ diff --git a/imgs/image_de.png b/imgs/image_de.png new file mode 100644 index 0000000..bca5281 Binary files /dev/null and b/imgs/image_de.png differ diff --git a/imgs/image_fr.png b/imgs/image_fr.png new file mode 100644 index 0000000..0707947 Binary files /dev/null and b/imgs/image_fr.png differ diff --git a/imgs/image_us.png b/imgs/image_us.png new file mode 100644 index 0000000..f9ed38f Binary files /dev/null and b/imgs/image_us.png differ