Отбираем хлеб у нативных разработчиков: миграция с Kotlin/Swift на RN

от автора

Меня зовут Александр Чернов, я фронтенд-разработчик в KODE и я использую React Native в разработке мобильных приложений уже более семи лет. Сейчас расскажу вам, как мы у нативных разработчиков хлеб отбирали.

Однажды к нам пришел заказчик с MVP от другой команды. Это были iOS и Android-приложение. iOS-приложение было написано на Swift, сверстано в сториборде, с архитектурой MVC (Massive Model View Controller). Android-приложение было написано на Kotlin, сверстано в XML. Архитектура отсутствовала — только два слоя, data и UI. Причем Activity или Fragment из UI-слоя содержали в себе всю бизнес-логику, которая зачастую дублировалась.

При ревью кодовой базы мы поняли, что в дальнейшем поддерживать MVP и добавлять новую функциональность будет больно. Поэтому решили основательно доработать продукт, чтобы при масштабировании бизнеса и внедрении новых фичей заказчику не пришлось переписывать код.

Для реализации мы рассматривали такие варианты:

  • Поддерживать нативное приложение, отрефакторить его и в дальнейшем дорабатывать на Kotlin/Swift. Но в таком случае бюджет проекта увеличился бы, поскольку заказчику нужно было бы задействовать iOS и Android-разработчиков.

  • Остановить поддержку Kotlin/Swift и разрабатывать приложение на React Native с нуля. Для заказчика этот вариант не подходил, так как для стартапа было важно быстрее получить готовый функционирующий продукт и начать разработку новых фич.

  • Поддерживать нативное приложение на Kotlin/Swift и постепенно переписать на React Native.

Мы обсудили перспективы с заказчиком и остановились на последнем варианте — плавном переезде на RN. У него было сильное преимущество: ранее на другом проекте мы уже опробовали React Native с этим заказчиком, и он остался доволен результатом. К тому же, у нас была уже собрана команда и наработана база, поэтому заказчику не пришлось заново проходить эти процессы и увеличивать стоимость проекта.

Как мы «переезжали»

  1. Потушили пожары

Сначала мы с минимальными усилиями сделали ребрендинг приложения, скрыли лишний функционал и исправили множество багов. Например, столкнулись с проблемой несогласованных моделей данных между клиентом и сервером. Также в приложении на Android отсутствовал некоторый функционал, который был на iOS — например, Google reCAPTCHA. 

В общем, мы провели минимальный рефакторинг и выпустили приложение в TestFlight, чтобы бизнес мог с ним работать и параллельно проходить проверку в сторах.

  1. Начали внедрять React Native

У React Native есть все необходимые инструменты и подробная документация, чтобы внедрять его в уже существующие нативные приложения.

Минимальная сущность, в которую рендерится RN-приложение для iOS — View, а для Android — Fragment. Зная это, я написал набор утилит, которые позволяют буквально в пару строчек запустить React-приложение. Получилось довольно лаконично, достаточно передать имя начального роута.

Android:

class RNDepositsFragment: RNFragment (   options = object : RNFragmentOptions () {     override fun getInitialRoute(): String {       return "deposits"     }   } )

iOS:

class RNDepositsViewController : RNViewController {  override var options: RNViewControllerOptions {    return RNDepositsOptions()  } }  struct RNDepositsOptions : RNViewControllerOptions {  func getInitialRoute() -> String {    return "deposits"  } }

Если возникает необходимость передать дополнительные данные, с этим также не возникает проблем — в JS-слое регистрируем компонент App,

AppRegistry.registerComponent('appName', () => App);

который принимает переданные из нативного слоя свойства.

const App = ({    authToken,    baseUrl,    initialScreenName,    …other  }: AppInitialProps) => { … }

Для управления нативной навигацией по экранам, а также передачи данных между RN-представлением и нативным слоем был написан Native Module, который мы назвали AppBridge (см. схему ниже).

