Мій досвід створення застосунку на KMP, або Як робити CI/CD для кросплатформи
Привіт, я — Вова Стельмащук, Team Lead Android-застосунку Hily в українській продуктовій IT-компанії appflame. Змушую Android працювати, роблю кілька цікавих сайтів та мрію вивчити JavaScript.
Вільний час присвячую пет-проєктам. Так я можу тестувати нові технології й підходи ще до того, як вони потрапляють у продакшн, та застосовувати інсайти на реальних проєктах, і це — одна з найулюбленіших частин моєї роботи.
Серед таких пет-проєктів — mixdrinks. Як Android-розробник, я не міг оминути мобільну платформу, тому вирішив створити для нього застосунок. Але цього разу захотілося поекспериментувати й зробити щось нестандартне:
- замість того щоб використовувати фреймворки від Google та Apple для застосунків Android та iOS відповідно, я вибрав Kotlin Multiplatform (KMP) і поставив за мету витиснути з цієї технології максимум;
- аби мати єдину кодову базу для Android та iOS-застосунку, я скомпілював Compose UI в iOS. І так, Compose можна запускати не тільки на Android. Він гарно працює і на iOS, а також на desktop. Принцип роботи нагадує Flutter: щоб відображати компоненти, Compose UI використовує нативний доступ до canvas.
Насправді Kotlin Multiplatform + Compose для iOS — це нова технологія. Підтримка iOS зараз у бета-версії (подивитися на план із технології можна тут), і, якщо вірити офіційній дорожній карті, перехід на stable відбудеться тільки цього року. (upd: KMM Compose для iOS вже stable)
Тому так трапилося, що я став одним з адептів і опинився серед перших, хто налаштував CI/CD для KMP-застосунку з нуля.
У цьому блозі я детально розповім, як зробив CI/CD для мобільного кросплатформного застосунку на KMP, а також чому використовувати KMP на реальних продуктах — не найкраща ідея. Мій досвід буде корисним передусім фахівцям, які працюють із KMP, цікавляться GitHub Actions або просто хочуть автоматизувати деплой свого проєкту.
Якщо ж вам зручніше ознайомитися зі стислою версією матеріалу, у мене є окрема стаття, яку опублікували в спільноті Mobile App Development на Medium. Отож вибирайте бажаний формат — і приємного читання!
Що таке CI/CD
Сьогодні світ швидкий як ніколи. Людей дратує, коли доводиться довго очікувати на завантаження сторінок сайту, а в контексті мобільних застосунків миттєве завантаження критичне для утримання користувачів. Тому розробники повинні стабільно пришвидшувати розробку та доставку застосунків. Окрім цього, ми маємо максимально швидко передавати нову функціональність у продакшн-оточення, щоб перевірити, чи все зробили правильно.
Зараз затримки команди розробки через ручні дії під час кожного релізу виглядають, м’яко кажучи, нелогічно. Кожна неавтоматизована частина процесу розробки або доставки — це гальма для вашого бізнесу та додаткові ризики через ручну роботу. Виливати білд у маркет мануально — монотонне завдання. А окрім того, можна легко натиснути не ту кнопку, запустити не ту Gradle job чи пропустити один крок — і щось піде не за планом.
Тому автоматизація релізного процесу — обов’язкова для кожного проєкту, незалежно від вашого релізного циклу. Бо якщо ви релізитеся щотижня, то завдяки автоматизації просто заощаджуєте час, а якщо раз на квартал — позбуваєтеся клопоту тримати всі нюанси в голові. Адже що рідше відбуваються релізи, то вищий ризик щось забути чи зробити не так.
На допомогу приходить CI/CD (Continuous Integration and Continuous Delivery) — неперервна інтеграція та доставка. Це набір інструментів і практик, які дозволяють швидко розробляти, тестувати та доставляти програмне забезпечення до користувачів. CI/CD давно вже став стандартом у розробці. У більшості проєктів він є за замовчуванням: хтось колись написав yml для GitHub Actions, GitLab Pipelines абощо — і відтоді всі цим користуються.
Але ці інструменти не з’явилися самі по собі — їх також писали програмісти, які були першими адептами технології.
Як налаштувати CI/CD
1. Визначити вимоги до автоматизації
Перед тим як налаштовувати CI/CD, треба визначити, що саме ми хочемо автоматизувати і які критерії характеризуватимуть виконану роботу. Тому ще на початку роботи із CI/CD я виділив ключові аспекти мого пет-проєкту:
- автоматична збірка Android-застосунку — як за графіком, так і за запитом (наприклад, коли потрібно вручну запустити збірку);
- автоматична збірка iOS-застосунку за тими ж принципами, що й Android;
- публікація Android-застосунку в Google Play — щоб нові версії могли виходити без зайвих мануальних дій;
- публікація iOS-застосунку в App Store.
2. Вибрати сервіс для CI/CD
Зараз є багато різних сервісів та інструментів, де можна зробити CI/CD для проєкту. Усі свої open source проєкти я зберігаю на GitHub, тому для CI/CD використав GitHub Action — відповідно, мені не треба інтегрувати сторонній сервіс. Також GitHub Action дає можливість створити рішення, яке відповідає моїм вимогам до автоматизації.
Якщо робити CI/CD для кросплатформного застосунку, то треба перевіряти, чи може сервіс надати всі потрібні операційні системи для запуску наших автоматизацій. У моєму випадку для автоматизації потрібні дві операційні системи — macOS для збірки iOS і Ubuntu для збірки Android. Файл yml, у якому я описав усю конфігурацію CI/CD для цього проєкту, можна переглянути тут.
3. Врахувати специфіку версіонування мобільних застосунків
У Google Play та App Store кожен застосунок для Android та iOS повинен мати два обовʼязкові поля, які відповідають за версію застосунку та визначають, як відбуватиметься їхнє оновлення. Ці поля потрібно вказувати під час розробки:
- version code, який має збільшуватися з кожним новим релізом. Якщо спробувати завантажити в маркетплейс версію з меншим або тим самим номером, що вже є, — він просто не пропустить нову версію;
- version name — версія, яку користувач бачить у магазині застосунків та в налаштуваннях свого пристрою.
Якщо ви, як і я, провели роки в розробці мобільних застосунків, то використання такого підходу виглядає логічним, адже дає можливість відрізняти релізи один від одного та відстежувати зміни в релізах.
Однак насправді версіонування ускладнює автоматизацію CI/CD, бо потребує додаткових дій для правильної роботи. Наприклад, необхідно окремо генерувати version code на основі version name за формулою: major * 10000 + minor * 100 + patch, навіть якщо попередній build не був доступний користувачам.
Будьмо чесними, кінцевим юзерам байдуже на номер версії вашого застосунку, і його не потрібно версіонувати, як enterprise-рішення, котре IBM постачає для JP Morgan Chase (назви компаній вигадані, збіги з реальністю випадкові). Якщо не вірите, спробуйте без підказки назвати версію застосунку monobank або «Нової пошти» на вашому телефоні. Правильно, перше, що ви згадаєте, — це їхній дизайн чи нова функціональність, а не номер версії.
Якби цих вимог від Google Play та App Store не було, ми могли б використовувати простіші підходи. Наприклад, для своїх пет-проєктів на web я створюю версію на основі commit hash, з якого зібрав docker image — це швидко та просто (бо немає обмежень Google Play та App Store). Відповідний yml-файл для GitHub Action можна переглянути тут.
Але цей підхід не підходить для мобільної розробки, адже маркетплейси не дозволяють використовувати будь-який довільний рядок замість version code. А commit hash — це рядок, а не число, що зростає. Через ці обмеження доводиться вигадувати спеціальні механізми для правильного версіонування.
На жаль, на політику Google чи Apple ми вплинути не можемо, а писати версії руками теж не дуже хочеться, тому я автоматизував процес. Використовую версії виду Semver і написав job, який бере крайній tag у репозиторії та автоматично збільшує йому patch-частину. Крім того, цей job створює version code і version name та зберігає їх у job output, щоб можна було використовувати на наступних етапах CI/CD. У коді це виглядає ось так:
prepare_deploy: runs-on: Ubuntu-22.04 steps: — uses: actions/checkout@v3 with: fetch-depth: 0 — name: Set env id: last_tag run: echo "LAST_TAG=$(git tag --sort=committerdate | tail -1)" >> $GitHub_OUTPUT — uses: actions-ecosystem/action-bump-semver@v1 id: bump-semver with: current_version: ${{ steps.last_tag.outputs.LAST_TAG }} level: patch — run: | git config user.name GitHub-actions git config user.email [email protected] git tag ${{ steps.bump-semver.outputs.new_version }} git push --tags — name: "Set output" id: set-output run: | echo "mix_drinks_mobile_version_name=${{ steps.bump-semver.outputs.new_version }}" >> $GitHub_OUTPUT IFS='.' read -r major minor patch <<< "${{ steps.bump-semver.outputs.new_version }}" mix_drinks_mobile_version_code=$((major * 10000 + minor * 100 + patch)) echo "mix_drinks_mobile_version_code=${mix_drinks_mobile_version_code}" >> $GitHub_OUTPUT — name: "Print output" run: | echo -e "Version name is: \n ${{ steps.set-output.outputs.mix_drinks_mobile_version_name }}" echo -e "Version code is: \n ${{ steps.set-output.outputs.mix_drinks_mobile_version_code }}" outputs: output_write_mix_drinks_mobile_version_name: ${{ steps.set-output.outputs.mix_drinks_mobile_version_name }} output_write_mix_drinks_mobile_version_code: ${{ steps.set-output.outputs.mix_drinks_mobile_version_code }}
4. Зібрати Android-застосунок
Щоб зібрати та опублікувати Android-застосунок, нам потрібно виконати Gradle job — androidApp:bundleRelease.
Для цього я використав офіційний GitHub Action від Gradle — gradle-build-action. Перед виконанням додаю в змінні оточення versionCode та versionName з попереднього кроку. Також я зробив відповідні зміни в самих Gradle script, щоб усе працювало автоматично.
versionCode = System.getenv("MIXDRINKS_MOBILE_APP_VERSION_CODE")?.toIntOrNull()?: 1 versionName = System.getenv("MIXDRINKS_MOBILE_APP_VERSION_NAME")?: "0.0.1"
Для автоматичного завантаження в Google Play я використав GitHub Action — r0adkll/upload-google-play, а також зробив сервісний акаунт, щоб CI/CD мав права завантажувати застосунок від мого імені. Для цього:
- перейдіть за посиланням та увімкніть Google Play Android Developer API для свого проєкту;
- виконайте офіційні інструкції Google зі створення service account;
- отриманий JSON-файл із ключами зберігайте в GitHub Action Secrets, щоб CI/CD міг використовувати його для завантаження нових версій застосунку.
5. Зібрати iOS-застосунок
Тут усе трішки складніше — xcodebuild не дає технічної можливості проставити значення CFBundleShortVersionString та CFBundleVersion через змінні оточення. Тому я зробив хак із модифікацією файлу Info.plist* як окремий крок на CI/CD. Перед виконанням, так само як для Android, додаю в змінні оточення version code та version name з попереднього кроку. У коді виглядає ось так:
run: | /usr/libexec/Plistbuddy -c "Set CFBundleVersion $MIXDRINKS_MOBILE_APP_VERSION_CODE" iOSApp/iOSApp/Info.plist /usr/libexec/Plistbuddy -c "Set CFBundleShortVersionString $MIXDRINKS_MOBILE_APP_VERSION_NAME" iOSApp/iOSApp/Info.plist
*Info.plist — файл в iOS-проєкті, який виконує роль маніфесту, є аналогом AndroidManifest.xml для OS Android або manifest.json для WebExtension.
З автоматизацією завантаження застосунку в App Store все також складніше, ніж із Google Play. Apple не дає створити сервісний акаунт, тому немає можливості зробити авторизаційний токен, який буде жити роками. Але можна зробити звичайний токен на основі Team Private Key. Для цього:
- зайдіть в App Store Connect і створіть private key;
- збережіть його зміст у GitHub Action Secret;
- використовуйте action, yuki0n0/action-appstoreconnect-token для генерації токена, що дозволить взаємодіяти з Rest API App Store Connect.
name: Get appstore token id: asc uses: yuki0n0/[email protected] with: issuer id: ${{ secrets.MIXDRINKS_IOS_APPSTORE_ISSUER_ID }} key id: ${{ secrets.MIXDRINKS_IOS_APPSTORE_ADMIN_API_KEY_ID }} key: ${{ secrets.MIXDRINKS_IOS_APPSTORE_ADMIN_API_PRIVATE_KEY_RAW }} - name: Create appstore version env: APP_VERSION: ${{ env.MIXDRINKS_MOBILE_APP_VERSION_NAME }} run: | JSON=`curl -sS -H "Authorization:Bearer ${{ steps.asc.outputs.token }}" https://api.appstoreconnect.apple.com/v1/appStoreVersions -d '{"data": {"type": "appStoreVersions","attributes": {"platform": "IOS","versionString": "${APP_VERSION}","copyright": "(c) 2020 MixDrinks","releaseType": "MANUAL","usesIdfa": false},"relationships": {"app": {"data": {"type": "apps","id": "6447103081"}}}}}' -H 'Content-Type: application/json'`
One more thing
GitHub Action повністю безкоштовний для open source проєктів. Для приватних проєктів є ліміт — 2000 хвилин на місяць. Це справжні 2000 хвилин тільки для job, які запускаються на Ubuntu. Для macOS кожна хвилина роботи буде рахуватися як 10 хвилин. Тобто якщо ваш macOS runner працює 1 хвилину, з вас буде знято 10 хвилин. Для Windows цей коефіцієнт 2, тобто за одну хвилину робити CI буде знято 2 хвилини з вашого балансу. Це зрозуміло, адже хостинг macOS або Windows у дата-центрах — набагато дорожче і складніше, ніж Ubuntu. Тому будьте уважні.
GitHub не дуже чітко повідомляє про ці правила — на мою думку, вони мають комунікувати про це прозоріше для користувачів. Детальну інформацію про minute multipliers можна знайти в офіційній документації.
Чому не раджу використовувати KMP на реальних продуктах
Я постарався підсвітити всі цікаві моменти створення CI/CD для проєкту на KMP. Якщо вам потрібна подібна автоматизація в проєкті, весь код доступний на GitHub — просто змініть назви змінних та секрети й використовуйте.
І ще одна порада наостанок: робіть CI/CD. Заощаджений час можна провести набагато цікавіше, ніж натискати кнопки в Google Play Console та App Store Connect. Але пам’ятайте, що KMP — це технологія, яку активно просувають JetBrains та Google. У перших немає мобільних проєктів, у других є звичка закривати ініціативи. Тому добре подумайте, перш ніж брати її в продакшн для реального продукту. Мій висновок із цього експерименту: я б не брав.
У Hily ми використовуємо нативні технології: Android SDK, Kotlin Coroutines, Jetpack Compose — всі ці інструменти напряму підтримуються Google та активно розвиваються.
У сучасному світі саме поняття «нативний» може бути розмите. Дехто вважає технологію нативною, якщо вона використовує прямий доступ до пам’яті, інші — якщо вона від тієї ж компанії, що робить операційну систему.
Я ж вважаю технологію нативною, якщо вона дозволяє повністю підтримувати фічі операційної системи з мінімальними обмеженнями та ризиками. Наприклад, ти не переживаєш, що буде з твоїм фреймворком після виходу нової версії на Android.
Окрім того, ці технології мають сильну підтримку — як від компаній, так і від спільноти програмістів. Тому ви також отримуєте перевагу у вигляді багатої інфраструктури: середовища розробки, готові рішення для CI/CD, інструменти для debug і profile та різних SAAS-рішень, що спрощують роботу.
Дякую за увагу! Якщо залишилися запитання, чекаю в коментарях — що знатиму, відповім. Також я відкритий до обговорення статті в LinkedIn та на інших платформах.
Більше власними думками про програмування регулярно ділюся на ютубі: тут — у довгому форматі, а тут — у короткому.
9 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів