Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

First Edition · Android 12, iOS 15, Desktop · Kotlin 1.6.10 · Android Studio Bumblebee

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, you’ll use CocoaPods in this chapter. (You’ll learn about another method in a later chapter).

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. It can run tasks to compile, package, test, deploy and even publish artifacts to a distribution center. Many programming systems use it. Although it can be quite complex, it can use 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 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.

Build files

Open build.gradle.kts in the root folder.

// 1
buildscript {
    // 2
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
   // 3
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
        classpath("com.android.tools.build:gradle:7.0.1")
    }
}
  1. The buildscript section describes all the information about where to get plugins and their versions.
  2. repositories describes the sources that host these plugins. mavenCentral is where most reside, but both Google and Gradle have their own.
  3. dependencies describes the plugins and versions you’ll use. Here, you’re using the Kotlin Gradle plugin and the Android Gradle plugin.

Next, there is:

// 1
allprojects {
    // 2
    repositories {
        google()
        mavenCentral()
    }
}
  1. allprojects means the following applies to all modules in this project.
  2. repositories defines all the repositories needed for all modules.

The last section:

tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
}

This defines a Gradle task called clean. The task deletes the root build directory. You use it when you need a fresh build.

Now, open androidApp/build.gradle.kts. Except for the different Kotlin syntax, this should look familiar to Android developers. It starts by defining the plugins needed for Android:

plugins {
    id("com.android.application")
    kotlin("android")
}

Android needs the application and the Android Kotlin plugins. The next section shows which dependencies you need:

dependencies {
    // 1
    implementation(project(":shared"))
    // 2
    implementation("com.google.android.material:material:1.4.0")
    implementation("androidx.appcompat:appcompat:1.3.1")
    implementation("androidx.constraintlayout:constraintlayout:2.1.1")
}
  1. Android depends on the shared module (where all shared business logic will reside).
  2. Android-specific libraries (Material Components & ConstraintLayout).

Next, the Android-specific settings:

android {
    // 1
    compileSdk = 31
    defaultConfig {
        // 2
        applicationId = "com.raywenderlich.findtime.android"
        // 3
        minSdk = 21
        targetSdk = 31
        versionCode = 1
        versionName = "1.0"
    }
    // 4
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
}
  1. compileSdk specifies the Android SDK version to compile against.
  2. applicationId is the ID for the Android App. This has to be unique for every app on the Google Play store.
  3. 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.
  4. 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.

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 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")
    kotlin("native.cocoapods")
    id("com.android.library")
}

The first plugin is for KMP and defines this module as a multiplatform module. The next plugin is for iOS and brings in CocoaPods. The last 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:

kotlin {
    // 1
    android()

    // 2
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    // 3
    cocoapods {
        summary = "Holds Time zone information"
        homepage = "Link to the Shared Module homepage"
        ios.deploymentTarget = "14.1"
        framework {
            baseName = "shared"
        }
        podfile = project.file("../iosApp/Podfile")
    }
    ...
}
  1. Use the android() method to define an Android target.
  2. iosX64 defines a target for the iOS simulator on x86_64 platforms, while iosArm64 defines a target for iOS on ARM64 platforms.
  3. Defines the details for building the CocoaPods Podfile (it will be in the iosApp directory). The main thing here is the baseName, which is shared, and the path to the Podfile.

Next, you define the source sets. These use predefined variables:

   sourceSets {
        // 1
        val commonMain by getting
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        // 2
        val androidMain by getting
        val androidTest by getting {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.13.2")
            }
        }
        // 3
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
        }
    }
  1. Define the commonMain and testing dependencies. Currently, commonMain has none.
  2. Define the Android dependencies.
  3. Define the iOS dependencies.

You’ll add dependencies to this section later.

Next, you define the Android section:

android {
    compileSdk = 31
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdk = 21
        targetSdk = 31
    }
}

This is similar to Android’s build file, except it just has the minimum information needed.

iOS

iOS uses several different dependency management systems. One of the more common systems is CocoaPods. If you open the iOSApp/Podfile file, you’ll see there isn’t much to it:

target 'iosApp' do
  use_frameworks!
  platform :ios, '14.1'
  pod 'shared', :path => '../shared'
end

The most important part is the pod 'shared', :path => '../shared' section. This defines the shared framework that KMP will create and that you can use in your iOS apps.

BuildSrc

One of the nice features of the newer Gradle versions is the concept of the buildSrc module. This is a module where you can define version variables as well as define your own plugins. To create this module, start by right-clicking the root folder in the project view. Then, select New ▸ Directory. Enter buildSrc. Since this will be a module, you’ll need a build file. Right-click buildSrc and choose New ▸ File. Name the file build.gradle.kts and add the following code to it:

repositories {
    mavenCentral()
}

plugins {
    `kotlin-dsl`
}

The code above includes the Kotlin DSL plugin. Now, sync your Gradle files by clicking Sync Now at the top right corner of the window. Then, right-click buildSrc, select New ▸ Directory and choose src/main/kotlin. Right-click the kotlin directory and select New ▸ Kotlin Class/File. Name the file Dependencies and choose File. This file will contain all the variables you’ll need to define all your plugins and dependencies. The reason for creating this file is to avoid having to maintain all the versions for your plugins and dependencies in many different files. Defining the versions in one place makes it easy to change later.

Define all the plugin names by adding this code to the file:

const val androidPlugin = "android"
const val androidApp = "com.android.application"
const val androidLib = "com.android.library"
const val multiplatform = "multiplatform"
const val composePlugin = "org.jetbrains.compose"
const val cocopods = "native.cocoapods"

The code above defines regular Kotlin variables you can use throughout your Gradle scripts. These aren’t required, but they make it easier to add. Next, define the version numbers by adding the following code:

object Versions {
    // 1
    const val min_sdk = 24
    const val target_sdk = 31
    const val compile_sdk = 31

    // 2
    // Plugins
    const val kotlin = "1.6.10"
    const val kotlin_gradle_plugin = "1.6.10"
    const val android_gradle_plugin = "7.0.4"
    const val desktop_compose_plugin = "1.0.1"
    const val compose_compiler_version= "1.1.0-rc02"
    const val compose_version= "1.1.0-rc01"
		// TODO: Add Other versions
}
// TODO: Add Deps
  1. These define the SDK versions for Android. A min SDK version of 24 is needed for some of the libraries. You’ll notice these versions are consistent with the ones you saw earlier in the build.gradle.kts files.
  2. These define the versions for your plugins.

Notice the two plugins with the word compose in them. You’ll write your Android UI in Jetpack Compose and your desktop app in Desktop Compose. Now, define the version numbers for your libraries. Replace // TODO: Add Other versions with:

const val coroutines = "1.5.0-native-mt"
const val junit = "4.13.2"
const val material = "1.4.0"
const val kotlinxDateTime = "0.3.1"
const val activity_compose = "1.4.0"
const val napier = "2.1.0"
const val junit5 = "1.5.10"
const val frameworkName = "shared"

This defines version numbers for some of the libraries you’ll use. Next, replace // TODO: Add Deps with:

object Deps {
    const val android_gradle_plugin = "com.android.tools.build:gradle:${Versions.android_gradle_plugin}"
    const val kotlin_gradle_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin_gradle_plugin}"
    
  
    const val junit = "junit:junit:${Versions.junit}"
    const val material = "com.google.android.material:material:${Versions.material}"
    const val napier = "io.github.aakira:napier:${Versions.napier}"

    // TODO: Add Compose
}

This defines variables for the plugins and for some useful libraries.

Notice how you use substitution to insert the version number. This ensures all modules reference the same dependencies and their versions.

Next, add the Compose libraries. These are libraries for building the new Jetpack Compose UI in Android. Replace // TODO: Add Compose with:

object Compose {
    const val ui = "androidx.compose.ui:ui:${Versions.compose_version}"
    const val uiUtil = "androidx.compose.ui:ui-util:${Versions.compose_version}"
    const val tooling = "androidx.compose.ui:ui-tooling:${Versions.compose_version}"
    const val foundation = "androidx.compose.foundation:foundation:${Versions.compose_version}"
    const val material = "androidx.compose.material:material:${Versions.compose_version}"
    const val material_icons = "androidx.compose.material:material-icons-extended:${Versions.compose_version}"
    const val runtime = "androidx.compose.runtime:runtime:${Versions.compose_version}"
    const val compiler = "androidx.compose.compiler:compiler:${Versions.compose_version}"
    const val runtime_livedata = "androidx.compose.runtime:runtime-livedata:${Versions.compose_version}"
    const val foundationLayout = "androidx.compose.foundation:foundation-layout:${Versions.compose_version}"
    const val activity = "androidx.activity:activity-compose:${Versions.activity_compose}"
}
// TODO: Add Coroutines

You may not need all these, but it will be useful for other projects. You’ll learn about quite a few libraries in a later chapter.

Next, replace // TODO: Add Coroutines with:

object Coroutines {
    const val common = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
    const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
    const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}"
}
// TODO: Add JetBrains

This provides access to the Kotlin Coroutine libraries — very useful for asynchronous programming.

Next, replace // TODO: Add JetBrains with:

object JetBrains {
    const val datetime = "org.jetbrains.kotlinx:kotlinx-datetime:${Versions.kotlinxDateTime}"
    const val uiDesktop = "org.jetbrains.compose.ui:ui-desktop:${Versions.desktop_compose_plugin}"
    const val uiUtil = "org.jetbrains.compose.ui:ui-util:${Versions.desktop_compose_plugin}"
}

These are a few libraries from JetBrains — the developers of KMP — for multiplatform datetime handling, as well as Desktop Compose, which will be introduced later.

Now that you have all your variables defined, you’ll clean up your build files with these new variables.

Shared build file

Open the shared build.gradle.kts and replace the plugins with:

kotlin(multiplatform)
id(androidLib)
kotlin(cocopods)

Notice how you didn’t need to include any files to get these variables. That’s because Gradle can automatically read the constants defined within buildSrc.

Clean up the android section by replacing it with the code below:

android {
    compileSdk =  Versions.compile_sdk
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdk = Versions.min_sdk
        targetSdk = Versions.target_sdk
    }
}

The code above replaces the SDK versions with the ones defined inside buildSrc.

Next, open the root build.gradle.kts and replace the classpath strings with:

classpath(Deps.android_gradle_plugin)
classpath(Deps.kotlin_gradle_plugin)

Do a Gradle sync to make sure it works.

Android build file

Open androidApp’s build.gradle.kts and replace the plugins section with:

plugins {
    id(androidApp)
    kotlin(androidPlugin)
}

This just replaces the strings with the variables. Replace the dependencies section with:

dependencies {
    // 1
    implementation(project(":shared"))
    // 2
    with(Deps) {
        implementation(napier)
        implementation(material)
    }

    // 3
    //Compose
    with(Deps.Compose) {
        implementation(compiler)
        implementation(runtime)
        implementation(runtime_livedata)
        implementation(ui)
        implementation(tooling)
        implementation(foundation)
        implementation(foundationLayout)
        implementation(material)
        implementation(material_icons)
        implementation(activity)
    }
}
  1. This imports the shared module.
  2. This defines napier, a multiplatform library for logging, and the Google material library. Notice how you can use the Kotlin with keyword to avoid repeating parts of the variable names.
  3. Define most of the Jetpack Compose libraries.

Next, replace the android section with the following code:

android {
    compileSdk = Versions.compile_sdk
    defaultConfig {
        applicationId = "com.raywenderlich.findtime.android"
        minSdk = Versions.min_sdk
        targetSdk = Versions.target_sdk
        versionCode = 1
        versionName = "1.0"
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    compileOptions {
      sourceCompatibility = JavaVersion.VERSION_1_8
      targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
      jvmTarget = "1.8"
    }
    buildFeatures {
      compose = true
    }
    composeOptions {
      kotlinCompilerExtensionVersion = Versions.compose_compiler_version
    }
}

The code above sets the Java compatibility to 1.8, enables Compose in the project and specifies the Kotlin compiler extension version that it should use.

Do another Gradle sync to make sure everything still works. Run the app on Android. You’ll see a screen similar to the one shown below:

Fig. 2.1 - Android app running fine
Fig. 2.1 - Android app running fine

You haven’t changed the UI yet, but this makes sure 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. You’ll find the Platform and Greeting classes. 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.

Fig. 2.2 - Usages of the Greeting class
Fig. 2.2 - Usages of the Greeting class

Double-click return Greeting() to open MainActivity. Delete the greet function, the imports and the reference to it below:

Fig. 2.3 - Remove references to the Greeting class
Fig. 2.3 - Remove references to the Greeting class

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. Change it to:

val commonMain by getting {
    dependencies {
        // 1
        implementation(Deps.JetBrains.datetime)
        // 2
        implementation(Deps.napier)
    }
}

This will import two libraries:

  1. JetBrains datetime library.
  2. Napier logging library.

Now do a Gradle sync.

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 and ZoneOffset help you convert between Instant and LocalDateTime.
  • Month is an enum representing all months in the year.
  • DayOfWeek is an enum representing all days of the week. It uses values like MONDAY, TUESDAY, etc. instead of integers.
  • DateTimePeriod represents the difference between 2 instants.
  • DatePeriod is a subclass of DateTimePeriod and represents the difference between two LocalDate instances.
  • DateTimeUnit provides a set of units such as NANOSECOND, WEEK, CENTURY, etc. that you can use to perform arithmetic operations on Instant and LocalDate.

Time Zone Helper

Go to the com/raywenderlich/findtime package in shared/commonMain/ and create a new Kotlin inteface 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 at endHour 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. Press Option-Return while keeping your cursor on TimeZoneHelperImpl and choose Implement Members.

Fig. 2.4 - Implement all methods of TimeZoneHelper
Fig. 2.4 - Implement all methods of TimeZoneHelper

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:

  1. Use a StringBuilder to build the string piece by piece.
  2. Get the hour and minutes from the dateTime argument.
  3. Since you want a string with am/pm, check if the hour is greater than noon (12).
  4. Build the hour and colon.
  5. Check to make sure numbers 0-9 are padded.
  6. Return the final string.

Now update the currentTime method to:

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 code above, you:

  1. Get the current time as an Instant.
  2. Convert the current moment into a LocalDateTime that’s based on the current user’s time zone.
  3. 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:

  1. Get the time zone with the given ID.
  2. Get the current time as an Instant.
  3. Convert the current moment into a LocalDateTime that’s based on the passed-in time zone.
  4. 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:

  1. Get the current time zone.
  2. Get the current time/instant.
  3. Get the other time zone.
  4. Convert the current time into a LocalDateTime class.
  5. Convert the current time in another time zone into a LocalDateTime class.
  6. 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 would 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
  1. Use datetime’s Clock.System.now method to get the current instant in the UTC time zone.
  2. Convert the instant into another time zone with toLocalDateTime, passing in the other time zone.
  3. 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:

  1. Convert that hour into the current time zone.
  2. Convert your time zone hour to the other time zone.
  3. 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:

  1. Create a list to return all the hours that are valid.
  2. Create a time range from start to end hours.
  3. Get the current time zone.
  4. Go through each hour in the time range.
  5. Go through each time zone in the time zone list.
  6. If it’s the same time zone as the current one, then you know it’s good.
  7. Check if the hour is valid.
  8. If, after going through every hour and it’s a good hour, add it to our list.
  9. 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. You’ll see the iOS build fail with the error shown below:

Fig. 2.5 - Xcode build error details
Fig. 2.5 - Xcode build error details

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.

Key points

  • Gradle is the build system for most of KMP projects.
  • You write the Gradle build files in Kotlin.
  • You use the buildSrc module to define variables and plugins.
  • 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 setup your project with the buildSrc module 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.

In the next chapter, you’ll start building the UI for the Find Time project.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.