Пример AppBridge-модуля для Android
class AppBridgeModule(   private val reactContext: ReactApplicationContext ) : ReactContextBaseJavaModule(reactContext) {     private val eventEmitter = AppBridgeEventEmitter(reactContext)     override fun getName(): String = MODULE_NAME      @ReactMethod     fun logout() {       ...     }      // пример авторизации нативной части приложения     // React-приложение отправляет токен в натив     @ReactMethod     fun login(token: String, promise: Promise) {       ...     }      // пример главного экрана     @ReactMethod     fun openHomeScreen() {         val intent =             Intent(reactContext, HomeActivity::class.java)         intent.addFlags(           Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK         )         reactContext.currentActivity?.startActivity(intent)     }      // Отправка ивентов с пейлоадом в RNFragment     // Например в RN представлении пользователь      // нажимает кнопку "назад",      // но нам нужно выполнить не навигационный переход внутри RN-приложения,      // а закрыть целиком Activity     @ReactMethod     fun send(eventName: String, payload: ReadableMap?) {         UiThreadUtil.runOnUiThread {           // RN - это проектное решение, singleton-объект            // для доступа к React-native представлениям           RN.shared.onEvent(eventName, payload);         }     } } 

Визуально это выглядит так. Нативный слой доминирует и занимает всю площадь экрана, нижний таббар тоже нативный. Но контент внутри таба — это React-приложение со своими моделями данных, навигацией, сетевым слоем и так далее.

Напомню, что в Android основной экран с таб-навигацией — Activity, а каждый таб — Fragment. Мне достаточно было добавить новый или заменить существующий на RN фрагмент, чтобы при переходе на таб запускалось RN-приложение с нужным флоу. Очень просто. В iOS тоже самое: UITabBarController для каждого таба рендерит UIViewController, мы также легко можем добавлять новые или изменять текущие контроллеры на RNViewController (см. код выше). 

Как это работает под капотом: нативный слой запускает React Native-представление, которое запускает React-приложение с начальными параметрами, такими как токен авторизации, ссылка на API, имя роута и другие.

Как все работает

Как все работает

Как видно из схемы, React-приложение может асинхронно обмениваться данными с нативным слоем и непосредственно с RN-представлением в обе стороны через AppBridge (Native Module). 

Также можно заметить, что все RN-представления открывают одно и то же React-приложение, просто с разными входящими параметрами. Слой React Native намеренно сделан моноприложением, так как конечной целью было оставить одну точку входа. Но ничто не мешает организовать и микроприложения, чтобы у каждого из них была своя песочница и не было общего контекста. А RN Utils — это набор самописных утилит для работы с RN, туда входят классы RNFragment, RNViewController и другие.

  1. Мигрировали на React Native

Следующим шагом была замена флоу авторизации/регистрации. Теперь React Native получает авторизационный токен, сохраняет его, рефрешит и передает в нативный слой. Также управляет биометрией, логаутом и т.д. 

А еще мы добавили CodePush — киллер-фичу React Native, которая позволяет обновлять мобильное приложение без прохождения проверок в сторах.

Постепенно заменили все табы и отдельные Activity/UIViewController на RN-представления. Но основная навигация по приложению — неавторизованная зона, флоу авторизации, основной экран с табами и другие экраны, до сих пор осуществляется в нативном слое. 

Наконец, остался последний нативный раздел. Прежде чем переделывать его, мы доработали приложение так, чтобы React Native начал доминировать. Получилось полноценное RN-приложение, запущенное в одном Activity/UIViewController, в котором вся навигация осуществляется через react-navigation. Оставшийся раздел я завернул в Native UI Component (документация Android, документация iOS) и использую его в React-приложении.

Финальным шагом мы переделали последний нативный раздел на RN и выпилили оставшийся мусор. Теперь это полностью React Native приложение, как будто мы написали его с нуля.

Преимущества и недостатки подхода

React Native — крутая и зрелая технология, которая хорошо себя проявляет. Забегая вперед, скажу, что при ее внедрении в существующее приложение я не встретил непреодолимых проблем, а процесс был подробно описан в документации.

Рассмотрим преимущества и недостатки нашего подхода.

Преимущества

  • Нон-стоп. Мы бесшовно переписали приложение, улучшили пользовательский опыт, кодовую базу и поддерживаемость продукта, и при всем этом не тормозили бизнес.

  • Снижение стоимости разработки. При переходе на кроссплатформу над проектом работает одна команда вместо двух, то есть вместо iOS и Android — React Native. 

  • Увеличение скорости разработки. Инструменты для React Native очень развиты и позволяют быстро разрабатывать приложения. В Storybook можно сверстать приложение практически полностью и посмотреть, как оно будет выглядеть. Плюс hot-reload — мы пишем код, он сразу же обновляется и мы видим результат. Тот же самый CodePush — очень крутая вещь.

Недостатки

  • Проблемы, с которыми мы бы не столкнулись при обычной разработке. Например, работа с диплинками или кнопкой «Назад» на Android. Когда на экране два слоя — нативное приложение и React Native, они оба начинают обрабатывать эти действия, происходят некоторые конфликты и приходится тратить время на их решение.

  • Высокий порог входа для React Native-разработчика. Помимо React Native, ему нужно неплохо разбираться в нативных платформах, писать нативный код и в целом понимать, как все работает.

P.S. Возможно, вы хотите спросить, где исходники RNViewController, RNFragment и RN Utils. Это были временные, узкоспециализированные решения, которые помогли нам переехать на RN, а после переезда потеряли ценность и были удалены из проекта. Поэтому исходники мы показать не можем.

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/865962/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *