2.
Getting Started
Written by Kevin D Moore
In the last chapter, you created your first KMP project. To get started, you’ll need to understand the build system KMP uses. For Android and desktop, that’s Gradle. For iOS, Android Studio will use the “Regular Framework” for building a library.
Getting to Know Gradle
If you come from the Android world, you already have some experience with Gradle — but it’s probably with the one written in the Groovy language. These files are named build.gradle. If you don’t know Android, Gradle is a build system for developing software like in Android. It can run tasks to compile, package, test, deploy, and even publish artifacts to a distribution center. Many programming systems use it. Gradle simplifies the often complex process of building and managing projects by making use of the different plugins for almost any purpose.
For KMP, you’ll use the Kotlin scripting version of Gradle. These files are named build.gradle.kts. The kts extension stands for “Kotlin script.” This version uses Kotlin for the Gradle DSL (Domain Specific Language) — which makes it much easier to use if you already know Kotlin. This project has a number of these build scripts in different directories. Each serves a specific purpose. Open the starter project or the project from the last chapter in Android Studio, switch to the Project view on the left and follow along to learn more about these build files.
If you don’t know what a particular command does, you can click the name while pressing the command key and Android Studio will open the class.
Version Catalog
One of the nice features of the newer Gradle versions is the concept of Version Catalogs. This is a file where you can define version, library, plugin and bundle variables. This allows you to have all of your versions defined in one place so that each build file uses the same one. This file was created for you when you created your project. Open gradle/libs.versions.toml. This file has four sections:
- versions
- libraries
- plugins
- bundles
The versions section is where you define the version numbers for libraries or plugins. The libraries section is where you define the library using the version defined above. Plugins are of course for plugins like Android, Compose, etc. Bundles are a very convenient section for defining bundles of libraries.
Compose is a great example as it needs a lot of libraries. Define them all in one bundle and you can add just the bundle to your dependency list. You will dig in a bit more about all of these 4 sections.
Versions
Let’s start with the versions section. The first step is to add all of the versions for the libraries needed. Replace the current list of versions with the following:
[versions]
agp = "8.1.2"
kotlin = "1.9.10"
core-ktx = "1.12.0"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
appcompat = "1.6.1"
material = "1.6.0-alpha07"
compose-bom = "2023.10.00"
compose-ui = "1.5.3"
viewModelVersion = "2.6.2"
androidx-activity = "1.8.0"
activity-compose = "1.8.0"
navigation="2.7.4"
material3 = "1.2.0-alpha09"
kotlinxDateTime = "0.4.1"
napier = "2.6.1"
composePlugin = "1.5.1"
multiplatformPlugin = "1.9.10"
- agp: agp is short for Android Gradle Plugin
- compose-bom: This is the version needed for the BOM (bill of material) for compose.
- kotlinxDateTime: This is a library needed for dealing with dates & timezones.
- napier: Multiplatform logging library
The BOM is a way to insure that you are using all of the correct versions for each library. The different compose libraries are changing every month and keeping track of each library version is very difficult. By keeping track of just the version for the BOM, you no longer have to know what version numbers to use for each library. You only need to specify the BOM version.
Libraries
Now that you have the versions defined, list the libraries that will be used in the app. Replace the libraries section with:
[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
# AndroidX
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
ui = { group = "androidx.compose.ui", name = "ui" }
ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
compose-viewmodel = {module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref="viewModelVersion" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
navigation = { module = "androidx.navigation:navigation-compose" , version.ref="navigation"}
# Material
# Material design icons
material-icons = { module = "androidx.compose.material:material-icons-core", version.ref = "compose-ui" }
material-iconsExtended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose-ui" }
material3 = { group = "androidx.compose.material3", name = "material3" , version.ref="material3"}
material = { group = "androidx.compose.material", name = "material", version.ref="material" }
# DateTime
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref ="kotlinxDateTime"}
#Napier
napier = { module = "io.github.aakira:napier", version.ref ="napier" }
The top set of libraries are the same but the rest are new. Notice the makeup of the library:
<library-name> = { module = "<full module name>", version.ref="<name of version>"}
or
<library-name> = { group = "<group name>", name="<library name>", version.ref="<name of version>"}
These are two different ways to define a library. The first one uses the full name (including the “:”). The second version separates the group and name into separate parameters. The version.ref
uses the string version name defined above.
Comments separate the different types of libraries. These are:
- AndroidX: Android specific libraries.
- Compose: There are a lot of these libraries. These libraries will be used to define the UI with the Composable functions available in the libraries.
- Material: Both Material & Material3 (newer) libraries.
- DateTime: Library for handling dates & times.
- Napier: For multiplatform logging.
Plugins
Now replace the plugins section with:
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
composePlugin = { id = "org.jetbrains.compose", version.ref = "composePlugin" }
multiplatformPlugin = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "multiplatformPlugin" }
This just adds two new plugins to our project: compose and multiplatform plugin. In general, a typical plugin declaration includes two components:
- plugin name: In the updated code, composePlugin and multiplatformPlugin are the names of the plugin that you will be using in the project to identify your plugin.
- id: The id (short for identifier) is used to specify which plugin you want to include.
- version.ref: The version reference allows you to specify the versions of the plugin. We have defined the versions required at the top in the versions section.
Bundles
The bundles section is where things get interesting. By creating a bundle you define one variable that can represent multiple libraries. Add the following at the bottom of the file:
[bundles]
androidx-activity = ["androidx-activity", "androidx-activity-ktx", "compose-viewmodel"]
material = ["material-icons", "material-iconsExtended", "material", "material3"]
compose-ui = ["activity-compose", "androidx-compose-foundation", "ui", "ui-ui", "ui-tooling", "ui-tooling-preview"]
Notice how this is a list of libraries identified with a user-defined identifier. To use this in a gradle file you would just add this to the dependency section:
implementation(libs.bundles.compose.ui)
Notice that “-” is replaced by “.”. Now you’re done making updates in the build files. Press the Sync Now text at the top of the file to sync changes into the project.
Build Files
Open build.gradle.kts in the root folder. Replace the current plugins with:
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.kotlinAndroid) apply false
alias(libs.plugins.multiplatformPlugin) apply false
Notice that there aren’t any versions here. The apply false
means that it does not use the plugin in this file. You want to use false in the top-level Gradle file. Press the Sync Now text at the top of the file.
There will be the following build files:
- Root level build.gradle.kts. This just has a few plugins & the clean task
- Root level settings.gradle.kts. This is where all of the settings for the Gradle project are stored. You set the project name and include any sub-projects here. Also, a good place to define where Gradle should fetch the plugins from.
- androidApp/build.gradle.kts: This defines the build for Android.
- shared/build.gradle.kts: This defines the build for shared files. Because of this, it needs to define all the targets used. (Android, iOS, desktop, etc).
Notice that iOS does not have a gradle file.
Android Build File
Now, open androidApp/build.gradle.kts. Except for the different Kotlin syntax, this should look familiar to Android developers. Replace the current plugins with:
alias(libs.plugins.androidApplication)
alias(libs.plugins.kotlinAndroid)
Android needs the application and the Android Kotlin plugins. Normally, you’ll see either id
or kotlin
keywords. Using the newer alias
keyword, you can reference your version catalog entry.
Next, update the Android-specific settings. Change the following:
-
kotlinCompilerExtensionVersion
to 1.5.3. - Java version to 17
- Kotlin
jvmTarget
to Java 17 - Kotlin compiler flags: These remove the need to add the experimental annotations all around the code (which can get very annoying)
// 1
android {
// 2
namespace = "com.kodeco.findtime.android"
// 3
compileSdk = 34
defaultConfig {
// 4
applicationId = "com.kodeco.findtime.android"
// 5
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
// 6
buildFeatures {
compose = true
}
// 7
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
// 8
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
// 9
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
// 10
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// 11
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
)
}
}
Here’s what the above code does:
-
Start the Android section.
-
Define the app namespace. This will be your app’s unique id. (Note that this is a new requirement. The applicationID was used before.)
-
compileSdk specifies the Android SDK version to compile against.
-
applicationId is the ID for the Android App. This has to be unique for every app on the Google Play store.
-
minSdk refers to the lowest Android version your app will run on, while targetSdk refers to the latest Android version you support. versionCode is the number you’ll use internally to differentiate between builds while versionName is the version that will be displayed on the Play Store.
-
Specify compose as a build feature. This enables the use of Jetpack Compose in the project.
-
Set the Compose compiler version.
-
Set up any packaging options. This enables you to customize the packaging of resources by specifying the exclusion rules for specific files.
-
Specify debug or release settings here. For the release version, this sets isMinifyEnabled to false, but you’ll probably want to set it to true when you’re ready to release your app. See: https://developer.android.com/reference/tools/gradle-api/8.2/com/android/build/api/dsl/BuildType#isMinifyEnabled().
-
Set the Java version compatibility to 17.
-
Target compile version of 17. Add the flags so that you don’t need to add the experimental annotations.
The next section shows which dependencies you need:
dependencies {
// 1
implementation(project(":shared"))
// 2
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose.ui)
implementation(libs.bundles.androidx.activity)
implementation(libs.bundles.material)
implementation(libs.napier)
}
- Android depends on the
shared
module (where all shared business logic will reside). - Android-specific libraries (Compose libraries).
Notice this line:
implementation(platform(libs.compose.bom))
This adds the Compose BoM file which syncs the correct versions of all of the Compose libraries.
The bundle below adds all the compose libraries.
implementation(libs.bundles.compose.ui)
Now, press the Sync Now text at the top of the file.
Shared Build File
Open shared/build.gradle.kts. This is the build script for the shared module. If you open the src directory, you’ll see the androidMain, commonMain and iosMain directories. These contain the shared files for Android and iOS, as well as files that all modules share.
The first section is the plugins:
plugins {
kotlin("multiplatform")
id("com.android.library")
}
The first plugin is for KMP and defines this module as a multiplatform module. The second plugin is for Android. You’ll use this to create an Android library for use in an Android app.
The Kotlin section is next. This section uses the multiplatform plugin above to configure this module for KMP. Change it to the following:
kotlin {
// 1
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
}
}
// 2
ios()
iosSimulatorArm64()
// 3
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
// 4
jvm("desktop")
// 5
sourceSets {
val commonMain by getting {
kotlin.srcDirs("src/commonMain/kotlin")
dependencies {
implementation(libs.datetime)
implementation(libs.napier)
}
}
val androidMain by getting {
kotlin.srcDirs("src/androidMain/kotlin")
}
val iosMain by getting {
kotlin.srcDirs("src/iosMain/kotlin")
}
val iosTest by getting
val iosSimulatorArm64Main by getting
val iosSimulatorArm64Test by getting
}
}
Remember to remove the @OptIn
and targetHierarchy.default()
blocks of code.
Here’s an explanation of the code:
- Use the androidTarget method to define an Android target.
- Define iOS targets.
- iosX64 defines a target for the iOS simulator on x86_64 platforms, while iosArm64 defines a target for iOS on ARM64 platforms. This will create a shared library with the name “shared”
- Defines a desktop version.
- Define the sources for each type:
- commonMain: This is the shared code. Notice that it also contains the dependencies common to all platforms.
- androidMain: Just for Android.
- iosMain: Just of iOS.
- Finally, you have platform-specific test source sets like iosTest.
Next, change the Android section to the following:
android {
namespace = "com.kodeco.findtime"
compileSdk = 34
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
This is similar to Android’s build file, except it just has the minimum information needed.
Do a Gradle sync to make sure everything still works.
Build and run the app on Android. You’ll see a screen similar to the one shown below:
You haven’t changed the UI yet, but this ensures that the Gradle files are still working.
Find Time
Have you ever needed to schedule a meeting with colleagues who work in different time zones? It can be a real pain. Are they awake at the hour you want to schedule? What are good times to schedule a meeting? You’re going to write the Find Time app to help find those hours that work best. To do that, you need to write some time zone logic to figure out the best hours to meet. If you were to write separate apps for iOS and Android, you would have to write that business logic twice.
Business Logic
One of the main benefits of KMP is you can share business logic among all your platforms. You’ll write your business logic in the shared module. This module is a multiplatform module you can use for iOS, Android, desktop and the web.
Open the shared/src folder and then androidMain, commonMain and iosMain folders. These are directories for:
- Android
- Shared
- iOS
You’ll find the Platform
and Greeting
classes in these folders. Delete these classes as you won’t use them. Note that both the Android and iOS platforms won’t run until you delete the code that called them. Android Studio will complain about their usages. Click the View Usages button.
Double-click GreetingView(Greeting().greet())
to open MainActivity. Delete the GreetingView
function, the imports and the reference to it below:
Run the delete again, and you’ll be able to delete the files without any problems.
DateTime Calculations
You’ll use JetBrains’ kotlinx-datetime
library to help with datetime calculations. Open shared/build.gradle.kts and find val commonMain
. You can see that you added the datetime and napier libraries:
val commonMain by getting {
dependencies {
// 1
implementation(libs.datetime)
// 2
implementation(libs.napier)
}
}
This will import two libraries:
- JetBrains datetime library.
- Napier logging library.
DateTime Library
Kotlin’s kotlinx-datetime
is an easy-to-use multiplatform library that helps with date- and time-based calculations. It uses several data types:
-
Instant
represents a moment in time. -
Clock
imitates a real-world clock and provides the current instant. -
LocalDateTime
represents a date with time but no associated time zone. -
LocalDate
represents only a date. -
TimeZone
andZoneOffset
help you convert betweenInstant
andLocalDateTime
. -
Month
is an enum representing all months in the year. -
DayOfWeek
is an enum representing all days of the week. It uses values likeMONDAY
,TUESDAY
, etc. instead of integers. -
DateTimePeriod
represents the difference between 2 instants. -
DatePeriod
is a subclass ofDateTimePeriod
and represents the difference between twoLocalDate
instances. -
DateTimeUnit
provides a set of units such asNANOSECOND
,WEEK
,CENTURY
, etc. that you can use to perform arithmetic operations onInstant
andLocalDate
.
Time Zone Helper
Go to the com/kodeco/findtime package in shared/commonMain/ and create a new Kotlin interface named TimeZoneHelper. Add the following:
interface TimeZoneHelper {
fun getTimeZoneStrings(): List<String>
fun currentTime(): String
fun currentTimeZone(): String
fun hoursFromTimeZone(otherTimeZoneId: String): Double
fun getTime(timezoneId: String): String
fun getDate(timezoneId: String): String
fun search(startHour: Int, endHour: Int, timezoneStrings: List<String>): List<Int>
}
This defines an interface that has seven functions.
- Return a list of time zone strings. (This is a list of all time zones from the JetBrains kotlinx-datetime library)
- Return the current formatted time.
- Return the current time zone id.
- Return the number of hours from the given time zone.
- Return the formatted time for the given time zone.
- Return the formatted date for the given time zone.
- Search for a list of hours that start at
startHour
, end atendHour
and are in all the given time zone strings.
Creating an interface makes it easy to test. This chapter doesn’t cover tests, but using an interface makes it easy to create mocked time zone helpers. Now, create an instance of that interface. Right-click the findtime folder and create a new Kotlin class named TimeZoneHelperImpl. This class will implement the interface. Update the class to extend TimeZoneHelper
as follows:
class TimeZoneHelperImpl: TimeZoneHelper {
}
You’ll see a red line underneath the class because you haven’t yet implemented the methods that are defined in TimeZoneHelper interface class. Press Option-Return while keeping your cursor on TimeZoneHelperImpl
and choose Implement Members.
Select all and click OK. You’ll see lots of TODOs.
Start with getTimeZoneStrings
. This sounds like it could be really hard, but the kotlinx-datetime library makes it easy. Replace the TODO with:
return TimeZone.availableZoneIds.sorted()
This line returns the available time zone IDs and sorts them. TimeZone
will be red. Place your cursor on TimeZone
and press Option-Return to import the library. You can use this technique to import the required classes in the next sections as well.
Next, you need a method to format datetime’s LocalDateTime
class. Add the following method at the bottom of the class:
fun formatDateTime(dateTime: LocalDateTime): String {
// 1
val stringBuilder = StringBuilder()
// 2
val minute = dateTime.minute
var hour = dateTime.hour % 12
if (hour == 0) hour = 12
// 3
val amPm = if (dateTime.hour < 12) " am" else " pm"
// 4
stringBuilder.append(hour.toString())
stringBuilder.append(":")
// 5
if (minute < 10) {
stringBuilder.append('0')
}
stringBuilder.append(minute.toString())
stringBuilder.append(amPm)
// 6
return stringBuilder.toString()
}
In the code above, you:
- Use a StringBuilder to build the string piece by piece.
- Get the hour and minutes from the dateTime argument.
- Since you want a string with am/pm, check if the hour is greater than noon (12).
- Build the hour and colon.
- Check to make sure numbers 0-9 are padded.
- Return the final string.
Get rid of the “Unresolved reference: LocalDateTime” by placing your cursor on LocalDateTime
and pressing Option-Return to import the library.
Now update the currentTime
method to the below code and import the required libraries:
override fun currentTime(): String {
// 1
val currentMoment: Instant = Clock.System.now()
// 2
val dateTime: LocalDateTime = currentMoment.toLocalDateTime(TimeZone.currentSystemDefault())
// 3
return formatDateTime(dateTime)
}
In the previous code, you:
- Get the current time as an Instant.
- Convert the current moment into a LocalDateTime that’s based on the current user’s time zone.
- Format the given date using the
formatDateTime
method you defined earlier.
Now implement the getTime
method:
override fun getTime(timezoneId: String): String {
// 1
val timezone = TimeZone.of(timezoneId)
// 2
val currentMoment: Instant = Clock.System.now()
// 3
val dateTime: LocalDateTime = currentMoment.toLocalDateTime(timezone)
// 4
return formatDateTime(dateTime)
}
In the code above, you:
- Get the time zone with the given ID.
- Get the current time as an Instant.
- Convert the current moment into a LocalDateTime that’s based on the passed-in time zone.
- Format the given date.
getDate
is similar to getTime
. Replace getDate
with the following code:
override fun getDate(timezoneId: String): String {
val timezone = TimeZone.of(timezoneId)
val currentMoment: Instant = Clock.System.now()
val dateTime: LocalDateTime = currentMoment.toLocalDateTime(timezone)
// 1
return "${dateTime.dayOfWeek.name.lowercase().replaceFirstChar { it.uppercase() }}, " +
"${dateTime.month.name.lowercase().replaceFirstChar { it.uppercase() }} ${dateTime.date.dayOfMonth}"
}
This takes the different parts of the DateTime to create a string like: “Monday, October 4.”
The currentTimeZone
method is pretty easy. Return the current time zone as a string:
override fun currentTimeZone(): String {
val currentTimeZone = TimeZone.currentSystemDefault()
return currentTimeZone.toString()
}
The hoursFromTimeZone
method is a bit tricky. You want to return the number of hours from the given time zone:
override fun hoursFromTimeZone(otherTimeZoneId: String): Double {
// 1
val currentTimeZone = TimeZone.currentSystemDefault()
// 2
val currentUTCInstant: Instant = Clock.System.now()
// Date time in other timezone
// 3
val otherTimeZone = TimeZone.of(otherTimeZoneId)
// 4
val currentDateTime: LocalDateTime = currentUTCInstant.toLocalDateTime(currentTimeZone)
// 5
val currentOtherDateTime: LocalDateTime = currentUTCInstant.toLocalDateTime(otherTimeZone)
// 6
return abs((currentDateTime.hour - currentOtherDateTime.hour) * 1.0)
}
In the code above, you:
- Get the current time zone.
- Get the current time/instant.
- Get the other time zone.
- Convert the current time into a LocalDateTime class.
- Convert the current time in another time zone into a LocalDateTime class.
- Return the absolute difference between the hours (shouldn’t be negative), making sure the result is a double.
Searching
Searching is a bit harder. Given a starting hour (like 8 a.m.), an ending hour (say 5 p.m.) and the list of time zones that everyone is in, you want to return a list of integers that represent the hours (0-23) that fit in everyone’s time zones. So, if you pass in 8 a.m. - 5 p.m. for Los Angeles and New York, you will get a list of hours:
[8,9,10,11,12,13,14]
All these hours for Los Angeles also work for New York. Los Angeles can go up to 2 p.m. (14), while New York will start at 11 a.m. and go until 5 p.m. To see if an hour is valid, add the isValid
method after the search
method:
private fun isValid(
timeRange: IntRange,
hour: Int,
currentTimeZone: TimeZone,
otherTimeZone: TimeZone
): Boolean {
if (hour !in timeRange) {
return false
}
// TODO: Add Current Time
}
This method takes a time range (like 8..17), the given hour to check, the current time zone for the user and the other time zone that you’re checking against. The first check verifies if the hour is in the time range. If not, it isn’t valid.
Now, replace // TODO: Add Current Time
with:
// 1
val currentUTCInstant: Instant = Clock.System.now()
// 2
val currentOtherDateTime: LocalDateTime = currentUTCInstant.toLocalDateTime(otherTimeZone)
// 3
val otherDateTimeWithHour = LocalDateTime(
currentOtherDateTime.year,
currentOtherDateTime.monthNumber,
currentOtherDateTime.dayOfMonth,
hour,
0,
0,
0
)
// TODO: Add Conversions
- Use datetime’s
Clock.System.now
method to get the current instant in the UTC time zone. - Convert the instant into another time zone with
toLocalDateTime
, passing in the other time zone. - Get a LocalDateTime with the given hour. (Minutes, seconds and nanoseconds aren’t needed)
Now, replace // TODO: Add Conversions
with:
// 1
val localInstant = otherDateTimeWithHour.toInstant(currentTimeZone)
// 2
val convertedTime = localInstant.toLocalDateTime(otherTimeZone)
Napier.d("Hour $hour in Time Range ${otherTimeZone.id} is ${convertedTime.hour}")
// 3
return convertedTime.hour in timeRange
Napier is the logging library and needs to be imported. Place your cursor on Napier
and press Option-Return on it to import the library.
In the previous code, you:
- Convert that hour into the current time zone.
- Convert your time zone hour to the other time zone.
- Check to see if it’s in your time range.
Now that you have the isValid
method, the search
method won’t be as hard. You just need to go through all the given time zones and hours and check if they are valid. Update the search
method with:
// 1
val goodHours = mutableListOf<Int>()
// 2
val timeRange = IntRange(max(0, startHour), min(23, endHour))
// 3
val currentTimeZone = TimeZone.currentSystemDefault()
// 4
for (hour in timeRange) {
var isGoodHour = false
// 5
for (zone in timezoneStrings) {
val timezone = TimeZone.of(zone)
// 6
if (timezone == currentTimeZone) {
continue
}
// 7
if (!isValid(
timeRange = timeRange,
hour = hour,
currentTimeZone = currentTimeZone,
otherTimeZone = timezone
)
) {
Napier.d("Hour $hour is not valid for time range")
isGoodHour = false
break
} else {
Napier.d("Hour $hour is Valid for time range")
isGoodHour = true
}
}
// 8
if (isGoodHour) {
goodHours.add(hour)
}
}
// 9
return goodHours
In this code, you:
- Create a list to return all the valid hours.
- Create a time range from start to end hours.
- Get the current time zone.
- Go through each hour in the time range.
- Go through each time zone in the time zone list.
- If it’s the same time zone as the current one, then you know it’s good.
- Check if the hour is valid.
- If, after going through every hour and it’s a good hour, add it to our list.
- Return the list of hours.
Import the min
and max
methods. You’ve now written the business logic for the Time Finder app! Android, iOS, desktop and web platforms can all share it.
Build the app to make sure it still compiles. You can try to run both Android and iOS again.
To run iOS, you’ll have to edit the iOS build configuration and pick a simulator:
You’ll see the iOS build fail with the error shown below if run from Xcode:
Remember that you removed the greet
method. How would you fix this?
Challenge
The iOS app no longer works. Figure out how to find the problem, fix the error and get the iOS App working again. To see the answer, review the file in the challenge folder.
Key Points
- Gradle is the build system for most of KMP projects.
- You write the Gradle build files in Kotlin.
- You use the libs.versions.toml file to define variables, libraries, plugins and bundles.
- You write the business logic in the shared module.
- The kotlinx-datetime library is a multiplatform library for handling dates and times.
Where to Go From Here?
In this chapter, you’ve learned how to set up your project with the libs.versions.toml file to make changing versions easier. You also learned how to work with dates and times in the shared module, making it available for all platforms.
- Gradle: https://gradle.org/
- Kotlinx Datetime: https://github.com/Kotlin/kotlinx-datetime
- Version Catalogs: https://docs.gradle.org/current/userguide/platforms.html and https://developer.android.com/build/migrate-to-catalogs
In the next chapter, you’ll start building the UI for the Find Time project.