Опыт выбора кроссплатформенной технологии для разработки приложения. Доклад Яндекса
Когда нужно сделать мобильное приложение, вариант по умолчанию — это нативная разработка. Другие способы по-прежнему не всегда принимаются в расчет. Но альтернативы, в том числе сравнительно новые, правда существуют. О них в своем докладе рассказал Геннадий Евстратов, руководитель одной из команд в Яндекс Go. Вы узнаете, почему Flutter — наиболее перспективная кроссплатформенная технология, погрузитесь в детали безопасности, узнаете об основных архитектурах, о разнице между BLoC и React и о тестировании Flutter-приложений.— Всем привет, меня зовут Гена, я руковожу iOS-разработкой партнерских продуктов Яндекс.Такси. Расскажу, почему мы выбрали для своего продукта flutter, через что мы проходили перед тем, как сделали этот выбор, и что из этого получилось. Доклад будет достаточно вводный. Поехали.
Какие проблемы мы решали
Прошлым летом мы в Такси решили сделать iOS-версию Таксометра — приложения, которым пользуются водители для приема заказов. До этого он существовал только для Android. Сроки были сжатые, перед нами встал вопрос выбора технологий, а кроме того, у нас был ряд ограничений.
Например, ограничения по времени. Нам нужно было сделать релиз примерно за два с половиной — три месяца с нуля. У нас не было никакой кодовой базы, ничего. Поэтому мы начали смотреть в сторону средств, которые заявляют о быстрой разработке и у которых очень хорошее, небольшое время компиляции. Нам больно было смотреть на iOS-проект Такси, который собирался примерно минут за 20. И мы искали средства с хорошим тулингом, чтобы программистам было удобно работать.
Кроме того, дизайнеры очень хотели, чтобы приложение выглядело идентично на iOS и Android. Они сделали собственную библиотеку UI-компонентов. И очень хотели, чтобы мы ее поддержали.
Когда мы стали смотреть в сторону кроссплатформенной разработки, мы поняли, что нам очень важна легкая интеграция нативного кода. Потому что в Яндексе масса внутренних библиотек, начиная с MapKit для отображения карт и заканчивая множеством других — для безопасности и прочего, которые написаны на плюсах. В том числе нативные. И нам нужно было, чтобы они очень легко интегрировались в наши решения.
Мы смотрели еще и в будущее — хотели, чтобы наша разработка легко интегрировалась в существующее Android-приложение, чтобы в будущем оно могло брать у нас фрагменты и применять их у себя.
Рассматриваемые варианты
Мы собрали эти требования и начали смотреть, какие у нас варианты. Первым вариантом оказался, к сожалению, React Native.
Это был достаточно хайповый выбор, потому что в тот момент все про него говорили. У меня уже был опыт работы с React Native, много народу в команде знало JS или TS. Тулинг для React Native тоже казался хорошим. Поэтому мы окунулись в это дело с головой и потратили месяц, чтобы сделать прототип на React Native.
Он даже завелся, но что-то пошло не так. А именно вот это. Во-первых, тулинг оказался не таким хорошим.
Когда мы готовили прототип, к нам в команду приходило много людей, и каждый тратил по несколько дней на то, чтобы завести редактор, флоу, Prettier — связку, которая отлично заводится в статьях на Medium. Но в жизни постоянно что-то отваливается, отсыхает, и это было больно.
Потом мы поговорили со службой безопасности и выяснили, что там тоже есть проблемы, потому что минифицированный JS, лежащий в бандле приложения, достаточно просто ломается.
Когда мы начали интегрироваться с React Native, мы тоже поймали ряд проблем: у нас достаточно много сообщений прокачивается между нативной и кроссплатформенной частью, и шина сообщений в React начала просто помирать от этого.
Кроме того, во время нашей разработки React Native обновился с 69 до 70 версии. У нас, естественно, все сломалось. Мы потратили пару дней на то, чтобы опять привести прототип в рабочее состояние. Это нас сильно расстроило и послужило еще одним камнем в огород RN, из-за которого мы в итоге от него отказались.
И последнее: мы поговорили с людьми из Facebook, которые разрабатывают RN, и услышали от них много инсайдерской информации. Не уверен, что про нее можно сказать, но получается так, что в Facebook есть два React Native — который они используют для себя и который отдают в опенсорс. Во второй версии появляется далеко не все, что есть внутри.
Мы решили выкинуть React Native. Грешным делом задумались о том, чтобы начать писать всё нативно, отказаться от нашей красивой кроссплатформенной истории. Но собрались, вспомнили про наши цели и перешли на flutter. Сделали на нем очень мелкий прототип.
Опасения бизнеса и top-tech
Нам осталось защитить flutter перед нашим top-tech — форумом технического менеджмента и старших технических специалистов.
Мы пошли на top-tech, показали им flutter и узнали, что их тревожит.
В первую очередь их тревожило то, что это полностью новая технология в стеке. Внутри Такси мы стараемся делать всё как можно более единообразно, по возможности использовать одни и те же языки, не использовать что-то нестандартное. Почему? Потому что это позволяет большинству людей разбираться в большинстве имеющегося кода.
Стараемся использовать самые распространенные технологии, чтобы потом было проще заниматься наймом или ротировать разработчиков. И мы были первыми в компании, кто хотел писать на flutter, и не во внутренних, тестовых или пет-проджектах, а в большом проекте для пользователей.
Потом у нас возник вопрос по поводу того, что flutter — это некий вендор-лок на Google, потому что и flutter, и dart делаются в Google.
Но мы подумали и убедили людей в том, что то же самое можно сказать и про другие библиотеки, то есть про любые большие фундаментальные библиотеки, которые мы используем в разработке. Они все равно принадлежат не нам, там может что-то сломаться, их могут перестать поддерживать. Google, как и мы, временами закапывает свои проекты. Но dart и flutter полностью опенсорсные — если с ними что-то случится, их можно зафоркать и жить с текущей версией. Так что этот консерн мы отмели практически сразу.
Был другой консёрн — что это очень эзотерический язык, на нем пишет мало народу. Сложно нанимать людей в команду: во-первых, их практически нет, а во-вторых, люди не очень хотят переучиваться. Но переучиться можно достаточно быстро, я про это и про некоторые метрики расскажу чуть дальше.
Основным консёрном было отсутствие историй успеха. Да, есть кейсы на flutter.dev. Там рассказывают про существующие приложения. Но нет реальных историй успеха от больших компаний, которые говорят: мы применили flutter, используем его, у нас все хорошо.
Мы знаем о нескольких крупных компаниях, которые сейчас применяют flutter, но они делают это не очень публично. Поэтому менеджменту было не очень понятно, насколько быстрой и хорошей будет разработка, а также насколько дорогостоящей и сложной окажется поддержка.
Как мы договорились использовать flutter (в mission critical-проекте)
Тем не менее, мы договорились использовать flutter. Точнее — договорились до того, что это будет пилотный для технологии проект, что мы будем снимать метрики с разработки, которые я покажу чуть позже, и что метрики станут основой для принятия решения, применять ли flutter дальше. Честно договорились: если метрики окажутся плохими, мы с ребятами перепишем всё на натив и будем разрабатывать приложение нативно.
Метрики
Какие важные метрики относительно бизнеса и относительно удовольствия разработчиков мы договорились снимать?
Для начала — скорость разработки. Мы могли ее померить, потому что делали клон уже существующего приложения. Мы смотрели по коммитам, сколько времени занимала реализация той или иной фичи, user story в существующем приложении и сколько получилось у нас.
Договорились, что мы будем мерить, насколько честное кроссплатформенное решение получилось. То есть одно из условий — приложение должно собираться под обе платформы и быть одинаково функциональным и там и там. Договорились, что мы будем измерять тестируемость кода, покрытие тестами, легкость написания тестов и прочие связанные вещи.
Также договорились померить субъективные метрики, например экосистему — это про тулинг, наличие библиотек и прочее — и удобство для разработчиков. Насколько удобное IDE, насколько все быстро компилируется, перезапускается, насколько хорошо задокументировано.
В итоге мы все это померили. То, что не попало под NDA, будет чуть дальше в докладе.
Архитектура
Расскажу, каким мы сделали приложение с точки зрения архитектуры. Те, кто уже работал с flutter, знают, что у него уже есть такая ярко выраженная реактивная архитектура. Поэтому здесь правильно говорить скорее не про архитектуру, а про подход к управлению состоянием приложения — как данные хранятся внутри и как они расходятся в виджеты.
Сначала мы начали пробовать BLoC, который Google называет, условно, go-to архитектурой для flutter-приложений. BLoC тесно связан с RX, стейт прокачивается во вьюхе стримами. Из вьюх события стримами уходят в контроллер. Чем-то напоминает MVVM, чем-то RIBs. Мы с этим поработали, нам не то чтобы понравилось, и мы решили выбрать redux.
redux — классическая архитектура для веба. Ее сделал Дэн Абрамов, который сейчас работает в Facebook. Это одна из архитектур Unidirectional Dataflow. Смысл очень простой. Есть центральный store, который хранит состояние всего приложения. Виджеты подписываются на его изменения. Store иммутабельный, его никто не меняет. Меняется он только через action. Action — некое событие, у которого есть типы, payload, которые определяют, что именно меняется.
Action обрабатывают с reducers. Reducers — чистая функция, когда результат выполнения зависит только от входных параметров, она ничего не меняет снаружи, не смотрит ни на что вне своего контекста. Поэтому она очень хорошо тестируется, об этом поговорим позже.
Понятно, что не все action можно обработать чистыми функциями. Есть action side effects — походы в сеть, изменения данных в базе. reducers, которые делают side effect, внезапно называются эффектами. Эффекты тоже легко тестировать, их тоже обсудим.
В общем, архитектура достаточно простая. Есть Store, в него приходят action, reducers или эффекты, меняют этот store. Когда store меняется, виджеты, подписанные на какие-то куски этого store, немедленно обновляются.
Такой подход дает возможность четко разделять зоны ответственности в коде. Он становится очень хорошо структурированным, и его от этого очень просто тестировать. Причем мы выбрали не просто redux, а fish-redux. это реализация redux-фреймворка от Alibaba. Там мы реализуем не только store, но и обвязки вокруг него — комбинации reducers и прочее. Это просто для упрощения работы, чтобы писать меньше кода. По ссылке можете посмотреть, что это такое.
Я часто упоминал тестирование. Для меня это, можно сказать, фетиш. Я видел много больших мобильных проектов и не видел ни одного проекта с нормальными тестами. Если у вас есть такой, я буду очень рад его увидеть.
Мы решили, что так жить нельзя и нам нужно с самого начала писать тесты. Это было одно из условий, из-за которых мы выбрали redux.
Мы посмотрели во фреймворк fish, посмотрели, что там есть на тему тестов. Спойлер: ничего. Мы начали смотреть roadmap, его пишет Alibaba на китайском. Начали применять автопереводчик и тоже ничего не увидели по поводу тестов. Может, автопереводчик как-то не так работал, а может, действительно ничего не было.
Поэтому мы решили всё делать сами, не отчаялись. Слава архитектуре: поскольку reducers чистые, их можно тестировать просто так, стандартными тестами.
С эффектами было сложнее. Мы начали использовать dependency injection, инъецировали моки и стабы в эффекты. Смотрели, что выполняется. И тоже без проблем начали тестировать эффекты.
Views тестировали стандартными виджет-тестами: вставляли туда моками store, смотрели, что action диспатчатся правильно. И ура: сейчас у нас практически все приложение покрыто тестами. Они выполняются автоматически, это супер.
Весь этот подход — не то чтобы наше изобретение. У меня был опыт работы с вебом, и мы использовали стандартные подходы для тестирования Single Page Applications, которые redux использует на вебе. Нам все подошло.
Как мы это писали
Что конкретно мы писали, я рассказал. Теперь расскажу, как мы это писали.
Всего нас четыре человека. Сперва мы делали прототип на React Native. Там у нас тоже был redux. Вся команда, кроме меня, впервые столкнулась и с тем и с другим. Мы писали прототип, прошел месяц, я предложил писать на flutter. У ребят немного потухли глаза. Но я их убедил попробовать dart. И через час ребята уже читали примеры dart-кода.
Еще через пару часов начали делать первые прототипы приложений. А через пару недель написали кастомную навигацию и менеджер тем, которые отлично интегрировались в iOS 13, переключали темную и светлую тему на лету и вот это все.
Это не сложно. Но в первое время все равно пришлось поработать. Сейчас это продолжается, но чуть меньше. Мы адски друг друга ревьюили. Пересмотрели, наверное, все видео и туториалы, которые есть в сети по поводу dart, стиля кода, по поводу всего.
И нам понравилось. Кстати, насколько я узнал по разговорам с командой, здесь все было отлично в отличие от React Native и TypeScript, где у всех всегда все горело, что-то отваливалось, не работало, не собиралось. Сейчас ребята периодически подходят друг к другу и говорят: «Блин, посмотри, как хорошо! Быстро, эффективно. На нативе мы бы это делали годами, а здесь все получается буквально за пару часов».
Из того, что я видел, основные проблемы были не с dart и с flutter, а со знанием архитектуры — как работают reducers, эффекты и так далее. Но это нормально. Для нативной разработки такой подход все еще, к сожалению, является диковинным, нестандартным, и поэтому ребята к нему были не очень привычны.
Что из этого получилось
Мы зарелизились, был первый продакшн-билд. С этого момента не было ни единого крыша, и все довольны. В Москве мы сами с тестерами делали поездки, тоже используя наше приложение. И тоже все работает.
Наверное, у нас получился самый большой русский публичный flutter-проект. Но пока мы в самом начале. (…)
В результате нашей работы получилось чуть больше десяти плагинов с платформенным кодом. Мы интегрировали внутренние яндексовые библиотеки. Самая простая интеграция была с appmetrica, буквально пару строк для отправки запросов.
Чуть сложнее была интеграция с Картами. Самой сложной оказалась интеграция с одной внутренней либой. Я не могу сказать, что именно она делает, но это такая большая плюсовая штука, для которой нам сначала пришлось генерировать wrappers через djinni, а полученный натив мы уже забирали во flutter. На Android внезапно есть проблемы с тем, чтобы завести djinni, но мы их побороли.
Метрики
Теперь про метрики. Повторюсь, что мы не до конца защитили flutter, но это в процессе. И про объем проекта.
У нас 29 тысяч строк — в основном, приложений. Еще около 7 тысяч строк в плагинах, там смешанный dart и платформенный код. Понятно, что это сильно меньше, чем полмиллиона нативных строк в клиенте Такси, или 400 тысяч строк в андроидном таксометре.
Теперь о том, что мы мерили.
Мы сравнивали скорости сборки проектов на разных платформах. Метрика, наверное, не совсем честная, потому что там проекты сильно больше. Но это, скажем, некоторые усредненные данные, в секундах. Как видим, iOS-клиенты Такси собираются очень долго, потому что это микс-приложение на Objective-C и Swift. Кто пробовал такие штуки собирать, знают, что время компиляции периодически исчисляется десятками минут. Андроидный Таксометр собирается поменьше, но мы все равно собираемся быстрее, и на iOS, и на Android. И только суммарная наша сборка, iOS плюс Android, занимает чуть больше времени.
Одна из вещей, которые нам больше всего нравятся во flutter, — скорость запуска и перезапуска приложения. Во время разработки во flutter есть hot reload. В чем его смысл? Я делал некие синтетические тесты, делал flutter- и нативные приложения, смотрел, сколько времени занимает их старт и рестарт при внесении небольших изменений.
При первом старте flutter проигрывает процентов на 30, потому что собирается и дартовый, и платформенный код, бандлится, запускается. Но дальше все становится сильно лучше, потому что из-за hot reload мы видим изменения во flutter-приложениях меньше, чем за секунду.
А вот платформенные приложения надо перезапускать. Для этого, в зависимости от положения луны и сложности изменений, нужно от трех до десяти секунд. И в процессе разработки, когда тебе приходится это делать постоянно, секунды складываются в достаточно значимое время.
Я говорил об этом с командой. По их субъективным ощущениям, они пишут код раза в три быстрее, чем на Native. Но вся моя команда пришла из Swift, они ничего не могут сказать про Android. Про него могу сказать, потому что какое-то время писал и на нем тоже.
И, да, у меня тоже есть ощущение, что все пишется раза в два с половиной — три быстрее. Я говорю не только про верстку UI. У нас вообще на dart, на flutter написано все: сеть, кеширование, базы. То есть это такая overall-метрика. С одной стороны, она субъективная, но с другой, дальше вы видите, что она достаточно объективна.
Мы померили скорость разработки фич по коммитам. Здесь я показываю две user story. Все stories занимают примерно шесть-восемь экранов. В случае фотоконтроля есть работа с камерой, в случае заработка — графики с анимациями. Красное — это Android, желтое — flutter на две платформы. В среднем получается, что на две платформы на flutter мы пишем в два с половиной раза быстрее, чем на Android. Профит очевидный.
Ложка дегтя
Может показаться, что все очень хорошо и во flutter нет никаких проблем. Они есть. Сейчас я про них расскажу.
Для flutter значительно меньше библиотек, чем для натива. Если посмотреть awesome-листы для Android, Swift и flutter, то для flutter awesome-лист раз в пять меньше, чем для Swift. Из того, с чем сталкивались мы, все очень плохо с шифрованием. Мы полностью импортировали свое шифрование из натива. Есть проблемы в единственном нормальном плагине камеры, его тоже подтачивали.
Для многого библиотек вообще не было. А если были, то, к сожалению, сделанные людьми изначально для себя, а потом выложенные в опенсорс. Оно было не общее, решало только конкретную частную задачу. И когда мы дописывали и делали пул-реквесты в эти библиотеки, авторы говорили: «Нам это уже не интересно, хотите, берите себе мейнтейнера и поддерживайте эту фигню дальше, мы это делать не хотим». Но мы возьмем себе мейнтейнеров и будем это поддерживать.
Основная проблема, с которой мы столкнулись, была в документации. Документации под сторонние, не гугловые библиотеки практически нет. Самые большие библиотеки под flutter внезапно китайские. Там есть документация, но на китайском. То есть ее, считайте, нет. Не знаю, почему они ее не переводят. Видимо, им это не нужно. Их тоже можно понять.
Поэтому нам приходилось читать достаточно много кода в процессе. Наверное, это хорошо, потому что это позволяло нам учиться. С чтением кода проблем особо нет, потому что все библиотеки достаточно молодые, кода в них пока не очень много. dart — достаточно приятный и экспрессивный язык, поэтому читаются легко, понятно, не как художественная литература. То есть ты не будешь читать его на ночь, но в рабочем процессе это не проблемно. И, да, сильно проще, чем читать код на каких-то нативных библиотеках. Кто-нибудь пробовал читать коды RX Swift, например? В dart всё сильно проще.
Какие-то либы, например тот же fish, мы описываем для себя внутри и планируем контрибьютить документацию, чтобы двигать сообщество и давать возможность для изучения другим людям. С самим языком и с самим flutter ситуация совершенно обратная. Это, наверное, лучшая документация, которую я видел в своей жизни. Там есть куча примеров, маленькие видосы, widget of the week и прочее. Google отлично подошел к этой истории, реально лучшая документация.
Из того, что для нас было важным, можно отдельно отметить отсутствие нормального приватного репозитория пакетов. pub — это аналог npm, CocoaPods для dart и flutter. Так как мы энтерпрайз, нам нельзя отдавать наружу все на свете, а нужно держать внутри приватные пакеты. И репозитория, который ты можешь держать внутри, нет. Это не очень удобно, потому что когда пакетов много, приходится через git tags их указывать в зависимостях. Это ведет к некоторому увеличению рутинной работы. Но Google движется в эту сторону. Они обещают скоро зарелизить.
Наверное, одна из основных проблем, которые у нас есть, — привлечение людей в команду. Я про это уже чуть-чуть говорил, но повторюсь. Да, очень легко конвертировать специалистов в разработчиков dart, flutter, если у них есть на это желание. Но люди не спешат конвертироваться, потому что технология достаточно молодая. Опять же, нет больших историй успеха. И многие говорят — сейчас я потрачу год, чтобы изучить эту штуку и стать в ней профессионалом, потом ее закопают, и я никому не буду нужен, пропущу год в нативной разработке.
Мы очень надеемся, что наша история успеха и озвученное намерение продолжать этим заниматься поможет изменить общественное мнение, и больше народа начнет писать на этой чудесной технологии. Спасибо.