Основы Kotlin Multiplatform. Бесплатный учебник на русском языке (KA0700)
Основы Kotlin Multiplatform. Бесплатный учебник на русском языке (KA0700)

KA0702 — Системы сборки для KMP (знакомство с Gradle)

  • Запись изменена:20.12.2022
  • Post category:Публикации
  • Reading time:28 минут чтения

В предыдущей главе вы создали свой первый проект KMP. Чтобы начать, вам нужно понять, какую систему сборки использует KMP. Для Android и настольных компьютеров это Gradle. В этой главе для iOS вы будете использовать CocoaPods. (Вы узнаете о другом методе в следующей главе).

Знакомство с Gradle

Если вы пришли из мира Android, у вас уже есть некоторый опыт работы с Gradle, но, вероятно, это тот, который написан на языке Groovy. Эти файлы называются build.gradle . Если вы не знакомы с Android, Gradle — это система сборки для разработки программного обеспечения. Он может выполнять задачи по компиляции, упаковке, тестированию, развертыванию и даже публикации артефактов в центре распространения. Многие системы программирования используют его. Хотя он может быть довольно сложным, он может использовать различные плагины практически для любых целей.

Для KMP вы будете использовать версию Gradle для сценариев Kotlin. Эти файлы называются build.gradle.kts. Расширение kts расшифровывается как “сценарий Kotlin”. Эта версия использует Kotlin для Gradle DSL (Domain Specific Language), что значительно упрощает ее использование, если вы уже знаете Kotlin. В этом проекте есть несколько таких сценариев сборки в разных каталогах. Каждый служит определенной цели. Откройте начальный проект или проект из последней главы в Android Studio, перейдите в режим просмотра проекта и следуйте инструкциям, чтобы узнать больше об этих файлах сборки.

Если вы не знаете, что делает конкретная команда, вы можете щелкнуть по имени, нажимая клавишу command, и Android Studio откроет класс.

Файлы сборки

Откройте build.gradle.kts в корневой папке.

// 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. Раздел buildscript описывает всю информацию о том, где можно получить плагины и их версии.
  2. репозитории описывают источники, в которых размещены эти плагины. mavenCentral это место, где проживает большинство, но и у Google, и у Gradle есть свои собственные.
  3. зависимости описывают плагины и версии, которые вы будете использовать. Здесь вы используете плагин Kotlin Gradle и плагин Android Gradle.

Далее, есть:

// 1
allprojects {
    // 2
    repositories {
        google()
        mavenCentral()
    }
}
  1. allprojects означает, что следующее применимо ко всем модулям в этом проекте.
  2. репозитории определяют все репозитории, необходимые для всех модулей.

Последний раздел:

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

Это определяет вызываемую задачу Gradle clean. Задача удаляет корневой каталог сборки. Вы используете его, когда вам нужна свежая сборка.

Теперь откройте AndroidApp/build.gradle.kts. За исключением другого синтаксиса Kotlin, это должно выглядеть знакомо разработчикам Android. Это начинается с определения плагинов, необходимых для Android:

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

Android нуждается в приложении и плагинах Android Kotlin. В следующем разделе показано, какие зависимости вам нужны:

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 зависит от sharedмодуля (где будет находиться вся общая бизнес-логика).
  2. Библиотеки, специфичные для Android (материальные компоненты и ConstraintLayout).

Далее, настройки, специфичные для Android:

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 указывает версию Android SDK для компиляции.
  2. applicationId — это идентификатор приложения для Android. Это должно быть уникальным для каждого приложения в магазине Google Play.
  3. minSdk относится к самой низкой версии Android, на которой будет работать ваше приложение, в то время как targetSdk относится к последней версии Android, которую вы поддерживаете. versionCode — это номер, который вы будете использовать внутри, чтобы различать сборки, в то время как versionName — это версия, которая будет отображаться в Play Store.
  4. Укажите параметры отладки или выпуска здесь. Для выпускной версии это значение присваивает isMinifyEnabled значение false, но вы, вероятно, захотите установить для него значение true, когда будете готовы к выпуску.

