Мультиплатформенная разработка: гипотезы, реальность и продуктовая история
Привет, меня зовут Ирина Наседкина, я Product Owner в EXANTE. Я отвечаю за наше трейдинговое мобильное приложение. В этой статье я поделилась историей нашего перехода к мультиплатформенной разработке приложения в рамках обновления UI/UX: как мы выбирали пути решения, с чем столкнулись и к чему пришли.
Часть 1. С чего всё началось
Два года назад у нас было два приложения
Первое приложение было классическим трейдинговым. Около десяти лет разработки, огромное количество фич, написанных нативно под iOS и Android. За эти годы приложение обросло функциональностью, но его дизайн сильно устарел. UX-подходы были неконсистентными: и внутри самого приложения, и особенно между платформами. То, что работало одним образом на iOS, на Android вело себя иначе. Это создавало постоянные вопросы и раздражение у клиентов.
Второе приложение было моложе и современнее, с консистентным и свежим UI/UX, но с гораздо меньшим количеством фич по сравнению с первым. При этом тоже нативное, под обе платформы.


Итого: первое приложение было богатым по функциям, но с устаревшим и рассыпающимся дизайном. Второе выглядело красиво и современно, но функционально было ограничено. И оба были нативными, то есть каждое изменение приходилось делать дважды.
Два приложения, которые делают примерно одно и то же, но живут параллельными жизнями.
Над обоими приложениями работало две команды: в каждой была разработка iOS и Android, QA и тестирование. Всё было общим по процессу, но раздельным по реализации. Одна и та же фича писалась дважды: отдельно для iOS, отдельно для Android, а иногда и четыре раза, в оба приложения под обе платформы. Это постоянно порождало вопросы, особенно для QA и авторазработчиков, которым приходилось учитывать разницу в реализации и писать тесты под каждую платформу отдельно. Конечно, мы рассматривали варианты разделения команд по приложениям, но разная величина бэклога и похожий опыт этого не позволяли, а разделение по платформам увеличило бы различия ещё больше.
Кроме того, сама фича на двух платформах могла работать по-разному, не потому, что так задумано, а потому, что её делали разные люди для разных платформ. Разные решения, разные интерпретации одного требования. И это нужно было постоянно выявлять и выравнивать. Я уже не говорю о том, что поддержка двух нативных кодовых баз означает в два раза больше всего: ревью, багов, техдолга, онбординга новых людей.
Отдельно сто́ит сказать про атмосферу внутри команды
Несмотря на то что несколько лет назад iOS и Android были объединены в одну команду с общим продуктом, общими процессами и общей организационной структурой, на практике единой командой это так и не стало. Разные платформы, разные кодовые базы, разная реализация одних и тех же фич исторически создавали дух соперничества. «У нас на iOS вот так сделано», «а мы на Android вот это запилили». Негласная конкуренция никуда не делась, она просто ушла вглубь.
Люди работали по своим привычным flow, принимали решения в рамках своей платформы и, даже находясь формально в одной команде, продолжали мыслить категориями «моя платформа». Это мешало договариваться и двигаться в одну сторону.
Часть 2. Первое решение и поворот к мультиплатформе
Когда мы решили разрабатывать новое мобильное приложение, первая идея казалась логичной: взять второе, более современное приложение за основу и добавить в него фичи из старого. Оно уже разрабатывалось по единым макетам, с едиными подходами и по общим требованиям, то есть было значительно более унифицированным вариантом. План был относительно простой: использовать его как фундамент, перенести недостающую функциональность и тем самым получить одно полноценное приложение. Предполагалось, что это займёт много времени, но основа-то уже есть, причём с документацией, дизайном, тест-кейсами и даже с 35% покрытия автотестами.
Но буквально через пару месяцев, пока готовились дизайны, появилась другая идея.
А что, если написать новое приложение на общем стеке технологий (Kotlin Multiplatform и Compose) и закрыть сразу несколько проблем одним решением? Разницу в реализациях между платформами, разницу в подходах и, в перспективе, сложность поддержки двух нативных кодовых баз.
Чтобы не рисковать, решили проверить гипотезу на практике. Взяли один модуль, написали его на KMP и Compose и встроили в старое приложение. Это был намеренно небольшой, но реальный эксперимент с конкретной целью: убедиться, что мультиплатформенные модули корректно встраиваются и работают внутри нативного приложения.
И в тот момент план был ещё достаточно консервативным: мы не собирались переписывать всё с нуля. Идея была в том, чтобы переходить на KMP постепенно: переписывать или добавлять только те модули, которых касаются новые фичи. Эволюционно, без полного сноса старого.
На время эксперимента весь проект по новому приложению был поставлен на паузу. Ждали результатов, прежде чем двигаться дальше. Это было осознанное решение: не тратить ресурсы на направление, которое могло не сработать.
Но команда не простаивала. Параллельно продолжалась обычная работа: разработка и поддержка фич в обоих существующих приложениях, плюс сам эксперимент. Причём мы подошли к нему прагматично: выбрали для теста реальную фичу, которую всё равно нужно было делать, и сразу выкатили её в продакшен. По факту получили не просто лабораторный эксперимент, а полноценное боевое тестирование подхода.
Это сыграло важную роль. Эксперимент занял несколько месяцев, в том числе потому, что команда одновременно тянула поддержку и развитие старых приложений. Но результат оказался положительным: фича на Kotlin Multiplatform и Compose успешно заработала на проде в одном из приложений, а значит, можно и всё новое приложение переносить на Kotlin Multiplatform и Compose.
И вот тогда произошёл настоящий поворот: мы фактически пересмотрели архитектурные решения будущего приложения. Стало понятно: мы делаем мультиплатформу. Не «попробуем встроить пару модулей», а делаем полноценное мультиплатформенное приложение на KMP и Compose.
Позже мы стали применять гибридный подход и для обоих приложений, где старый дизайн соседствует с новым. Но у старого приложения дизайн настолько устарел, что хуже уже не сделаешь. Поэтому мы осознанно выбрали этот путь для новых фич, не дожидаясь полного перехода.
Пожалуй, это и была настоящая точка старта проекта.
Часть 3. Архитектурные решения: как перейти на одно приложение с возможностью конфигурации
Когда стало ясно, что мы делаем мультиплатформу, нужно было принять ряд фундаментальных архитектурных решений. Одним из первых стал вопрос модульности: как управлять набором функций для разных клиентов. Мы понимали, что разным клиентам нужны разные конфигурации приложения.
Рассматривали три варианта.
Первый вариант: переключать модули через внешний сервис брендинга. Настройка снаружи, приложение получает нужный набор функций при сборке или запуске.
Второй вариант: сделать два фиксированных сетапа с разными наборами модулей и переключать между ними одним флагом.
Третий вариант: управлять каждым модулем независимо, включая и выключая их по отдельности прямо внутри приложения.
В итоге выбрали первый вариант, конфигурацию через брендинг. Решили, что это наиболее быстрореализуемый путь, а если понадобится более гибкое управление модулями, всегда успеем доработать.
Как выяснилось впоследствии, разные «состояния» модулей нам особо и не понадобились. А вот простая история «модуль доступен или недоступен» очень даже пригодилась. Так что выбор оказался правильным: не стали усложнять там, где не требовалось, и выиграли время.
Часть 4. Компонентная система: один раз — везде
Отдельно сто́ит рассказать про подход к компонентам, потому что это один из столпов всей архитектуры.
Принцип простой: каждый UI/UX-компонент в дизайне жёстко связан с компонентом в коде. Компонент служит базовым строительным блоком приложения. Мы составили список всех компонентов, согласовали его между дизайном и разработкой, и дальше всё строится из этого списка: и дизайн модулей, и их разработка. Кнопки, поля ввода, свитчи, таблицы и всё остальное.
Главный принцип при разработке: максимальное переиспользование. Прежде чем создавать что-то новое, мы задаём вопрос: можно ли обойтись существующим компонентом? И в большинстве случаев да. Сейчас, выбирая между удобством в конкретном месте и универсальностью компонента, мы осознанно выбираем универсальность.
Почему это важно именно сейчас? Потому что мы всё ещё поддерживаем старые приложения параллельно с разработкой нового. Ресурсы ограничены, и экономия времени критична. Компонентный подход даёт её в полной мере: поведение компонента описывается один раз и работает везде одинаково. Не нужно каждый раз заново проектировать, описывать в требованиях и проверять на QA.
Да, в будущем наверняка появятся отклонения от этого правила. Но мы готовы их принимать только с серьёзным обоснованием, потому что каждое исключение означает дополнительные затраты на поддержку.
Часть 5. Люди и процессы: новые процессы и новые знания
Архитектурные решения важны. Но любое кардинальное изменение в подходе к разработке прежде всего означает изменение для людей.
Переход на мультиплатформу означал, что нативные разработчики перестают быть ответственными за отдельную платформу. iOS-разработчик больше не «iOS-разработчик», а разработчик общей платформы. Это вызвало много вопросов и сопротивления. Люди годами строили экспертизу в конкретной технологии, и теперь им говорят: всё меняется, и вам нужно изучать новую платформу.
Организационно это тоже означало серьёзную перестройку. Один репозиторий вместо двух. Гораздо больше людей работают в одном пространстве. Новые правила: как устроена структура кода, как проводится ревью, как принимаются решения. Всё, что раньше решалось внутри iOS- или Android-команды, теперь требует согласования.
Изменилась и работа с дизайном. Она стала значительно теснее. При компонентном подходе каждое дизайн-решение напрямую влияет на разработку: новый компонент в макете означает новый компонент в коде, со своими состояниями, поведением и документацией. И главное, изменение компонента затрагивает сразу несколько модулей. Поэтому важно проверить все места использования и убедиться, что ничего не сломалось.
И ещё один важный момент: разработка стала требовать больше связности внутри команд. Добавляя новый модуль, нужно не просто написать его, но и проверить, можно ли переиспользовать существующие компоненты. А если создаёшь новый компонент, важно убедиться, что он впишется в архитектуру и пригодится не только здесь, но и в других местах. Это требует другого уровня коммуникации и другого мышления.
Но произошло кое-что, чего, возможно, стоило ожидать, но всё равно приятно удивило.
Такие масштабные изменения сплачивают. Конкуренция между платформами растворилась в общей цели. Работать над единым продуктом и единым пользовательским опытом совсем не то же самое, что делать похожие фичи параллельно в разных приложениях. Разработчики стали иначе относиться и к продукту, и к коллегам из других команд. Границы между iOS и Android постепенно стираются, появляется одна большая платформенная команда. Стало сложнее? Да, однозначно. Но и значительно интереснее.
В рамках работы над приложением нам очень помогли и помогают внутренние ревью команды, когда каждый в команде устанавливал себе новое приложение и старался посмотреть на него глазами пользователя. Так мы собрали много улучшений и исправлений. Казалось бы, очевидная практика, но часто разработчик фокусируется на том, чтобы хорошо сделать свою фичу, а пользовательский взгляд охватывает опыт использования приложения в целом.
Часть 6. Релиз: как и что дальше
Когда встал вопрос о способе выпуска, рассматривали два варианта: выпустить отдельное новое приложение или выкатить всё как обновление существующего.
Изучив оба пути, выбрали обновление. Главная причина в ресурсах. Новое отдельное приложение означало бы необходимость поддерживать два продукта параллельно, убеждать пользователей перейти, тянуть две аудитории. Обновление позволяет сосредоточить все силы на стабилизации и развитии нового продукта, не распыляясь.
Но любое обновление вызывает стресс у пользователя. Особенно когда меняется всё. Поэтому мы уделили особое внимание сохранению общей структуры приложения: постарались максимально сохранить старые паттерны навигации, привычное расположение фич и знакомые сценарии поведения. Чтобы пользователь открыл обновление и почувствовал: выглядит иначе, но я понимаю, где что находится.


