Як ми інтегрували React Native у наявний Android застосунок. Розглядаємо реальний кейс

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всім привіт 👋 Мене звати Андрій, я Engineering Manager та Android Engineer в компанії Uptech. Вже понад 5 років я створюю Android-застосунки, щоб заощаджувати час людей та підвищувати фінансові показники різних продуктів. Мені часто доводиться стикатись з чимось незвичним для Android-інженерів.

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

Більшість статей, які я читав про React Native, пропонують схожий підхід: створити монорепозиторій з конфігураціями за замовчуванням і далі вже працювати над основною функціональністю застосунку.

А якщо, наприклад, потрібно реалізувати частину мобільного застосунку нативною мовою, а частину на React Native? Може здатися, що це жарт із попсового серіалу про програмістів. Втім, це реальний кейс із практики нашої команди. Дуже часто бізнес говорить фінансовою мовою, і якщо є хоча б якась технічна можливість покращити ефективність та швидкість розробки — бізнес використовує її, і це абсолютно логічно.

В яких випадках може знадобитися підхід інтеграції React Native в наявний Android-застосунок:

  • ви хочете пришвидшити розробку Android, iOS та Web, проте у вас вже є застосунки, і вам необхідно робити перехід на нову технологію більш плавно;
  • у вашій команді з’явились сильні React Native спеціалісти, які можуть забезпечити стабільність нової функціональності.

Історія одного розробника...

Одного дня наш клієнт вирішив провалідувати гіпотезу інтеграції React Native в наявний застосунок. Як Android-девелоперам нам необхідно було не писати код на JS, а просто виконати інтеграцію в наявний застосунок. Недовго думаючи, ми висвітлили клієнту всі технічні та продуктові ризики, які може спричинити інтеграція, та пройшли всі етапи моделі Кюблера-Росса.

Спочатку ми повністю заперечували цей підхід, адже він має багато недоліків: ускладнення деплоймент-процесів, ускладнення підтримки кодової бази тощо. Потім команда гнівалася. Після цього ми перейшли до торгів і надавали експертну оцінку, де зважували всі «за» та «проти» обраного підходу. Зрозумівши, що є шанс, що короткостроково це може бути фінансово невигідним для продукту, проте довгостроково може бути виграш, команда прийняла та почала інтеграцію.

Метою експерименту було визначити, наскільки можна покращити фінансові показники, адже за такого підходу розробка велась би в одній кодовій базі. Це означає, що необхідно мати одного розробника (React Native) замість трьох (Android, iOS, Web) і кожна продуктова зміна велась би лише один раз замість трьох. Перевірка мала відбуватись на одній з частин застосунку, а саме на Signup Flow.

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

Проблема № 1. Керування репозиторіями

Звичайний підхід, який ви можете зустріти в одному з мільйонів гайдів з розробки React Native застосунку — це створення монорепозиторія. Логічно це означає, що розробка над Android, iOS та Web відбувається в одному репозиторії.

Це абсолютно нормально при розробці мобільних застосунків за допомогою фреймворка React Native, оскільки весь застосунок зазвичай пишеться мовою JavaScript/TypeScript та більшість пул-реквестів стосуватимуться лише однієї кодової бази.

Проте якщо потрібно вести Android-розробку паралельно React Native розробці, матимемо наступні проблеми:

  • список пул-реквестів завжди буде переповненим, це додасть складності в процес розробки;
  • будь-яка зміна в конфігурації буде впливати на розробку всіх платформ, особливо під час критичних релізів;
  • перенесення наявного застосунку в інший репозиторій — це тривалий процес, що потребує зупинки роботи всіх розробників над поточними задачами. Також це може призвести до видалення git історії;
  • монорепозиторій міститиме повноцінні нативні застосунки та лише частину функціональності у вигляді React Native.

Вирішення проблеми № 1