Общий файл сборки

Откройте общий доступ/build.gradle.kts. Это сценарий сборки для общего модуля. Если вы откроете каталог src, вы увидите каталоги androidMaincommonMain и iosMain. Они содержат общие файлы для Android и iOS, а также файлы, которые совместно используются всеми модулями.

Первый раздел — это плагины:

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
}

Первый плагин предназначен для KMP и определяет этот модуль как мультиплатформенный модуль. Следующий плагин предназначен для iOS и включает CocoaPods. Последний плагин предназначен для Android. Вы будете использовать это для создания библиотеки Android для использования в приложении Android.

Следующим будет раздел Котлина. В этом разделе используется описанный выше мультиплатформенный плагин для настройки этого модуля для 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. Используйте метод android(), чтобы определить цель Android.
  2. iosX64 определяет цель для симулятора iOS на платформах x86_64, в то время как iosArm64 определяет цель для iOS на платформах ARM64.
  3. Определяет детали для создания подфайла CocoaPods (он будет находиться в каталоге iosApp). Главное здесь — это общее базовое имя и путь к подфайлу.

Затем вы определяете исходные наборы. Они используют предопределенные переменные:

   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. Определите общие основные и тестовые зависимости. В настоящее время у commonMain нет ни одного.
  2. Определите зависимости Android.
  3. Определите зависимости iOS.

Вы добавите зависимости в этот раздел позже.

Далее вы определяете раздел Android:

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

Это похоже на файл сборки Android, за исключением того, что он содержит только минимум необходимой информации.

iOS

iOS использует несколько различных систем управления зависимостями. Одной из наиболее распространенных систем являются коконоподия. Если вы откроете файл iOSApp / Podfile, вы увидите, что в нем не так уж много:

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

Самая важная часть — это pod 'shared', :path => '../shared'раздел. Это определяет общую структуру, которую создаст KMP и которую вы сможете использовать в своих приложениях для iOS.

buildSrc

Одной из приятных особенностей новых версий Gradle является концепция модуля buildSrc. Это модуль, в котором вы можете определять переменные версии, а также определять свои собственные плагины. Чтобы создать этот модуль, начните с щелчка правой кнопкой мыши по корневой папке в представлении проекта. Затем выберите Создать ▸ Каталог. Войдите в buildSrc. Поскольку это будет модуль, вам понадобится файл сборки. Щелкните правой кнопкой мыши buildSrc и выберите Создать ▸ Файл. Назовите файл build.gradle.kts и добавьте к нему следующий код:

repositories {
    mavenCentral()
}

plugins {
    `kotlin-dsl`
}

Приведенный выше код включает в себя плагин Kotlin DSL. Теперь синхронизируйте файлы Gradle, нажав Синхронизировать сейчас в правом верхнем углу окна. Затем щелкните правой кнопкой мыши buildSrc, выберите Создать ▸ Каталог и выберите src/main/kotlin. Щелкните правой кнопкой мыши каталог kotlin и выберите Создать ▸ Класс/файл Kotlin. Назовите зависимости файлов и выберите Файл. Этот файл будет содержать все переменные, которые вам понадобятся для определения всех ваших плагинов и зависимостей. Причина создания этого файла заключается в том, чтобы избежать необходимости поддерживать все версии ваших плагинов и зависимостей во многих разных файлах. Определение версий в одном месте упрощает последующее изменение.

Определите все имена плагинов, добавив этот код в файл:

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"

Приведенный выше код определяет обычные переменные Kotlin, которые вы можете использовать в своих скриптах Gradle. Они не являются обязательными, но они упрощают добавление. Затем определите номера версий, добавив следующий код:

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. Они определяют версии SDK для Android. Для некоторых библиотек требуется минимальная версия SDK 24. Вы заметите, что эти версии соответствуют тем, которые вы видели ранее в файлах build.gradle.kts.
  2. Они определяют версии для ваших плагинов.

Обратите внимание на два плагина со словом composeв них. Вы напишете свой пользовательский интерфейс для Android в Jetpack Compose, а свое настольное приложение — в Desktop Compose. Теперь определите номера версий для ваших библиотек. Заменить // TODO: Add Other versionsна:

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"

Это определяет номера версий для некоторых библиотек, которые вы будете использовать. Затем замените // TODO: Add Depsна:

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
}

Это определяет переменные для плагинов и для некоторых полезных библиотек.

Обратите внимание, как вы используете замену для вставки номера версии. Это гарантирует, что все модули ссылаются на одни и те же зависимости и их версии.

Затем добавьте Composeбиблиотеки. Это библиотеки для создания нового пользовательского интерфейса Jetpack Compose в Android. Заменить // TODO: Add Composeна:

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

Возможно, вам все это не понадобится, но это будет полезно для других проектов. Вы узнаете о нескольких библиотеках в следующей главе.

Затем замените // TODO: Add Coroutinesна:

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

Это обеспечивает доступ к библиотекам сопрограмм Kotlin, что очень полезно для асинхронного программирования.

Затем замените // TODO: Add JetBrainsна:

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}"
}

Это несколько библиотек от JetBrains — разработчиков KMP — для мультиплатформенной обработки данных и времени, а также для создания рабочего стола, который будет представлен позже.

Теперь, когда у вас определены все переменные, вы очистите файлы сборки с помощью этих новых переменных.

Общий файл сборки

Откройте общий файл build.gradle.kts и замените плагины на:

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

Обратите внимание, что вам не нужно было включать какие-либо файлы, чтобы получить эти переменные. Это потому, что Gradle может автоматически считывать константы, определенные в buildSrc.

Очистите androidраздел, заменив его приведенным ниже кодом:

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

Приведенный выше код заменяет версии SDK версиями, определенными внутри buildSrc.

Затем откройте корневой файл build.gradle.kts и замените строки пути к классу на:

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

Выполните синхронизацию Gradle, чтобы убедиться, что она работает.

Файл сборки Android

Откройте build.gradle.kts приложения AndroidApp и замените pluginsраздел на:

plugins {
    id(androidApp)
    kotlin(androidPlugin)
}

Это просто заменяет строки переменными. Заменить dependenciesраздел на:

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. Это импортирует общий модуль.
  2. Это определяет napierмультиплатформенную библиотеку для ведения журнала и библиотеку Googlematerial. Обратите внимание, как вы можете использовать withключевое слово Kotlin, чтобы избежать повторения частей имен переменных.
  3. Определите большинство библиотек JetpackCompose.

Затем замените androidраздел следующим кодом:

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
    }
}

Приведенный выше код устанавливает совместимость с Java на 1.8, включает Compose в проекте и указывает версию расширения компилятора Kotlin, которую он должен использовать.

Выполните еще одну синхронизацию Gradle, чтобы убедиться, что все по-прежнему работает. Запустите приложение на Android. Вы увидите экран, аналогичный показанному ниже:

Рис. 2.1 - Приложение для Android работает нормально
Рис. 2.1 — Приложение для Android работает нормально

Вы еще не изменили пользовательский интерфейс, но это гарантирует, что файлы Gradle все еще работают.

Найдите время

Вам когда-нибудь нужно было назначить встречу с коллегами, которые работают в разных часовых поясах? Это может быть настоящей болью. Бодрствуют ли они в то время, которое вы хотите назначить? На какое время лучше назначить встречу? Вы собираетесь написать приложение Find Time, чтобы помочь найти те часы, которые работают лучше всего. Чтобы сделать это, вам нужно написать некоторую логику часовых поясов, чтобы определить лучшие часы для встречи. Если бы вы писали отдельные приложения для iOS и Android, вам пришлось бы написать эту бизнес-логику дважды.

Бизнес-логика

Одним из основных преимуществ KMP является то, что вы можете совместно использовать бизнес-логику для всех своих платформ. Вы напишете свою бизнес-логику в общем модуле. Этот модуль является мультиплатформенным модулем, который можно использовать для iOS, Android, настольных компьютеров и Интернета.

Откройте общую папку /src, а затем папки androidMain, commonMain и iosMain. Вы найдете PlatformGreetingклассы и. Удалите эти классы, так как вы не будете их использовать. Обратите внимание, что платформы Android и iOS не будут запускаться, пока вы не удалите код, который их вызвал. Android Studio будет жаловаться на их использование. Нажмите кнопку Просмотреть способы использования.

Рис. 2.2 - Использование класса приветствия
Рис. 2.2 — Использование класса приветствия

Дважды щелкнитеreturn Greeting(), чтобы открыть MainActivity. Удалите greetфункцию, импорт и ссылку на нее ниже:

Рис. 2.3 - Удаление ссылок на класс приветствия
Рис. 2.3 — Удаление ссылок на класс приветствия

Запустите удаление еще раз, и вы сможете удалить файлы без каких-либо проблем.

Вычисления даты и времени

Вы будете использовать kotlinx-datetimeбиблиотеку JetBrains, чтобы помочь с вычислением даты и времени. Откройте shared/build.gradle.kts и найдите val commonMain. Измените его на:

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

Это позволит импортировать две библиотеки:

  1. Библиотека даты и времени JetBrains.
  2. Библиотека протоколирования Нейпира.

Теперь выполните синхронизацию Gradle.

Библиотека даты и времени

Kotlin’s kotlinx-datetime— это простая в использовании мультиплатформенная библиотека, которая помогает выполнять вычисления на основе даты и времени. Он использует несколько типов данных:

  • Instant представляет собой момент времени.
  • Clock имитирует реальные часы и показывает текущее мгновение.
  • LocalDateTime представляет дату со временем, но без связанного часового пояса.
  • LocalDate представляет только дату.
  • TimeZone и ZoneOffsetпоможет вам конвертировать между Instantи LocalDateTime.
  • Month является перечислением, представляющим все месяцы в году.
  • DayOfWeek является перечислением, представляющим все дни недели. Он использует такие значения, как MONDAYTUESDAY, и т.д. вместо целых чисел.
  • DateTimePeriod представляет собой разницу между 2 моментами.
  • DatePeriod является подклассом DateTimePeriodи представляет разницу между двумя LocalDateэкземплярами.
  • DateTimeUnit предоставляет набор единиц, таких как NANOSECONDWEEKCENTURY, и т.д., которые можно использовать для выполнения арифметических операций над и .InstantLocalDate

Помощник по часовому поясу

Перейдите в пакет com/raywenderlich/findtime в shared/commonMain/ и создайте новый интерфейс Kotlin с именем TimeZoneHelper. Добавьте следующее:

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>
}

Это определяет интерфейс, который имеет семь функций.

  • Возвращает список строк часовых поясов. (Это список всех часовых поясов из библиотеки JetBrains kotlinx-datetime)
  • Возвращает текущее отформатированное время.
  • Возвращает текущий идентификатор часового пояса.
  • Возвращает количество часов из заданного часового пояса.
  • Возвращает форматированное время для данного часового пояса.
  • Возвращает форматированную дату для данного часового пояса.
  • Найдите список часов, которые начинаютсяstartHour, заканчиваются endHourи находятся во всех заданных строках часовых поясов.

Создание интерфейса упрощает тестирование. В этой главе не рассматриваются тесты, но использование интерфейса упрощает создание помощников с имитацией часового пояса. Теперь создайте экземпляр этого интерфейса. Щелкните правой кнопкой мыши папку findtime и создайте новый класс Kotlin с именем TimeZoneHelperImpl. Этот класс будет реализовывать интерфейс. Обновите класс, чтобы расширить TimeZoneHelperего следующим образом:

class TimeZoneHelperImpl: TimeZoneHelper {
}

Вы увидите красную линию под классом, потому что вы еще не внедрили методы. Нажмите Option-Return, удерживая курсор включеннымTimeZoneHelperImpl, и выберите Элементы реализации.

Рис. 2.4 - Реализация всех методов TimeZoneHelper
Рис. 2.4 — Реализация всех методов TimeZoneHelper

Выберите все и нажмите кнопку ОК. Вы увидите много задач.

Начнем с getTimeZoneStringsчего . Звучит так, как будто это может быть действительно сложно, но библиотека kotlinx-datetime упрощает это. Замените задачу на:

return TimeZone.availableZoneIds.sorted()

Эта строка возвращает доступные идентификаторы часовых поясов и сортирует их. TimeZoneбудет красным. Наведите курсор TimeZoneи нажмите Option-Return, чтобы импортировать библиотеку. Вы также можете использовать этот метод для импорта необходимых классов в следующих разделах.

Далее вам понадобится метод для форматирования LocalDateTimeкласса datetime. Добавьте следующий метод в нижней части класса:

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()
}

В приведенном выше коде вы:

  1. Используйте StringBuilder для сборки строки по частям.
  2. Получите часы и минуты из аргумента DateTime .
  3. Поскольку вам нужна строка с am / pm, проверьте, не больше ли час, чем полдень (12).
  4. Постройте час и двоеточие.
  5. Убедитесь, что цифры 0-9 дополнены.
  6. Верните последнюю строку.

Теперь обновите currentTimeметод, чтобы:

override fun currentTime(): String {
  // 1
  val currentMoment: Instant = Clock.System.now()
  // 2
  val dateTime: LocalDateTime = currentMoment.toLocalDateTime(TimeZone.currentSystemDefault())
  // 3
  return formatDateTime(dateTime)
}

В приведенном выше коде вы:

  1. Получите текущее время как мгновенное.
  2. Преобразуйте текущий момент в LocalDateTime, основанный на часовом поясе текущего пользователя.
  3. Отформатируйте заданную дату, используя formatDateTimeметод, который вы определили ранее.

Теперь реализуем getTimeметод:

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)
}

В приведенном выше коде вы:

  1. Получить часовой пояс с заданным идентификатором.
  2. Получите текущее время как мгновенное.
  3. Преобразуйте текущий момент в LocalDateTime, основанный на переданном часовом поясе.
  4. Отформатируйте заданную дату.

getDate аналогично getTimegetDateЗамените следующим кодом:

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}"
}

Это использует разные части DateTime для создания строки типа: “Понедельник, 4 октября”.

currentTimeZoneМетод довольно прост. Возвращает текущий часовой пояс в виде строки:

override fun currentTimeZone(): String {
    val currentTimeZone = TimeZone.currentSystemDefault()
    return currentTimeZone.toString()
}

hoursFromTimeZoneМетод немного сложный. Вы хотите вернуть количество часов из заданного часового пояса:

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)
}

В приведенном выше коде вы:

  1. Получить текущий часовой пояс.
  2. Получите текущее / мгновенное время.
  3. Получите другой часовой пояс.
  4. Преобразуйте текущее время в класс LocalDateTime.
  5. Преобразуйте текущее время в другом часовом поясе в класс LocalDateTime.
  6. Верните абсолютную разницу между часами (не должна быть отрицательной), убедившись, что результат равен двойному.

Поиск

Поиск немного сложнее. Учитывая начальный час (например, 8 утра), конечный час (скажем, 5 вечера) и список часовых поясов, в которых все находятся, вы хотите вернуть список целых чисел, представляющих часы (0-23), которые соответствуют часовым поясам каждого. Итак, если вы проедете с 8 утра до 5 вечера в Лос-Анджелес и Нью-Йорк, вы получите список часов:

[8,9,10,11,12,13,14]

Все эти часы для Лос-Анджелеса работают и для Нью-Йорка. Лос-Анджелес может работать до 2 часов дня (14), в то время как Нью-Йорк начнется в 11 часов утра и продлится до 5 часов вечера. Чтобы узнать, действителен ли час, добавьте isValidметод после searchметода:

private fun isValid(
    timeRange: IntRange,
    hour: Int,
    currentTimeZone: TimeZone,
    otherTimeZone: TimeZone
): Boolean {
    if (hour !in timeRange) {
        return false
    }
    // TODO: Add Current Time
}

Этот метод требует временного диапазона (например, 8 ..17), заданного часа для проверки, текущего часового пояса для пользователя и другого часового пояса, который вы проверяете. Первая проверка проверяет, находится ли час в заданном временном диапазоне. Если нет, то это недействительно.

Теперь замените // TODO: Add Current Timeна:

// 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. Используйте Clock.System.nowметод datetime, чтобы получить текущий момент в часовом поясе UTC.
  2. Преобразуйте мгновение в другой часовой пояс с toLocalDateTimeпомощью , переходя в другой часовой пояс.
  3. Получите LocalDateTime с заданным часом. (Минуты, секунды и наносекунды не требуются)

Теперь замените // TODO: Add Conversionsна:

// 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 — это библиотека ведения журнала, которую необходимо импортировать. Наведите курсор Napierи нажмите Option-Return на него, чтобы импортировать библиотеку.

В предыдущем коде вы:

  1. Преобразуйте этот час в текущий часовой пояс.
  2. Преобразуйте часы вашего часового пояса в часы другого часового пояса.
  3. Проверьте, соответствует ли это вашему временному диапазону.

Теперь, когда у вас есть isValidметод, searchметод не будет таким сложным. Вам просто нужно просмотреть все заданные часовые пояса и часы и проверить, действительны ли они. Обновите searchметод с помощью:

// 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

В этом коде вы:

  1. Создайте список, чтобы вернуть все действительные часы.
  2. Создайте временной диапазон от начального до конечного времени.
  3. Получить текущий часовой пояс.
  4. Просматривайте каждый час в диапазоне времени.
  5. Просмотрите каждый часовой пояс в списке часовых поясов.
  6. Если это тот же часовой пояс, что и текущий, то вы знаете, что это хорошо.
  7. Проверьте, действителен ли указанный час.
  8. Если, пройдя через каждый час, и это хороший час, добавьте его в наш список.
  9. Верните список часов.

Импортируйте методы minи.max Теперь вы написали бизнес-логику для приложения Time Finder! Android, iOS, настольные и веб-платформы могут использовать его совместно.

Создайте приложение, чтобы убедиться, что оно все еще компилируется. Вы можете попробовать снова запустить как Android, так и iOS. Вы увидите сбой сборки iOS с ошибкой, показанной ниже:

Рис. 2.5 - Сведения об ошибке сборки Xcode
Рис. 2.5 — Сведения об ошибке сборки Xcode

Помните, что вы удалили этот greetметод. Как бы вы это исправили?

Вызов

Приложение для iOS больше не работает. Выясните, как найти проблему, исправить ошибку и возобновить работу приложения для iOS.

Ключевые моменты

  • Gradle — это система сборки для большинства проектов KMP.
  • Вы пишете файлы сборки Gradle в Kotlin.
  • Вы используете модуль buildSrc для определения переменных и плагинов.
  • Вы пишете бизнес-логику в общем модуле.
  • Библиотека kotlinx-datetime — это мультиплатформенная библиотека для обработки дат и времени.

Куда идти дальше?

В этой главе вы узнали, как настроить свой проект с помощью модуля buildSrc, чтобы упростить изменение версий. Вы также узнали, как работать с датами и временем в общем модуле, что делает его доступным для всех платформ.

В следующей главе вы приступите к созданию пользовательского интерфейса для проекта Find Time.