Міграція застосунків на Kotlin Multiplatform: покроковий гайд

💡 Усі статті, обговорення, новини про Mobile — в одному місці. Приєднуйтесь до Mobile спільноти!

Вітаю, спільното, я Анатолій Берчанов, Android Tech Lead у N-iX! У цій статті розповім про свій досвід та думки щодо міграції Android-застосунків на KMP.

Мабуть, кожен Android-розробник щось вже чув про Koltin Multiplatform (KMP). Це технологія, яка дозволяє запускати Kotlin-код і на Android, і на iOS, і на багатьох інших платформах. А від недавнього часу дає змогу навіть робити спільний графічний інтерфейс.

Однак що робити, якщо вже існуючий Android-застосунок потрібно перевести на KMP? Чи це можливо? Чи це зручно? Чи це складно, довго? Чи доведеться для цього щось переробляти? Які є обмеження?

У процесі свого дослідження я прагнув знайти відповіді на ці запитання.

Типовий Android-проєкт

Для початку розповім про структуру проєкту, в рамках якого проводилось дослідження. Така структура є типовою для абсолютної більшості Android-проєктів.

Проєкт побудовано на основі класичної MVVM-архітектури. У data-шарі репозиторії розбиті на DataStore. ViewModel напряму використовує репозиторії, від використання UseCase вирішили відмовитись. UI побудований на класичному XML, без Compose.

Офлайн DOU Mobile Meetup, Київ 👇

Використані бібліотеки:

  • Koin — бібліотека для DI.
  • Coroutines — для виконання асинхронної та багатопоточної роботи.
  • AndroidX DataStore — для локального сховища даних.
  • Retrofit — для мережевих запитів.
  • Gson — для парсингу json.

Головною задачею дослідження було перевірити можливість та ресурсоємкість перенесення логіки у KMP для подальшого використання на iOS. Графічний інтерфейс вирішили реалізовувати нативно на iOS.

Налаштовуємо середовище

Для створення KMP-проєктів прийнято використовувати Kotlin Multiplatform Wizard.

Отже я створив окремий проєкт і скопіювався із папки «iosApp» (iOS-застосунок, до якого підключена KMP-бібліотека) та «shared» (модуль із KMP-кодом) у Android-проєкт.

Також варто встановити KMP Plugin у Android Studio. Фактично його головна фіча — можливість запускати iOS-застосунок. Вона доступна тільки на MacOS і для цього повинен бути встановлений XCode.

Потрібно мати на увазі, що версії Kotlin, Gradle, AGP, XCode повинні бути сумісними між собою.

JetBrains працює над окремою IDE для KMP-розробки — Fleet. Хоч вона ще у preview (тобто навіть не alpha), однак я раджу її встановити. Інколи доводиться миритись із її нестабільністю, але в ній доступні такі фічі:

  • підсвітка та підказки не тільки Kotlin, а й Swift;
  • зручна навігація між Kotlin та Swift кодом;
  • брейкпоінти дебагеру можна розставляти і в Swift і в Kotlin, і вони будуть відпрацьовувати в обох мовах під час однієї дебаг-сесій;
  • зручний UI (для тих, кому не подобається XCode).

Як виглядає Fleet:

Переносимо DI

Koin — це DI, реалізований на чистому Kotlin, то ця бібліотека сумісна із KMP. Тож достатньо обʼявити ту ж саму залежність у commonMain.dependencies.

Оскільки за своєю структурою shared-модуль вміщує і загальний (common) код, і специфічний для кожної платформи, можна обʼявити і специфічну Android-залежність Koin:
shared/build.gradle.kts

androidMain.dependencies {
            implementation("io.insert-koin:koin-android:3.4.3")
 }

Що дозволить використовувати інʼєкцію контексту в Android-частині:

shared/src/androidMain/kotlin

import org.koin.android.ext.koin.androidContext

internal actual fun getDataStoreModule(): Module = module {
    single {
        createDataStore(androidContext())
    }
}

Окрім Koin є й інші DI бібліотеки для KMP. Однак серед них ви не знайдете Dagger та Hilt. Ось стаття про це.

Переносимо локальне сховище даних

Деякі із Jetpack-бібліотек вже мають сумісність із KMP. Серед них є і бібліотека datastore, яка використовується у нашому Android-застосунку.

Клас AuthDataStoreImp,l який був у Android, виглядав так:

class AuthDataStoreImpl(
    private val context: Application
) : AuthDataStore {

    override suspend fun getBearerToken(): String? {
        return context.dataStore.data.map { preferences ->
            preferences[stringPreferencesKey(name = BEARER_TOKEN_KEY)]
        }.firstOrNull()
    }

    override suspend fun setBearerToken(bearerToken: String) {
        context.dataStore.edit { preferences ->
            preferences[stringPreferencesKey(name = BEARER_TOKEN_KEY)] = bearerToken
        }
    }

У shared-модулі він тепер виглядає так:

commonMain

internal class AuthDataStoreImpl(
    private val dataStore: DataStore<Preferences>,
) : AuthDataStore {

    override suspend fun getBearerToken(): String? {
        return dataStore.data.map { preferences ->
            preferences[stringPreferencesKey(name = BEARER_TOKEN_KEY)]
        }.firstOrNull()
    }

    override suspend fun setBearerToken(bearerToken: String) {
        dataStore.edit { preferences ->
            preferences[stringPreferencesKey(name = BEARER_TOKEN_KEY)] = bearerToken
        }
    }

Основне, що змінилось: у конструктор тепер приймається DataStore, а не Context. Адже це тепер не Android, а KMP-модуль і Android SDK у ньому немає.

Однак створення обʼєкту AuthDataStoreImpl буде відрізнятись для Android та iOS. Для початку розглянему спільну логіку створення:

commonMain

private val pathToDataStore = mutableMapOf<String, DataStore<Preferences>>()

@OptIn(InternalCoroutinesApi::class)
private val lock = SynchronizedObject()

@OptIn(InternalCoroutinesApi::class)
fun createDataStore(producePath: () -> String): DataStore<Preferences> =
    synchronized(lock) {
        val path = producePath()
        val dataStore = pathToDataStore[path]
        if (dataStore != null) {
            dataStore
        } else {
            PreferenceDataStoreFactory.createWithPath(produceFile = { path.toPath() })
                .also { pathToDataStore[path] = it }
        }
    }

internal expect fun getDataStoreModule(): Module

Функція createDataStore приймає лямбду producePath, мета якої надати шлях до файлу. Використання lock та pathToDataStore забезпечує, що для кожного шляху існує лише один обʼєкт DataStore. Це вимога бібліотеки для коректної роботи.

Зверніть увагу на функцію getDataStoreModule, вона обʼявлена як expected-функція, що повертає Koin-модуль. Отже ми зобовʼязані зробити actual-реалізації для кожної платформи.

Android-реалізація виглядає таким чином:

androidMain

internal actual fun getDataStoreModule(): Module = module {
    single {
        createDataStore(androidContext())
    }
}

private fun createDataStore(context: Context): DataStore<Preferences> = createDataStore(
    producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath }
)

Тут використовується функція createDataStore із папки commonMain, у яку передається шлях, отриманий із Context. Оскільки це Android платформена частина, то Android SDK тут доступний.

iOS-реалізація:

iosMain

internal actual fun getDataStoreModule(): Module = module {
    single {
        createDataStore()
    }
}

@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
fun createDataStore(): DataStore<Preferences> = createDataStore(
    producePath = {
        val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = null,
        )
        requireNotNull(documentDirectory).path + "/$dataStoreFileName"
    }
)

У iOS-реалізації доступний iOS SDK. Наприклад, NSFileManager у вигляді Kotlin-враперів. Тож можна писати звичний Kotlin код, однак із використанням iOS SDK.

Підсумуємо, що виявилось необхідним для міграції локального сховища даних у KMP shared-модуль:

  • Заміна Context на DataStore у конструкторі. Вся інша логіка запису та зчитування даних лишилась без змін.
  • Реалізація досить хитрої логіки створення обʼєктів. Однак це необхідно зробити лише один раз на початку. У подальшому достатньо додавати залежності до існуючого Koin-модуля за шаблоном.

Переносимо мережеву взаємодію

Якщо із локальним сховищем нам пощастило (DataStore повністю сумісний із KMP) то із мережевою взаємодією все не так просто. Найпопулярніші бібліотеки для Android http запитів — Retrofit + OkHttp. Вони не мають сумісності із KMP. Навпаки, для KMP основою http-бібліотекою вважається Ktor.

Однак від переписування повністю всієї network-логіки нас врятує Ktorfit:

Із документації: «Ktorfit is a HTTP client/Kotlin Symbol Processor for Kotlin Multiplatform (Js, Jvm, Android, iOS, Linux) using KSP and Ktor clients inspired by Retrofit».

Інтерфейс SandboxDashboard мав такий вигляд у Android:

import retrofit2.http.*

interface SandboxDashboard {

    @POST("authenticate-user/")
    suspend fun authenticateStep1(
        @Header("Authorization") bearerToken: String,
        @Header("App-Id") appId: String,
        @Header("Device-Id") deviceId: String,
        @Body authModel: AuthStep1Request
    ): AuthStep1Response
}

Щоб використати його у KMP, переносимо у shared-модуль, та замінюємо тільки import із Retrofit на Ktorfit. Уся семантика, тобто тіло інтерфейсу, лишається без змін.

import de.jensklingenberg.ktorfit.http.*

interface SandboxDashboard {

    @POST("authenticate-user/")
    suspend fun authenticateStep1(
        @Header("Authorization") bearerToken: String,
        @Header("App-Id") appId: String,
        @Header("Device-Id") deviceId: String,
        @Body authModel: AuthStep1Request
    ): AuthStep1Response
}

Отже пренесення інтерфейсів не вимагає суттєвих змін. Однак отримання обʼєктів буде відрізнятись.

Таким чином отримувались обʼєкти інтерфейсів у Android:

private fun <T> getApiClient(baseUrl: String, api: Class<T>): T {
    val httpClient = getOkHttpClient()
    return Retrofit.Builder()
        .client(httpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(baseUrl)
        .build()
        .create(api)
}

private fun getOkHttpClient(): OkHttpClient {
    val interceptor = HttpLoggingInterceptor()
    val httpClient = OkHttpClient.Builder()
    interceptor.level = HttpLoggingInterceptor.Level.BODY
    if (BuildConfig.DEBUG) {
        httpClient.addInterceptor(interceptor)
    }
    return httpClient.build()
}

Використання у DI:

single {
        getApiClient(
            "YOUR_URL",
            SandboxDashboard::class.java,
        )
}

Однак із KMP ми використовуємо Ktor замість OkHttp:

internal inline fun <T> getApiClient(baseUrl: String, createApi: Ktorfit.() -> T): T {
    val ktorfit = ktorfit {
        baseUrl(baseUrl)
        httpClient(HttpClient {
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
            install(DefaultRequest) {
                contentType(ContentType.Application.Json)
            }
            install(Logging) {
                level = LogLevel.ALL
            }
        })
    }
    return ktorfit.createApi()
}

Використання у DI виглядатиме досить схоже:

single {
        getApiClient(
           “YOUR_URL”,
            Ktorfit::createSandboxDashboard,
        )
}

Однак міграція мережевої логіки ще не завершена. Лишається json-парсинг. У Android використовувалась бібліотека Gson, яка не підтримує KMP. Тож доведеться мігрувати моделі даних на Kotlinx.Serialization.

Android

import com.google.gson.annotations.*

data class AuthStep1Request(
    @SerializedName("email")
    val email: String,
    @SerializedName("password")
    val password: String
)

KMP

import kotlinx.serialization.*

@Serializable
data class AuthStep1Request(
    @SerialName("email")
    val email: String,
    @SerialName("password")
    val password: String
)

Видно, що суть міграції на Kotlinx.Serialization полягає тільки у заміні анотацій. Хоч це і не складний процес, однак якщо у проєкті моделей даних багато, це може зайняти певний час. Швидко застосувати такі шаблонні зміни до проекту допомагає функціональність Android Studio — replace all (Command + Shift + R).

Оскільки репозиторії це чисті Kotlin-класи, без залежностей на сторонні бібліотеки їх можна просто скопіювати у shared-модуль.

На цьому міграцію мережевих викликів на KMP можна вважати завершеною, підсумуємо:

  • Ktorfit дозволяє швидко мігрувати Retrofit-інтерфейси.
  • Метод утворення обʼєктів інтерфейсів необхідно переписати на Ktor.
  • Необхідно мігрувати всі моделі даних на Kotlinx.Serializable.

Перевіримо працездатність на iOS

shared — це звичайний gradle-модуль, тому його використання у Android-застосунку не потребує детального пояснення. Тому сфокусуємось на тому, як використати мігровані сутності із data-шару у iOS-застосунку.

Зробимо DI доступним у iOS:

shared/src/iosMain/kotlin/DiHelper.kt

class DiComponent : KoinComponent {
    fun authRepository() = getKoin().get<AuthRepository>()
}

fun setupDi() {
    startKoin {
        modules(sharedModule)
    }
} 

У наведеному вище фрагменті коду обʼявлений клас DiComponent, який є Koin-компонентом. Що дає змогу викликати всередині нього функцію getKoin, за допомогою якої можна отримувати залежності. Щоб ініціалізувати граф залежностей Koin, обʼявлена окрема функція — setupKoin.

Оскільки це публічний Kotlin-код у папці iosMain, його можна викликати із Swift-коду iOS-застосунку.

Ось як виглядає виклик KMP коду у SwiftUI:

iosApp/iosApp/ContentView.swift

struct ContentView: View {

    let component: DiComponent

    init() {
        DiHelperKt.setupDi()
        component = DiComponent()
    }

    var body: some View {
        VStack(spacing: 20) {
            Button (action: {
                Task {
                    let result = try? await component.authRepository()
                        .authenticateStep1(email: "MAIL", password: "PASS")
                    print("message: " + (result?.message ?? "nil"))
                }
            }) {
                Text("Start Async Action")
            }

У функції init написана ініцілазація Koin та утворення DiComponent. А у action при натисканні кнопки відбувається отримання AuthRepository та виклик authenticateStep1. Варто зазначити, що функція authenticateStep1 обʼявлена у Kotlin коді як suspend. Тому її необхідно викликати асинхронно. Для цього можна використати async / await у Swift.

Запускаємо iOS-застосунок, натискаємо кнопку і бачимо логи мережевого запиту. Логіку з шару даних вдалося мігрувати успішно.

Build types, flavors, build config у KMP

Зазвичай мережева логіка будується таким чином, що релізна версія застосунку використовує продакшн-оточення. У той час як дебажна версія — інше оточення. Інколи ще буває і третє оточення для тестових версій.

Конфігурації оточення не прийнято тримати у Kotlin-коді, щоб їх не можна було дістати методами реверс-інженірінгу. Замість цього у Android-розробці використовується buildConfig. Однак у KMP такої можливості немає із коробки. Щось подібне buildConfig можна отримати, якщо використати сторонній плагін BuildKonfig.

Цей плагін дозволить налаштувати різні значення констант для окремих конфігурацій. Тож із певними костилями задачу налаштування різних конфігурацій у KMP shared-модулі виконати можна.

Однак якщо у вашому застосунку використовуються різні ресурси або різний код для окремих build types чи flavors, міграція на KMP може бути серйозним челенджем. У такому випадку доведеться розбивати специфічний код або ресурси на окремі модулі і підключати їх у залежності від конфігурації. Ось обговорення цієї проблеми.

Приклад налаштування BuildKonfig:

shared/build.gradle.kts

buildkonfig {
    packageName = "com.some.package"

    val DASHBOARD_URL_SANDBOX = "your_sandbox_url"

    val DASHBOARD_URL_PROD = "your_prod_url"

    defaultConfigs {
        buildConfigField(STRING, "DASHBOARD_URL", DASHBOARD_URL_SANDBOX)
    }
    defaultConfigs("debug") {
        buildConfigField(STRING, "SHARED_TYPE", "debug")
    }
    defaultConfigs("qa") {
        buildConfigField(STRING, "SHARED_TYPE", "qa")
    }
    defaultConfigs("release") {
        buildConfigField(STRING, "SHARED_TYPE", "release")
        buildConfigField(STRING, "DASHBOARD_URL", DASHBOARD_URL_PROD)
    }
}

Мігруємо ViewModel у KMP

Коли маємо шар роботи із даними у KMP, наступним кроком буде мігрувати бізнес та презентаційну логіку. У цьому проєкті відсутній окремий шар для бізнеc-логіки (UseCase-s, Interactor-s). Однак сутності бізнес-логіки прийнято робити незалежними від бібліотек, це мають бути чисті Kotlin-класи. Тож їх міграція не повинна скласти проблем і має обмежитись копіпастингом із одного модуля у інший.

Однак із презентаційною логікою у ViewModel трохи інша ситуація. Базовий клас androidx.lifecycle.ViewModel є сторонньою залежністю. Але, о чудо, пакет androidx.lifecycle має підтримку KMP:

Обʼявляємо залежність у shared-модулі:

sourceSets {
        commonMain.dependencies {
            // ...
            implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.4")
        }
        androidMain.dependencies {
            implementation("io.insert-koin:koin-android:3.4.3")
        }
}

І ViewModel можна сміливо копіювати у shared-модуль. Звісно, її залежності (репозиторії чи UseCase-s) вже мають бути мігровані.

Логіка створення обʼєктів ViewModel буде відрізнятись на різних платформах. Адже у Android є lifecycle, до якого потрібно підвʼязатись. Тому створимо абстрактну expected-функцію getNativeModule.

shared/src/commonMain/kotlin/di/Modules.kt

internal expect fun getNativeModule(): Module

val sharedModule = module {
    includes(getNativeModule())

Android-реалізація буде використовувати koin-android:
shared/src/androidMain/kotlin/di/Modules.android.kt

internal actual fun getNativeModule(): Module = module {
    viewModel { LoginViewModel(get(), get()) }
}

А у iOS-реалізації буде звичайне створення обʼєкту:
shared/src/iosMain/kotlin/di/Modules.kt

internal actual fun getNativeModule(): Module = module {
    factory { LoginViewModel(get(), get()) }
}

Оскільки у iOS немає lifecycle, до якого можна було б підвʼязати ViewModel, то скоупінг та очистку ресурсів необхідно реалізовувати самостійно. Ось приклад, як це можна реалізувати завдяки Koin-скоупам.

Для отримання результатів із ViewModel на UI використовуються Flow:

class LoginViewModel(
    private val authRepository: AuthRepository,
    private val exceptionHandler: ExceptionHandler
) : ViewModel(), ExceptionProvider by exceptionHandler {

    private val _onResult: MutableSharedFlow<Result> = MutableSharedFlow()
    val onResult: SharedFlow<Result> = _onResult

Якщо ми отримаємо onResult у Swift-коді, його тип не буде містити Result — тип, що був у generic:

let result: Kotlinx_coroutines_coreSharedFlow = loginViewModel. onResult

Це пояснюється тим, що для інтеропу між KMP та iOS код на Kotlin конвертується у ObjectiveC. А оскільки усі сучасні iOS-застосунки використовують Swift, доводиться викликати конвертований ObjectiveC-код. Конвертація Kotlin у ObjectiveC, мʼяко кажучи, не ідеальна, бо синтаксичні можливості Kotlin значно ширші за ObjectiveC. Відустнісь generic-типів це лише одна із проблем, на яку можна наштовхнутись. Через це існують цілі серії статей про те, як писати KMP Kotlin код так, щоб мати менше клопотів із інтеропом на iOS (стаття).

Команда JetBrains, що відповідає за розробку KMP, відчуває біль розробників через це, тому планує у 2024 році реалізувати конвертацію Kotlin у Swift.

Однак на цей момент існують рішення від сторонніх розробників, що допомагають зробити використання Kotlin-коду із Swift приємнішим. Одне із таких — плагін Skie.

Підключаємо його:
shared/build.gradle.kts

plugins {
// …
id("co.touchlab.skie") version "0.8.4"
}

Чистимо кеші, перезбираємо проєкт. І тепер у Swift-коді тип onResult із generic. Магія :)

let result: SkieSwiftSharedFlow<LoginViewModel.Result> = loginViewModel.onResult

Завдяки Skie підписатись на Flow можемо, використовуючи конструкцію await for:

for await state in loginViewModel.onResult {
    isLoading = state is LoginViewModel.ResultLoading
    if let resultAuth = state as? LoginViewModel. ResultAuth {
        // process resultAuth
    }
}

Отже, з міграції ViewModel у shared-модуль можна зробити такі висновки:

  • Мігрувати код ViewModel можна швидко завдяки сумісності jetpack бібліотек із KMP.
  • Через відсутність звичного для Android-розробників lifecycle у iOS доведеться чистити ViewModel самостійно. Краще порадитись із iOS-розробниками, як це робити правильно.
  • Проблемна сумісність Kotlin-коду із Swift може викликати неприємні сюрпризи.

Про обробку помилок

Якщо у вашому KMP-коді випаде необроблена помилка у iOS-застосунку, зрозуміти де і чому вона впала може бути досить складно. Особливо якщо у коді не розставлені логи.

Приклад того, як виглядає помилка:

Навіть якщо у Swift-коді поставити try / catch навколо викликів KMP-функцій, це може не врятувати від крешу.

Тому окрему увагу доведеться звернути на стабільний error handling у KMP-коді, що може викликати можливий рефакторинг.

Підсумки

Мігрувавши роботу із даними та логіку із вже реалізованого Android-застосунку у KMP, можемо виділити такі переваги цього процесу:

Гнучкість KMP. На відміну від найпопулярніших кросплатформених фреймворків Flutter та ReactNative, KMP дає гнучкість. Ви можете обмежити зону, яку збираєтесь зробити спільною, і для цього не доведеться вчити нову мову чи наймати додаткових людей у команду. Можете зробити нативний графічний інтерфейс, а всю логіку лишити у KMP. А можете перенести тільки частину логіки, якщо немає часу на рефакторинг.

Сумісність Jetpack-бібліотек та підтримка спільноти. KMP став стабільним не так давно, але вже можна бачити розвиток тулінгу та екосистеми. Це вже дозволяє швидко знайти рішення для багатьох проблем. Далі — більше.

Однак і не без недоліків.

Не всі бібліотеки сумісні із KMP. Вам може пощастити, якщо альтернативні бібліотеки побудовані таким чином, щоб спростити міграцію, як Ktorfit. А може і не пощастити.

Різні IDE. Оскільки Fleet ще не стабільний, щоб повністю обмежитись тільки ним, мені доводилось часто стрибати між 3 IDE: Android Studio, XCode, Fleet. Це відволікає.

Часті підводні камені. Відсутність підтримки розділення логіки для flavors та build types, нюанси сумісносності Kotlin та Swift, потенційні проблеми із error handling, і хто зна ще що може вилізти. Це все проблеми, які мають рішення, однак це займає час. Тому зробити прогнозовану оцінку того, скільки часу займе міграція на KMP, майже неможливо.

Наштовхувася на статті, де у команд пішло на це втричі більше часу, ніж було заплановано. Для проєктів із стислими термінами це ризиковано.

Вплив KMP на Android-розробку очевидний. На мою думку, можна робити Android-застосунки і вже старатись використовувати бібліотеки та архітектуру, сумісні із KMP. За потреби можна перевикористати частину застосунку, мінімізуючи додаткові зусилля. Для Android-застосунків це не створює значного оверхеду. Навпаки, KMP дозволяє покращити якість архітектури через чіткіше розділення логіки на шари.

Конкретні кроки, які полегшать міграцію, якщо це зробити на старті нового проєкту:

  • Використовувати Ktor (Ktorfit), забути про OkHttp (Retrofit).
  • Використовувати Koin або інші KMP DI бібліотеки.
  • Використовувати сумісні з KMP бібліотеки (Jetpack, для сховища даних, для логування, для парсингу json, і так далі).
  • Робити багатомодульну архітектуру. Модулі з логікою чи роботою із даними НЕ робити як android library. Замість цього створювати їх як java-модулі чи одразу KMP, навіть якщо поки це не потрібно.
  • Забути за groovie, тільки kts для gradle.
  • Забути за розділення коду чи ресурсів за flavors чи build types. Замість цього робити окремі модулі, та підключати їх відповідно до конфігурації у properties або env vars.

Якщо проєкт вже написаний, звісно, радикальні рішення будуть зайвими. Ліпше буде притримуватись вже встановлених кращих практик. І натякати усім, що ось цю фічу можна було б зробити на KMP 😉

Якщо бажаєте ознайомитись із прикладом міграції Android на KMP, то можете зазирнути у репозиторій:
github.com/tberchanov/AndroidToKMP

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Напишіть ще, будь ласка, статтю як позбутися кмп у іос додатку

Зустрічався із такою ситуацією.

ІОС розробник переписав все на Swift, через те що попередники прийняли необгрунтоване рішення використовувати КМП.
Тож завжди треба стратегічно думати, приймаючи рішення які фреймворки будуть використовуватись

Я правильно розумію висновок: для існуючих проектів — краще не ризикувати і писати нативно все, а для абсолютно нових Андроїд додатків краще починати одразу з Compose Multiplatform www.jetbrains.com/compose-multiplatform ?

У існуючих проектах можна пробувати частини логіки переводити на KMP.
Поступово і без радикальних змін.
Для цього необхідно провести аналіз проекту: що вийде легко перевести на KMP, що доведеться переписати, які ризики, чи не буде це впливати на бізнес цілі, і т.д.

Щодо Compose Multiplatform, то у статті про це не було мови.
Compose Multiplatform для IOS ще у beta, тож для production проектів я б поки не поспішав його використвувати.

дайте лінку на github з готовим example KMP, так щоб збирався без танців з бубном.

kmp.jetbrains.com проставляєте що потрібно і віддається архів проекту що запускається, плюс є кілька шаблонних проектів, наче все що потрібно є

Welcome github.com/tberchanov/AndroidToKMP 🙂

Трохи зайняло часу, але краще пізніше ніж ніколи)

Дуже цікаво. Була проблема знайти гарний та зрозумілий туторіал.

Підписатись на коментарі