Звичайно, потрібно тримати репозиторії мобільних застосунків окремо від React Native коду. При цьому рекомендую робити React Native репозиторій як монорепо, оскільки можна буде тестувати окрему частину функціональності, що написана на React Native, в режимі standalone. У такому випадку ми не матимемо тих проблем, які б мали при створенні монорепозиторія для всіх платформ. Як це можна реалізувати? Є два варіанти, які працюють фактично однаково:

  • Робити імпорт репозиторія з React Native кодом, як підмодуль (git submodule). Це потужний механізм, який може зробити зв’язку двох репозиторіїв чистішою.
  • Мануально клонувати репозиторій в директорію з Android-проєктом, або ж будь-яку іншу директорію. Головне, щоб репозиторій клонувався стабільно в ту саму директорію, оскільки потрібно буде правильно встановити зв’язок в gradle файлах.

Проблема № 2. Генерація релізних та демонстраційних білдів

Мені, як Android-розробнику, було незрозуміло, де розташований JS код та як я можу його додати в кінцевий .aab/.apk файл. Відповідь виявилась очевидною: файл index.bundle, який можна згенерувати за допомогою звичайної команди в React Native репозиторії:

$ react-native bundle --platform android --dev false --entry-file packages/signup/index.js --bundle-output path-to-assets/assets/index.android.bundle --assets-dest path-to-app/src/main/res/"

Ця команда створить index.bundle файл та запише його в директорію assets з вашим Android-проєктом. Також, якщо React Native містить будь-які файли ресурсів (картинки, мелодії, тощо), він автоматично запише їх у папку res.

Врахуйте, що path-to-assets та path-to-app будуть різні для кожної конфігурації та залежатимуть від того, де розташований React Native проєкт (підмодуль, клонований у папку з визначеним розробником шляхом).

Проте, як ви розумієте, проблема не в тому, щоб створити файл index.bundle. Проблема з’являється тоді, коли вам необхідно згенерувати різні .aab/.apk файли для різних середовищ, на кшталт production та staging.

Вирішення проблеми № 2

По-перше, ваш Android-проєкт повинен містити вже сконфігуровані build variants. По-друге, частина застосунку, що написана на React Native, повинна містити будь-який механізм роботи з різними типами середовищ. Ми, наприклад, використовували бібліотеку react-native-dotenv для конфігурації середовищ. Усе, що потрібно було змінювати — лише .env файл, що розташований у корені React Native проєкту.

Щоб кожного разу не виписувати конфіденційні дані (токени, ключі) в .env файл вручну, був створений react_env_generator.sh скрипт, що брав необхідні ключі з .gradle/gradle.properties файлу (або ж environment variables сховища на СІ) та переносив їх в .env файл у необхідному для React Native частини застосунку форматі.

Приклади скриптів ви можете знайти в наступних GitHub gist-ах.

  1. Генератор змінних середовища, що отримуються локально з gradle.properties файлу
  2. Генератор змінних середовища, що отримуються з змінних СІ

Тепер ми знаємо, що середовище React Native частини залежить лише від .env файлу, тому щоб зробити два різних .aab/.apk файли (production та staging), нам потрібно лише:

  1. Згенерувати .env з production ключами генератором, після чого створити index.bundle файл командою, описаною вище.
  2. Згенерувати .env з staging ключами генератором, після чого створити index.bundle файл командою, описаною вище.

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

Ви хочете запитати, чому? Відповідь дуже неочевидна. React Native фреймворк робить кешування .env файлів при послідовній генерації бандлів і, щоб запобігти цьому, нам потрібно додати прапор reset-cache в команду генерації index.bundle файлу. У результаті маємо:

$ react-native bundle --platform android --dev false --reset-cache --entry-file packages/signup/index.js --bundle-output path-to-assets/assets/index.android.bundle --assets-dest path-to-app/src/main/res/"

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

Проблема № 3. Навігація

Є безліч способів організації навігації в межах React Native застосунку. Серед них: React Navigation, React Native navigation від Wix, React Native Router Flux та багато інших. Було б круто, якби React Native розробники доповнили цей список у коментарях з описом підходів, які їм найбільше подобаються для різних випадків.

Проте не забуваймо, що ми говоримо про випадок, коли в нас лише частина застосунку написана на React Native, а решта написана нативною мовою програмування. І навігацію між React Native та Native частиною трошки складніше організувати, особливо коли необхідно мати механізм передачі даних. Якщо React Native розробник не дуже досвідчений у нативній розробці, бізнесу необхідно буде залучити нативних розробників (що збільшує виробничі витрати), адже потрібно створити міст між React Native кодом і нативним у Android-застосунку.

Вирішення проблеми № 3

Щоб коректно побудувати навігацію між нативною частиною застосунку та React Native стороною, необхідно побудувати міст, про деталі реалізації та тестування якого ви можете почитати в офіційній документації на reactnative.dev. Я постараюсь коротко описати основні компоненти, які необхідно додати для побудови навігаційного моста.

Android

Перш за все я рекомендую розбити застосунок на модулі. Якщо у вас застосунок уже містить модулі, то створіть окремий для запуску частини React Native. Якщо ж застосунок не розбитий на модулі — створіть 2 модулі: native-application та react-native-flow. Це дозволить вашій gradle системі не робити заново білд усіх залежностей react native у випадку, коли ви будете робити зміни у нативній частині коду.

  1. Ствоюємо клас, що наслідується від ReactContextBaseJavaModule та створюємо методи, які ми будемо викликати з ReactNative сторони. Не рекомендується змінювати назви методів, оскільки в такому випадку доведеться це змінювати і в React Native репозиторії:
  2. class NativeNavigationModule constructor(private val navigator: ReactNativeSignupNavigation, reactContext: ReactApplicationContext)
     : ReactContextBaseJavaModule(reactContext) {
    
     override fun getName(): String = "NativeNavigation"
    
    
     @ReactMethod
     fun navigateToLoginScreen(email: String?) {
          navigator.logoutAndNavigateToLoginScreen(currentActivity, email)
     }
    
     @ReactMethod
     fun navigateToWelcomeScreen() {
          navigator.logoutAndNavigateToWelcomeScreen(currentActivity)
     }
    
    }
    
    У прикладі вище ви бачите об’єкт navigator: ReactNativeSignupNavigation. Це клас, що надає нам Dagger (чи будь-який інший контейнер залежностей) в наш react-native-flow модуль. Його реалізація розташована в модулі app, оскільки лише в цьому модулі є доступ до екранів з native-application модуля.
    interface ReactNativeSignupNavigation {
        fun logoutAndNavigateToLoginScreen(currentActivity: Activity?, email: String?)
        fun logoutAndNavigateToWelcomeScreen(currentActivity: Activity?)
    }
    
  3. Створюємо package клас, що наслідує ReactPackage зі стандартної бібліотеки ReactNative, де створюємо екземпляр класу, описаного в попередньому пункті:
  4. class NativeNavigationPackage constructor(private val navigator: ReactNativeSignupNavigation): ReactPackage {
        override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(NativeNavigationModule(navigator, reactContext))
    
        override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<View, ReactShadowNode<*>>> = Collections.emptyList()
    }
    
  5. При створенні екземпляру react application передаємо наш package, що створили в попередньому пункті:
  6. val reactInstanceBuilder = ReactInstanceManager.builder()
         .setApplication(application)
         .setCurrentActivity(this)
         .setJSMainModulePath("packages/signup/index.js")
         .addPackage(MainReactPackage())
         ...
         .addPackage(NativeNavigationPackage(reactSignupNavigator))
         .setUseDeveloperSupport(BuildConfig.DEBUG)
         .setInitialLifecycleState(LifecycleState.RESUMED)
    

React Native

Сворюємо файл (наприклад NativeNavigation.js), який буде отримувати методи, створені в нативному коді:

import { NativeModules } from 'react-native';

export default NativeModules.NativeNavigation;

Далі в тому місці, де необхідно зробити навігацію, просто викликаємо наш створений метод:

import NativeNavigation from '../../services/Native/NativeNavigation';

             ...

NativeNavigation.navigateToLoginScreen(user.email);

             ...

Ось і все, тепер ми успішно зробимо навігацію з React Native частини в Native.

Якщо ж нам потрібно зробити навігацію навпаки — з нативної частини в React Native, то необхідно запустити Activity, що наслідується від ReactActivity, з необхідними параметрами, які визначатимуть поточний стан ReactNative частини застосунку:

class SomeReactNativeScreen : ReactActivity() {

 private var reactInstanceManager: ReactInstanceManager? = null
 @Inject lateinit var reactNavigator: ReactNativeSignupNavigation

 override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   AppInjector.provideReactSignupComponent<ReactSignupComponent>()?.inject(this)

   setupReactInstance()

   val targetScreenEntryPoint = intent?.getStringExtra(TARGET_SCREEN_ENTRY_POINT) ?: ""

   setContentView(getReactRootView(targetScreenEntryPoint))
 }

 private fun setupReactInstance() {
   val reactInstanceBuilder = ReactInstanceManager.builder()
     .setApplication(application)
     .setCurrentActivity(this)
     .setJSMainModulePath("packages/signup/index.js")
     .addPackage(MainReactPackage())
     .addPackage(NativeNavigationPackage(reactNavigator))
     .setUseDeveloperSupport(BuildConfig.DEBUG)
     .setInitialLifecycleState(LifecycleState.RESUMED)

   if (BuildConfig.BUILD_TYPE != "debug") {
     reactInstanceBuilder.setBundleAssetName("index.android.bundle")
     reactInstanceBuilder.setJSBundleFile("assets://index.android.bundle")
   }

   reactInstanceManager = reactInstanceBuilder.build()
 }

 private fun getReactRootView(targetScreenEntryPoint: String): ReactRootView {
   val result = ReactRootView(this)

   result.startReactApplication(reactInstanceManager, "signup", Bundle().apply {
     putString("ENTRY_POINT", targetScreenEntryPoint)
     putString("SOME_DATA", "value")
   })

   return result
 }

  companion object {
   const val TARGET_SCREEN_ENTRY_POINT = "SomeReactNativeScreen.TargetScreenEntryPoint"
 }
}

Проблема № 4. Continuous Integration

Нині в світі без автоматизації ніяк, саме тому весь проєкт разом із React Native частиною необхідно якось запустити на CI. Оскільки ми використовуємо CircleCI, приклади наводитиму саме для цієї СІ, проте не думаю, що буде проблемно змінити конфігурацію на будь-яку іншу СІ. Всі jobs я не описуватиму, лише основні.

Варто уточнити, що ми не використовуємо підмодулі, а напряму клонуємо React Native репозиторій у кореневу папку з Android-проєктом. Чому саме так? Просто тому, що так склалось на проєкті в умовах швидкої інтеграції. Ви можете робити так само або з використанням підмодулів, різниця буде несуттєвою.