Отдельное внимание уделили полноте переноса функциональности. Важно было убедиться, что ни одна фича не потерялась по дороге.
Да, негативная реакция на изменения неизбежна. Любой переход болезненный, даже если новое объективно лучше. Мы это понимаем и готовы к этому. Но параллельная поддержка двух полноценных приложений причиняет боль другого порядка, системную и постоянную. Мы выбрали разовую боль перехода вместо хронической боли двойной поддержки.
Вместо заключения
Если оглянуться назад, эта история не про технологии. Точнее, не только про них.
Kotlin Multiplatform и Compose остаются инструментами. Они сработали. Но настоящая работа была про другое: про то, как поменять мышление команды, как перестать делать одно и то же дважды, как из двух конкурирующих групп стать одной.
Апдейт приложения в самом разгаре, пользователи получают обновления постепенно, поэтому сложно сказать, насколько быстрее стала разработка. Точно можно сказать, что код пишется один раз. UI- и unit-тесты теперь тоже общие, и 85% кодовой базы общие для iOS и Android. А ещё поведение приложений стало более предсказуемым, потому что у них общий сетевой слой и одинаковое взаимодействие с бэкендом. Это значит, что тестировать проще, а багов меньше.
Если бы нужно было выделить один главный вывод, он такой: не начинай мультиплатформу с переноса старого кода.
Возьми одну новую фичу, напиши её на общей платформе, задеплой в оба приложения. За месяц ты узнаешь больше, чем за полгода планирования. Именно так мы и поняли, что это работает. И именно с этого момента началась настоящая разработка проекта.