Привет! Меня зовут Антон, я Android-разработчик. Недавно у меня появилась идея создать приложение, которое в будущем можно будет опубликовать в сторы. С самого начала я знал, что хочу, чтобы оно работало сразу на двух платформах — iOS и Android.
Передо мной стояло два пути: погрузиться в нативную разработку для iOS или воспользоваться кроссплатформенной технологией. Первый вариант, безусловно, интересен, но требует слишком много времени на освоение. А вот с кроссплатформенной разработкой у меня уже был опыт, поэтому решение далось легко — я выбрал Kotlin Multiplatform (KMP).
На одном из этапов разработки я задумался: как лучше организовать версионирование проекта, если почти весь код находится в shared-модуле? Мне нужно было задать версию приложения в коде, например, для механизма force update, но без дублирования значений в shared-коде, Gradle (на Android) и Info.plist (на iOS). Такой разнобой чреват ошибками — человеческий фактор никто не отменял. Конечно, можно делегировать эту задачу CI, но мне хотелось простого решения: изменил один раз — и все работает.
Мой pet-проект написан на Compose Multiplatform, что делает его практически независимым от платформ. Мне нравится концепция «написал один раз — запустил везде». Однако, изучая материалы по теме, я не нашел готового решения, которое бы полностью меня устроило. Встречались похожие подходы, но у каждого были свои недостатки. Поэтому, вдохновившись чужими наработками, я решил создать универсальное решение для обеих платформ.
Итак, начнем с того, что мы укажем в нашем gradle.properties
файла версию
version=1.0.0
Затем мы создадим buildSrc
«модуль», в котором будет храниться kotlin код, который мы сможем использовать в gradle-файлах различных модулей нашего проекта
plugins { `kotlin-dsl` } repositories { google() mavenCentral() }
Теперь мы можем создать файл с котлин функциями, чтобы переиспользовать их в различных build.gradle.kts
файлах.
Расположите ваш класс по пути buildSrc/src/main/kotlin/.../VersionUtils.kt
Мой код, который я буду в дальнейшем использовать выглядит следующим образом.
import org.gradle.api.Project fun Project.getAppVersionString(): String { val (major, minor, patch) = getSanitizedVersionParts() return "$major.$minor.$patch" } fun Project.getNumericAppVersion(): Int { val (major, minor, patch) = getSanitizedVersionParts() return major * 1_000_000 + minor * 1_000 + patch } private fun Project.getSanitizedVersionParts(): Triple<Int, Int, Int> { val sanitizedVersionString = project.version.toString().let { appVersionString -> appVersionString.indexOfOrNull("-")?.let { indexOfFirstDash -> appVersionString.substring(0, indexOfFirstDash) } ?: appVersionString } return sanitizedVersionString .split('.') .take(3) .map { it.toInt() } .let { Triple(it[0], it[1], it[2]) } } private fun String.indexOfOrNull(substring: String): Int? { val index = indexOf(substring) return if (index >= 0) index else null }
Здесь мы извлекаем версию из gradle.properties
и преобразуем ее как в числовой, так и в строковый формат. Теперь у нас есть возможность получать версию прямо в kts-файлах. Но как использовать ее непосредственно в коде? На этом этапе мы можем просто сгенерировать Kotlin-класс. Для этого создадим Gradle-таску, которая автоматически будет генерировать этот класс. Не забудьте импортировать ваши getNumericAppVersion
и getAppVersionString
.
tasks.register("generateVersionConfig") { val configFile = file(project.rootDir.toString() + "/shared/src/commonMain/kotlin/.../VersionConfig.kt") outputs.file(configFile) val content = """ // Не менять файл, он сгенерирован с помощью таски generateVersionConfig object VersionConfig { const val VERSION_CODE = ${getNumericAppVersion()} const val VERSION_NAME = "${getAppVersionString()}" } """.trimIndent() outputs.upToDateWhen { configFile.takeIf { it.exists() }?.readText() == content } doLast { configFile.writeText(content) } }
Теперь в общем модуле у нас есть возможность получать версию приложения. Давайте разберемся, как это реализовать на нативных платформах, начиная с Android. Тут все довольно просто: открываем build.gradle.kts
и обновляем versionCode
и versionName
. Не забываем про корректные импорты!
versionCode = getNumericAppVersion() versionName = getAppVersionString()
Но с IOS будет слегка сложнее. Тут я снова воспользовался возможностью генерацией файлов в gradle-таске
tasks.register("generateXcodeVersionConfig") { val configFile = file(project.rootDir.toString() + "/iosApp/Versions.xcconfig") outputs.file(configFile) val content = """ BUNDLE_VERSION=${getNumericAppVersion()} BUNDLE_SHORT_VERSION_STRING=${getAppVersionString()} """.trimIndent() outputs.upToDateWhen { configFile.takeIf { it.exists() }?.readText() == content } doLast { configFile.writeText(content) } }
Теперь идем в IOS проект, в файлик-конфигурации Info.plist
и меняем CFBundleShortVersionString
и CFBundleVersion
<key>CFBundleShortVersionString</key> <string>$(BUNDLE_SHORT_VERSION_STRING)</string> <key>CFBundleVersion</key> <string>$(BUNDLE_VERSION)</string>
Не забудьте добавить ссылку на Versions.xcconfig
и сам файл Versions.xcconfig
в iosApp.xcodeproj
в секции Configurations как «Based on Configuration File».
Теперь у нас есть две Gradle-таски, которые необходимо выполнять перед сборкой. Но помним, что наша цель — минимизировать вероятность ошибок из-за человеческого фактора, поэтому стоит автоматизировать их выполнение.
Чтобы гарантировать запуск этих тасок перед сборкой на Android, можно добавить следующий код:
tasks.named("preBuild") { dependsOn("generateXcodeVersionConfig") dependsOn("generateVersionConfig") }
И такой для IOS
tasks.matching { it.name.startsWith("compileKotlinIos") }.configureEach { dependsOn("generateXcodeVersionConfig") dependsOn("generateVersionConfig") }
Понимаю, что для Android генерация Xcode-версии не обязательна, но так как этот процесс занимает считаные миллисекунды, я решил оставить его — лишним не будет.
Чтобы упростить и оптимизировать build.gradle.kts
, я вынес всю логику в отдельный файл versions-tasks.gradle.kts
и просто подключаю его в основном build.gradle.kts
. Это делает код чище и упрощает поддержку.
apply(from = "versions-tasks.gradle.kts")
Полное содержимое versions-tasks.gradle.kts
у меня вышло таким
import getAppVersionString import getNumericAppVersion tasks.named("preBuild") { dependsOn("generateXcodeVersionConfig") dependsOn("generateVersionConfig") } tasks.matching { it.name.startsWith("compileKotlinIos") }.configureEach { dependsOn("generateXcodeVersionConfig") dependsOn("generateVersionConfig") } tasks.register("generateXcodeVersionConfig") { val configFile = file(project.rootDir.toString() + "/iosApp/Versions.xcconfig") outputs.file(configFile) val content = """ BUNDLE_VERSION=${getNumericAppVersion()} BUNDLE_SHORT_VERSION_STRING=${getAppVersionString()} """.trimIndent() outputs.upToDateWhen { configFile.takeIf { it.exists() }?.readText() == content } doLast { configFile.writeText(content) } } tasks.register("generateVersionConfig") { val configFile = file(project.rootDir.toString() + "/shared/src/commonMain/kotlin/.../VersionConfig.kt") outputs.file(configFile) val content = """ // Не менять файл, он сгенерирован с помощью таски generateVersionConfig object VersionConfig { const val VERSION_CODE = ${getNumericAppVersion()} const val VERSION_NAME = "${getAppVersionString()}" } """.trimIndent() outputs.upToDateWhen { configFile.takeIf { it.exists() }?.readText() == content } doLast { configFile.writeText(content) } }
Теперь при каждой сборке проекта, независимо от платформы — iOS или Android, у нас всегда будет актуальная версия приложения.
Конечно, у такого подхода есть нюанс: обе платформы будут иметь одинаковую версию, что не совсем корректно с точки зрения их независимого обновления. Однако, поскольку у меня почти весь код общий, я решил этим пренебречь.
Использовать такой метод или нет — выбор за вами. Спасибо за прочтение!
Код примера можно найти здесь: GitHub.
Это моя первая статья, поэтому буду рад любым отзывам и предложениям!
ссылка на оригинал статьи https://habr.com/ru/articles/895362/
Добавить комментарий