Вирішення проблеми № 4

  1. Build react native job:
  2. build-react-native:
        <<: *container_config
        steps:
          - checkout
          - run:
              name: Clone React Native repository
              command: git clone  git@github.com:YOUR_PROJECT/YOUR_REACT_NATIVE_REPOSITORY.git ~/YOUR_PROJECT_FOLDER_NAME
          - restore_cache:
              key: yarn-v1-{{ checksum "~/YOUR_PROJECT_FOLDER_NAME/yarn.lock" }}-{{ arch }}
          - restore_cache:
              key: node-v1-{{ checksum "~/YOUR_PROJECT_FOLDER_NAME/package.json" }}-{{ arch }}
          - run:
              command: "cd ~/YOUR_PROJECT_FOLDER_NAME && yarn install"
              name: "Install react native dependencies"
          - save_cache:
              key: yarn-v1-{{ checksum "~/YOUR_PROJECT_FOLDER_NAME/yarn.lock" }}-{{ arch }}
              paths:
                - ~/.cache/yarn
          - save_cache:
              key: node-v1-{{ checksum "~/YOUR_PROJECT_FOLDER_NAME/package.json" }}-{{ arch }}
              paths:
                - ~/YOUR_PROJECT_FOLDER_NAME/node_modules
          - run:
              name: Create folders for react native bundles
              command: |
                mkdir -p ~/tmp/stageBundle
                mkdir -p ~/tmp/prodBundle
                sudo chmod u+x ./scripts/react_env_generator.sh
          - run:
              name: Generate react Staging env variables
              command: |
                ./scripts/react_env_generator.sh stage
          - run:
              name: Make react native stage bundle
              command: "cd ~/YOUR_PROJECT_FOLDER_NAME/packages/signup && yarn android-ci-stage-bundle"
          - run:
              name: Clean React Native cache
              command: |
                cd ~/YOUR_PROJECT_FOLDER_NAME
                rm -rf $TMPDIR/metro-cache
                rm -rf $TMPDIR/react-native-packager-cache-*
                rm -rf .env
                rm -rf packages/signup-web/.env
                yarn cache clean
          - run:
              name: Generate react Prod env variables
              command: |
                ~/code/scripts/react_env_generator.sh prod
          - run:
              name: Make react native prod bundle
              command: "cd ~/YOUR_PROJECT_FOLDER_NAME/packages/signup && yarn android-ci-prod-bundle"
          - persist_to_workspace:
              root: *workspace_root
              paths:
                - YOUR_PROJECT_FOLDER_NAME
                - tmp
                - code
    
  3. Deploy stage build to Firebase App Distribution (ми завантажуємо білди за допомогою Fastlane):
  4. stage:
        <<: *container_config
        steps:
          - *attach_workspace
          - run: yes | sdkmanager --licenses || exit 0
          - run: yes | sdkmanager --update || exit 0
          -
            run:
              command: gem install bundler && bundle install && bundle update fastlane
              name: configure build
          -
            restore_cache:
              <<: *general_cache_key
          - run:
              name: Use stage react native bundle
              command: |
                rm -rf ~/code/App/app/src/main/assets/
                mkdir -p ~/code/App/app/src/main/assets/
                cp ~/tmp/stageBundle/index.android.bundle ~/code/App/app/src/main/assets/
          -
            run:
              name: "Install Firebase CLI"
              command: |
                curl -sL firebase.tools | bash
          -
            run:
              name: "Deploy stage build to Firebase"
              no_output_timeout: 30m
              command: |
                cd App/ && bundle exec fastlane stage firebase_token:$FIREBASE_TOKEN 
    
          - store_artifacts:
              path: ~/code/App/app/build/outputs/apk/debug/stage/app-stage-debug.apk
    
  5. Release app into Google Play store
  6. deploy:
        <<: *container_config
        steps:
          - *attach_workspace
          - run: yes | sdkmanager --licenses || exit 0
          - run: yes | sdkmanager --update || exit 0
          -
            run:
              command: gem install bundler && bundle install && bundle update fastlane
              name: configure build
          -
            restore_cache:
              <<: *general_cache_key
          - run:
              name: Use prod react native bundle
              command: |
                rm -rf ~/code/App/app/src/main/assets/
                mkdir -p ~/code/App/app/src/main/assets/
                cp ~/tmp/prodBundle/index.android.bundle ~/code/App/app/src/main/assets/
          -
            run:
              name: "Generate signed aab prod bundle"
              command: |
                cd App/ && bundle exec fastlane sign_aab_lane flavor:Prod type:Release
          -
            run:
              name: "Fastlane deploy"
              no_output_timeout: 30m
              command: |
                cd App/ && bundle exec fastlane deploy_aab path:app/build/outputs/bundle/prodRelease/app-prod-release.aab mapping:app/build/outputs/mapping/prodRelease/mapping.txt
    

Зміни у процесах команди і продукту в цілому

Комунікація

До інтеграції React Native в нативні застосунки ми мали окремі команди Android, iOS та Web девелоперів. Якщо візуалізувати, то наша проєктна команда виглядала приблизно так:

Після інтеграції наша проєктна команда стала виглядати приблизно так:

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

Git

Щодо змін у Git процесах, після інтеграції React Native в нативний застосунок потрібно розуміти, що тепер у вас не один репозиторій, а декілька. Кожна зміна в React Native іншими розробниками може впливати на поведінку Android-застосунку. Саме тому необхідно, щоб React Native розробники тримали принаймні одну вітку стабільною. Це можуть бути як декілька різних віток для різних застосунків (Android/iOS), так і різні вітки для різних flow (signup/login). Нам, як Android-інженерам, необхідно слідкувати за стабільними вітками, особливо перед релізами. В іншому разі в один прекрасний день ви не зможете збілдити ваш проєкт, вже не кажучи про те, що частина функціональності не працюватиме.

Release management

Release management після інтеграції повинен включати комунікацію нативних розробників з React Native задля забезпечення наявності свіжих змін у всіх частинах застосунку. Перед релізом необхідно зібрати номери всіх тікетів (нативних та React Native) задля генерації свіжих release notes.

Issue tracking

Перед інтеграцією необхідно чітко визначити, де будуть розташовані тікети мобільних розробників, а де будуть розташовані тікети React Native. Дуже часто виникає ситуація, коли потрібно на React Native частині виправити помилку, яка існує лише на Android (наприклад, навігація назад за допомогою фізичної кнопки «назад»). У таких випадках не зрозуміло, на якій дошці повинен бути тікет, і лише чітке розмежування з самого початку інтеграції дозволить вам вирішити такі питання.

Також, якщо ви рахуєте velocity команди, необхідно визначити з самого початку як її рахувати для тих розробників, які працюватимуть над інтеграцією React Native. Звичайно, все залежить від проєкту, але один із варіантів трекінгу задач для React Native частини — це створення окремої дошки для React Native задач. У такому випадку вся робота, яка відбуватиметься на React Native частині, буде відслідковуватися на окремій дошці, що спрощує velocity calculation.

Onboarding

Нові розробники не повинні бути Альбертами Ейнштейнами для того, щоб увійти в курс справ проєкту. Навіть з такою дивною технічною інтеграцією процес онбордингу має бути суперпростим. Для цього я рекомендую створити набір команд, який робитиме весь процес налаштування React Native репозиторія на машинах розробників автоматично. Ось приклад одного з таких налаштувань:

fun Project.setupReactNativeRepository() {
    runCommand("""
          rm -rf ../../../RELATIVE_PATH_TO_REACT_NATIVE_PROJECT
          mkdir ../../../RELATIVE_PATH_TO_REACT_NATIVE_PROJECT
          git clone https://github.com/YOUR_PROJECT/YOUR_REACT_NATIVE_REPOSITORY.git ../../../RELATIVE_PATH_TO_REACT_NATIVE_PROJECT
          chmod a+x ../../scripts/react_env_generator.sh
          ${getEnvGeneratorCommand()}
          rm -rf src/main/assets
          mkdir src/main/assets
          cd ../../../RELATIVE_PATH_TO_REACT_NATIVE_PROJECT && yarn clean
          ${getAndroidBundleCommand("../../../RELATIVE_PATH_TO_REACT_NATIVE_PROJECT/packages/signup")}
  """)
}

fun Project.getEnvGeneratorCommand(): String {
    return if (gradle.startParameter.taskNames.any { it.contains("Prod", true) }) {
      "../../scripts/react_env_generator.sh prod"
    } else {
      "../../scripts/react_env_generator.sh stage"
    }
}

fun Project.getAndroidBundleCommand(path: String): String {
    println(gradle.startParameter.taskNames)
    return if (!gradle.startParameter.taskNames.any { it.contains("Debug", true) }) {
      "cd $path && yarn android-bundle"
    } else {
      "echo \"To test react native flow you should run metro bundler by your own\""
    }
}

// a wrapper closure around executing a string
// can take either a string or a list of strings (for arguments with \n)
fun Project.runCommand(strList: String) {
    val lines = strList.split("\n")
    lines.forEach {
      if (it.isNotBlank()) {
        try {
           project.exec { commandLine("bash", "-c", it.trim()) }
        } catch (e: org.gradle.process.internal.ExecException) {
      }
    }
}

Виклик даного методу відбувається в build.gradle.kts вашого react-native модуля відразу після опису плагінів:

try {
    apply("../../../RELATIVE_PATH_TO_REACT_NATIVE_PROJECT/node_modules/react-native/react.gradle")
} catch (ignored: Exception) {
    setupReactNativeRepository()
}

Висновки

Як ви вже помітили, інтеграція React Native коду в наявний Android-застосунок — це складна задача, яка потребує багато зусиль та часу на реалізацію не лише на початкових етапах, а й при подальшій розробці. Робити таку інтеграцію чи ні — вирішувати вам. Такий підхід може бути релевантним, якщо нативні проєкти досить великі та повний перехід займає досить багато часу. В такому випадку нова функціональність може писатись на React Native, при тому паралельно в окремих модулях можна переписувати стару функціональність. Такий метод переходу на нову технологію є надійнішим в плані якості розробки.

Зі своєї сторони я опишу переваги та недоліки такого підходу.

Переваги

  • Встановлення виробничих процесів та налаштування зв’язки React Native <-> Native — це лише одноразова задача, яка робиться на початку інтеграції
  • Це може пришвидшити розробку як мінімум частини функціональності застосунків
  • Компоненти застосунку, які виглядатимуть однаково
  • Поведінка застосунку буде однаковою на всіх платформах
  • Instant feature reload and hot reload options замість компіляції значно пришвидшує розробку

Ресурси:

Недоліки

  • Потрібно декомпозувати логіку між нативною частиною та React Native. Особливої уваги заслуговують такі частини, як deeplinks, navigation, push notifications, тощо
  • Розмір кінцевого .aab/.apk файлу швидше за все збільшиться, що може негативно вплинути на acquisition ваших користувачів
  • Час компіляції виросте, навіть якщо ви виділите React Native flow в окремий модуль
  • Складнощі при онбордингу на проєкт, необхідно виділяти багато часу на написання технічної документації
  • Зміни виробничих процесів, що впливають на velocity команди
  • Стає складніше керувати релізами

Ресурси:

Чи призвело це до підвищення ефективності на нашому проєкті? Неможливо сказати точно, оскільки velocity calculation збився через короткий таймлайн та процесуальні зміни в команді. Проте тепер ми майже не витрачаємо час на підтримку та функціональні зміни в signup flow, крім змін у навігації. За це тепер відповідає лише React Native команда.

Чи рекомендуємо ми інтегрувати React Native для частини застосунку? Ні, краще розпочинати розробку паралельно та намагатися наздогнати нативну розробку. Це короткострокові інвестиції, які потенційно (залежить від кейсу) можуть окупитись у майбутньому за рахунок зменшення команди нативних розробників або ж їх переводу в режим підтримки нативної функціональності.

👍НравитсяПонравилось11
В избранноеВ избранном4
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

Зачем так жить?
Реакт только добавляет головной боли: увеличивает время билда, размер апк, утечек памяти — море, дебажить сложно, инструментов в ide мало, это javascript, сетап проекта у нового дева занимает минимум день, так как начинаются какие-нить конфликты c вашим и принятым в проекте руби/xcode tools/node.js/RN, а нативный проект на гредле заводится за 20 минут. В итоге разбираться в этом не хочется, — лучше смотреть в сторону флаттера, с точки зрения нативного разработчика. Сам через это прошел.
Короче, увольняйтесь и ищите новый проект, вакансий полно.

А навіщо жити, якщо протягом більшої частини часу ганяти JSON файли на бек? Навіщо жити, щоб верстати примітивні UI? Звільнятись не планую, тому що мені важливі challenges і робити щось, що до мене мало хто робив, чим ми й займаємось в Uptech. Наврядчи я це знайду хоча б десь зі списку тих вакансій, яких повно.

Справедливості ради, моя інженерна команда не наполягала на даному технічному рішенні, проте, як я й написав в статті, бізнес говорить фінансовою мовою. Ми прекрасно розуміли всі ті проблеми, які ви описали вище. Флаттер — теж не silver bullet, та має свої переваги та недоліки.

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

Якщо це грамнотно розділити на модуль можна значно пришвидшити інкрементальний білд. До речі в нас на проекті час білду 3хв. До розділення дійсно був 10хв.

сетап проекта у нового дева занимает минимум день

Сетап проекта й без React Native може займати день, все залежить від якості онбординг докуменації та скриптів для автоматизації.

А навіщо жити, якщо протягом більшої частини часу ганяти JSON файли на бек? Навіщо жити, щоб верстати примітивні UI?

А как ReactNative решает эту проблему?

Наоборот для Standalone приложений которые независимые от сервера и всяких толстые клиентов предпочтительнее нативная разработка так как бриджи начинают начинают составлять практически все приложение.

практика показывает, что решения на реакте делают те, кто имеет ограниченный бюджет и хочет сильно сэкономить на разработчиках. Но по итогу придётся всё равно выбросить и переписать с нуля нативно.
Ну а с точки зрения пользователя ненативный UI очень легко отличим, а на андроиде ещё и выделяется тормозами.

На різні продукти різні клієнти. Ми теж не купуємо завжди преміум речі.

Дякую за сттатю. Хороший приклад про комунікацію 🤣

Цікава стаття, дякую. На практиці лише бачила, що просто переписують на React Native додаток з нуля як новий проект.

та реакт нейтив — это треш, неудивительно что простейшие хелловорлды уже по 100 МБ весят, и гуи тормозит на простейших эффектах, то что формошлепам впадло изучить нормальные языки для написания мобильных аппликух

гуи тормозит на простейших эффектах

Кто-то двадцать лет просидел в бункере, и не слышал про reactnative.dev/docs/animated ?

формошлепам впадло изучить нормальные языки для написания мобильных аппликух

Есть случаи, когда одна и та же логика должна одинаково работать на Вебе и двух мобильных платформах. Не менее тупо писать эту логику три раза под разные языки программирования и платформы.

Телеграмм на куче платформ работает telegram.org/apps и клиенты даже на пхп есть и их решение не было тупым все не писать на реакт нейтиве. Как это обычно делается, когда нет денег садят фуллстеков пилить на одном говнофреймворке под все платформы

sudo chmod u+x

У вас что билды под рутом собираются?

Телеграм — не очень показательный пример; они могут себе позволить инвестировать столько бабла, чтобы писать отдельные клиенты нативно под каждую платформу. У многих стартапов, да и не только стартапов, бюджеты сильно скромнее.

У вас что билды под рутом собираются?

Это точно мне вопрос? Я даже не вполне понимаю, откуда цитата с командой chmod.
Могу только предположить, что на девелоперских машинах таки обычно работают под аккаунтом, у которого есть рутовые права.

в статье

— run:
name: Create folders for react native bundles
command: |
mkdir -p ~/tmp/stageBundle
mkdir -p ~/tmp/prodBundle
sudo chmod u+x ./scripts/react_env_generator.sh

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

Похоже, вы даже не разобрались, что я не автор статьи

Срібної пулі не існує. Не слід писати все під ряд на React Native. Тому приклад із Telegram явно поганий.

На устройстве будет ещё меньше. aab bundle содержит код и ресурсы сразу для нескольких архитектур/устройств.

Вирішив це не уточнювати оскільки само собою розуміється.
Але дійсно, це краще для наочності.
Дякую.
blog.illuzor.com/...​/ezgif-2-72f65c1931c0.gif

Хм, цікава думка яка не прикріплена практичними справами. Є якісь детальніші ідеї чому це дійсно так, можливо нелюбов до JavaScript?

и это приводит к куче проблем

И каких же именно?

Может быть, не в нативных элементах дело, а в том, что RN гораздо менее склонен «прощать» ненужные ре-рендеры, неэффективную компоновку и прочие «срезания углов», которыми нередко грешат фронт-енд разработчики исключительно благодаря тому, что обычные браузеры прилагают огромное количество усилий к тому, чтобы даже криво написанная страница работала хорошо?

Схоже замовник зробив команді розробників пропозицію, від якої вони не змогли відмовитись )

Цікава стаття. Дякую що поділився практичним кейсом!

Спасибо, интересная статья.

«Було б круто, якби React Native розробники доповнили цей список у коментарях з описом підходів, які їм найбільше подобаються для різних випадків.»

Я юзал все три и React Navigation(первая) наилучшая — большая поддержка сообщества и гуд документация.

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