Перенос приложения Jetpack Compose на Kotlin Multiplatform
Это руководство посвящено переносу приложения только для Android на мультиплатформенную архитектуру во всём стеке — от бизнес-логики до пользовательского интерфейса. В нём на примере продвинутого приложения Compose показаны распространённые сложности и способы их решения. Вы можете подробно следовать последовательности коммитов или бегло ознакомиться с общими шагами переноса и углубиться в интересующие вас части.
В качестве исходного приложения используется Jetcaster — пример приложения для подкастов, созданного для Android с помощью Jetpack Compose. Это полнофункциональное приложение использует:
Несколько модулей.
Управление ресурсами Android.
Доступ к сети и базе данных.
Compose Navigation.
Новейшие компоненты Material Expressive.
Все эти возможности можно адаптировать для кроссплатформенного приложения с помощью Kotlin Multiplatform и фреймворка Compose Multiplatform.
Чтобы подготовить приложение Android к работе на других платформах, вы можете:
Узнать, как оценить проект на предмет целесообразности переноса на Kotlin Multiplatform (KMP).
Узнать, как разделить модули Gradle на кроссплатформенные и платформенные. В Jetcaster удалось сделать мультиплатформенными большинство модулей бизнес-логики, за исключением некоторых низкоуровневых системных вызовов, которые пришлось отдельно реализовать для iOS и Android.
Проследить, как поочерёдно сделать модули бизнес-логики мультиплатформенными, постепенно обновляя скрипты сборки и код, чтобы переходить от одного рабочего состояния к другому с минимальными изменениями.
Узнать, как перейти к общей реализации пользовательского интерфейса: с помощью Compose Multiplatform можно совместно использовать большую часть кода UI в Jetcaster. Что ещё важнее, вы увидите, как выполнять этот переход постепенно, экран за экраном.
В результате приложение будет работать на Android, iOS и настольных платформах. Настольное приложение также служит примером Compose Hot Reload — способа быстро проверять поведение пользовательского интерфейса.
Контрольный список для возможного переноса на Kotlin Multiplatform
Основные препятствия для возможного переноса на KMP — Java и Android Views. Если проект уже написан на Kotlin и для пользовательского интерфейса используется Jetpack Compose, сложность переноса значительно ниже.
Ниже приведён общий список подготовительных действий, которые следует рассмотреть перед переносом проекта или модуля:
Преобразуйте или изолируйте код на Java
В исходном примере Jetcaster для Android есть вызовы, доступные только в Java, например Objects.hash() и Uri.encode(), а также широкое использование пакета java.time.
Хотя из Kotlin можно вызывать Java-код и наоборот, набор исходных файлов commonMain, содержащий общий код модуля Kotlin Multiplatform, не может включать код на Java. Поэтому, переводя приложение Android на мультиплатформенную архитектуру, необходимо либо:
Изолировать этот код в
androidMain(и переписать его для iOS), либоПреобразовать код на Java в Kotlin, используя совместимые с мультиплатформенной разработкой зависимости.
Ещё одна библиотека, специфичная для Java, — RxJava. Она не используется в Jetcaster, но широко распространена. Поскольку это Java-фреймворк для управления асинхронными операциями, перед переносом на KMP рекомендуется перейти на kotlinx-coroutines.
Существуют руководства по переносу с Java на Kotlin, а также инструмент в IntelliJ IDEA, который может автоматически преобразовать код на Java и упростить этот процесс.
Проверьте зависимости только для Android/JVM
Хотя во многих проектах, особенно новых, может быть немного кода на Java, в них часто используются зависимости только для Android. В случае с Jetcaster большая часть работы заключалась в поиске альтернатив и переходе на них.
Важный шаг — составить список зависимостей, используемых в коде, который вы планируете сделать общим, и убедиться, что для них доступны мультиплатформенные альтернативы. Экосистема мультиплатформенных библиотек пока не так велика, как экосистема Java, но быстро развивается. Для оценки возможных вариантов воспользуйтесь сайтом klibs.io.
Для Jetcaster список таких библиотек выглядел следующим образом:
-
Dagger/Hilt — популярное решение для внедрения зависимостей (заменено на Koin).
Koin — надёжный мультиплатформенный фреймворк для внедрения зависимостей. Если он не отвечает вашим требованиям или объём необходимой переработки слишком велик, есть и другие решения. Фреймворк Metro также является мультиплатформенным. Он может упростить перенос благодаря поддержке взаимодействия с другими аннотациями, включая Dagger и Kotlin Inject.
Coil 2 — библиотека для загрузки изображений (в версии 3 стала мультиплатформенной).
ROME — фреймворк для RSS (заменён мультиплатформенным RSS Parser).
JUnit — фреймворк для тестирования (заменён на kotlin-test).
В процессе вы можете обнаружить небольшие фрагменты кода, которые перестают работать в мультиплатформенной среде, поскольку для них ещё нет кроссплатформенной реализации. Например, в Jetcaster пришлось заменить функцию AnnotatedString.fromHtml(), входящую в библиотеку Compose UI, сторонней мультиплатформенной зависимостью.
Заранее выявить все подобные случаи сложно, поэтому будьте готовы находить замены или переписывать код в процессе переноса. Именно поэтому мы показываем, как переходить от одного рабочего состояния к другому, выполняя как можно меньше изменений за раз. Так одна проблема не остановит ваш прогресс, даже если одновременно меняется много частей приложения.
Устраните технический долг в модульной структуре
KMP позволяет выборочно переводить проект на мультиплатформенную архитектуру — модуль за модулем, экран за экраном. Чтобы этот процесс проходил гладко, структура модулей должна быть понятной и удобной для изменений. Рекомендуем оценить модульную структуру с учётом принципа высокой связности и слабой связанности, а также других рекомендаций по структурированию модулей.
Общие рекомендации можно сформулировать следующим образом:
Выделяйте отдельные функциональные части приложения в модули функций и отделяйте их от модулей данных, которые управляют данными и предоставляют к ним доступ.
Инкапсулируйте данные и бизнес-логику конкретной предметной области в одном модуле. Объединяйте связанные типы данных и не смешивайте логику или данные разных предметных областей.
Ограничивайте доступ извне к деталям реализации модуля и источникам данных с помощью модификаторов видимости Kotlin.
При чёткой структуре вы сможете переносить модули на KMP по отдельности, даже если в проекте их много. Такой подход проще, чем пытаться полностью переписать проект.
Перейдите с Views на Jetpack Compose
Kotlin Multiplatform предоставляет Compose Multiplatform для создания кроссплатформенного кода пользовательского интерфейса. Чтобы плавно перейти на Compose Multiplatform, интерфейс уже должен быть написан с помощью Compose. Если сейчас вы используете Views, код придётся переписать, применив новую парадигму и новый фреймворк. Очевидно, что сделать это заранее проще.
Google уже давно развивает и совершенствует Compose. Ознакомьтесь с руководствами по переносу на Jetpack Compose, которые помогут в наиболее распространённых сценариях, или попробуйте навык агента для переноса с помощью ИИ. Также можно использовать совместимость Views и Compose, но, как и код на Java, такой код необходимо изолировать в наборе исходных файлов androidMain.
Этапы перехода приложения на мультиплатформенную архитектуру
После завершения предварительной подготовки и оценки общий процесс выглядит следующим образом:
-
Перевести бизнес-логику на KMP.
Начните с модуля, от которого зависит наименьшее количество других модулей.
Переведите его на структуру модулей KMP и начните использовать мультиплатформенные библиотеки.
Выберите следующий модуль в дереве зависимостей и повторите процесс.
Переведите код пользовательского интерфейса на Compose Multiplatform. Если вся бизнес-логика уже мультиплатформенная, переход на Compose Multiplatform будет относительно простым. В случае с Jetcaster мы показываем постепенный перенос — экран за экраном. Мы также показываем, как изменять граф навигации, когда одни экраны уже перенесены, а другие — ещё нет.
Чтобы упростить пример, мы сразу удалили специфичные для Android целевые платформы Glance, TV и wearable, поскольку они всё равно не взаимодействуют с мультиплатформенным кодом и переносить их не потребуется.
Подготовьте среду
Если вы хотите повторить шаги переноса или запустить предоставленный пример на своём компьютере, сначала подготовьте среду:
-
Выполните инструкции из краткого руководства, чтобы настроить среду для Kotlin Multiplatform.
-
В IntelliJ IDEA или Android Studio создайте новый проект, клонировав репозиторий с примером:
git@github.com:kotlin-hands-on/jetcaster-kmp-migration.git
Перейдите на мультиплатформенные библиотеки
Работа большинства функций приложения зависит от нескольких библиотек. До настройки модулей для поддержки мультиплатформенности можно перейти на их версии, совместимые с KMP:
-
Замените парсер ROME tools на мультиплатформенный RSS Parser. При этом необходимо учесть различия между API, в частности способы обработки дат.
-
Замените Dagger/Hilt на Koin 4 во всём приложении, включая модуль точки входа, предназначенный только для Android,
mobile. Для этого потребуется переписать логику внедрения зависимостей в соответствии с подходом Koin, но код за пределами пакетов*.diпочти не изменится.При переходе с Hilt обязательно очистите каталоги
/build, чтобы избежать ошибок компиляции из-за ранее сгенерированного кода Hilt. -
Обновите Coil 2 до Coil 3. И здесь было изменено относительно мало кода.
-
Замените JUnit на
kotlin-test. Это касается всех модулей с тестами, но благодаря совместимости сkotlin-testдля переноса потребуется совсем немного изменений.
Перепишите зависящий от Java код на Kotlin
Теперь, когда все основные библиотеки стали мультиплатформенными, необходимо устранить зависимости только для Java.
Простой пример вызова, доступного только в Java, — Objects.hash(). Мы повторно реализовали его на Kotlin. См. итоговый коммит.
Однако главная причина, мешающая сразу сделать код Jetcaster общим, — пакет java.time. В приложении для подкастов вычисления времени встречаются повсюду, поэтому для полноценного совместного использования кода KMP необходимо перенести этот код на kotlin.time и kotlinx-datetime.
Переписывание всего кода, связанного со временем, собрано в этом коммите.
Перенос бизнес-логики
Когда основные зависимости становятся мультиплатформенными, можно выбрать модуль, с которого начать перенос. Полезно составить граф зависимостей модулей в проекте. С этим легко поможет ИИ-агент, например Junie. Для Jetcaster упрощённый граф зависимостей модулей выглядел так:
Например, можно придерживаться такой последовательности:
:core:data:core:data-testing:core:domain:core:domain-testing:core:designsystem— хотя у него нет зависимостей от других модулей, это вспомогательный модуль пользовательского интерфейса, поэтому мы займёмся им, когда будем готовы перенести код пользовательского интерфейса в общий модуль.
Перенос :core:data
Настройка :core:data и перенос кода базы данных
В Jetcaster в качестве библиотеки для работы с базой данных используется Room. Поскольку Room поддерживает мультиплатформенность начиная с версии 2.7.0, нам нужно лишь обновить код, чтобы он работал на разных платформах. На этом этапе у нас ещё нет приложения для iOS, но мы уже можем написать платформенно-зависимый код, который будет вызываться при настройке точки входа для iOS. Мы также добавляем конфигурацию целевых платформ (iOS и JVM), чтобы подготовиться к добавлению новых точек входа в будущем.
Чтобы перейти на мультиплатформенную версию Room, мы воспользовались общим руководством по настройке для Android.
Обратите внимание на новую структуру кода с наборами исходного кода
androidMain,commonMain,iosMainиjvmMain.Большинство изменений в коде связано с созданием структуры expect/actual для Room и соответствующими изменениями DI.
Добавлен новый интерфейс
OnlineChecker, который позволяет учитывать тот факт, что проверка подключения к интернету выполняется только на Android. Пока мы не добавим приложение iOS в качестве целевой платформы, проверка подключения будет заглушкой.
Можно сразу же перенастроить и модуль :core:data-testing, чтобы сделать его мультиплатформенным. См. итоговый коммит. Для этого нужно лишь обновить конфигурацию Gradle и перейти к структуре каталогов наборов исходного кода.
Настройка и перенос :core:domain
Если все зависимости уже учтены и переведены на мультиплатформенную версию, остаётся только переместить код и перенастроить модуль.
Как и в случае с :core:data-testing, модуль :core:domain-testing также легко можно перевести на мультиплатформенную основу.
Настройка и перенос :core:designsystem
Когда осталось перенести только код пользовательского интерфейса, мы начинаем переводить модуль :core:designsystem, включая ресурсы шрифтов и типографику. Помимо настройки модуля KMP и создания набора исходного кода commonMain, мы сделали аргумент JetcasterTypography для MaterialExpressiveTheme компонуемой функцией, инкапсулировав вызовы мультиплатформенных шрифтов.
Переход на мультиплатформенный пользовательский интерфейс
Когда вся логика :core становится мультиплатформенной, можно начать переносить пользовательский интерфейс в общий код. И снова: поскольку мы стремимся выполнить полный перенос, пока не добавляем целевую платформу iOS, а лишь убеждаемся, что приложение для Android работает с компонентами Compose, размещёнными в общем коде.
Чтобы наглядно представить последовательность действий, ниже приведена упрощённая схема взаимосвязей между экранами Jetcaster:
Сначала мы создали общий модуль пользовательского интерфейса для кода, который собираемся сделать общим.
Чтобы показать, как постепенно переносить пользовательский интерфейс, мы будем переносить его экран за экраном. Каждый этап завершается коммитом, в котором приложение находится в рабочем состоянии и становится немного ближе к полностью общему пользовательскому интерфейсу.
Следуя приведённой выше схеме экранов, мы начали с экрана сведений о подкасте:
-
Перенесённый экран будет работать, даже если тема Compose останется в модуле Android. Для этого необходимо:
Обновить ViewModel и соответствующий код DI.
-
Обновить ресурсы и средства доступа к ресурсам. Хотя мультиплатформенная библиотека ресурсов во многом соответствует Android, есть несколько заметных отличий, которые необходимо учесть:
Обработка файлов ресурсов немного отличается. Например, каталог ресурсов должен называться
composeResourcesвместоres, а в XML-файлах Android необходимо заменить использования@android:colorна шестнадцатеричные коды цветов. Подробнее см. в документации о мультиплатформенных ресурсах.Сгенерированный класс со средствами доступа к ресурсам называется
Res(в отличие отRв Android). Переместив и настроив файлы ресурсов, повторно сгенерируйте средства доступа и замените импорты каждого ресурса в коде пользовательского интерфейса.
-
Перенести тему Compose. Мы также добавим заглушки для платформенно-зависимых реализаций цветовых схем.
-
Продолжить с главным экраном:
Перенести ViewModel.
Переместить код в
commonMainобщего модуля пользовательского интерфейса.Переместить и настроить ссылки на ресурсы.
-
Чтобы показать ещё один способ разделить перенос на небольшие этапы, мы частично перенесли навигацию. Можно объединить экраны из общего кода с нативным экраном Android.
PlayerScreenпо-прежнему находится в модулеmobileи включается в навигацию только для точки входа Android. Он внедряется в общую мультиплатформенную навигацию. -
В завершение перенесите всё оставшееся:
Перенесите оставшуюся часть навигации в общий код (итоговый коммит).
Перенесите последний экран,
PlayerScreen, в Compose Multiplatform (итоговый коммит).
Теперь, когда весь код пользовательского интерфейса стал общим, можно быстро создавать на его основе приложения для других платформ.
Необязательно: добавление точки входа JVM
Этот необязательный этап позволяет:
Показать, как мало усилий требуется, чтобы создать настольное приложение на основе приложения Android, полностью переведённого на мультиплатформенную основу.
Продемонстрировать Compose Hot Reload — инструмент для быстрой итеративной разработки пользовательского интерфейса Compose, который в настоящее время поддерживается только для целевых настольных платформ.
Когда весь код пользовательского интерфейса общий, для добавления новой точки входа настольного приложения JVM достаточно создать функцию main() и интегрировать её с DI-фреймворком.
Добавление точки входа iOS
Для точки входа iOS необходим проект iOS, связанный с кодом KMP.
Создание и встраивание приложения iOS в проект KMP описано в руководстве «Перевод приложения на мультиплатформенную основу».
В приложении iOS необходимо связать код Swift UI с кодом Compose Multiplatform. Для этого мы добавляем функцию, которая возвращает UIViewController со встроенным компонуемым элементом JetcasterApp, в приложение iOS.
Запуск приложения
В итоговой версии перенесённого приложения есть конфигурации запуска для исходного модуля Android (mobile) и нового приложения iOS. Настольное приложение можно запустить из соответствующего файла main.kt. Запустите оба приложения, чтобы увидеть, как общий пользовательский интерфейс работает на всех платформах!
Итоги
При переносе мы следовали общим рекомендациям по преобразованию обычного приложения Android в приложение Kotlin Multiplatform:
Перейти на мультиплатформенные зависимости или переписать код, если это невозможно.
Поочерёдно преобразовать модули Android, которые можно использовать на других платформах, в мультиплатформенные модули.
Создать общий модуль пользовательского интерфейса для кода Compose Multiplatform и поэтапно переносить в него пользовательский интерфейс, экран за экраном.
Создать точки входа для других платформ.
Эта последовательность не является обязательной. Можно начать с точек входа для других платформ и постепенно создавать для них основу, пока они не заработают. В примере Jetcaster мы выбрали более понятную последовательность изменений, которой легко следовать шаг за шагом.
Если у вас есть отзывы о руководстве или представленных решениях, создайте задачу в YouTrack.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/migrate-from-android.html