Головна / Статті / Підключення модуля Turbo всередині додатку від початку до кінця за допомогою React Native Codegen

Підключення модуля Turbo всередині додатку від початку до кінця за допомогою React Native Codegen

Визначте типову специфікацію, запустіть генерацію коду та реалізуйте Turbo Module для iOS та Android із синхронними, Promise-, callback- та event-emitter-методами.

1409 слів

Нова архітектура React Native замінює асинхронний міст на Turbo Modules, які JavaScript викликає через JSI за допомогою типованих, генерованих зв’язків. Це призводить до зниження навантаження від викликів та до перевірки контракту за допомогою codegen замість умовних правил. У цьому посібнику створюється один модуль у межах додатку (без пакета npm) та розглядаються три стилі викликів, які ви будете використовувати на практиці: синхронне повернення значення, асинхронні результати через Promise чи callback та потік подій.

Складання типованої специфікації

Створіть папку specs у корені проекту для зберігання схем модулів:

/your-app
  /specs

Усередині неї додайте NativeShowCase.ts. Codegen бере до уваги лише файли специфікації, назви яких починаються з Native. Наведена нижче специфікація оголошує по одній методу на кожен стиль: haveCameraFlash повертає результат синхронно, toggleFlashLight повертає Promise, fetchUser приймає копі-функцію, а evenListenerCalled є емітером подій, який передає пари ключ/значення. getEnforcing кидає помилку під час запуску, якщо відсутня нативна частина, що дозволяє рано виявити помилки підключення. Типовані емітери подій у специфікаціях — це нововведення, тому переконайтеся, що ваша версія React Native підтримує CodegenTypes.EventEmitter.

import type { CodegenTypes, TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";

export type KeyValuePair = {
  key: string;
  value: string;
};

export interface Spec extends TurboModule {
  toggleFlashLight(): Promise<boolean>;
  haveCameraFlash(): boolean;
  fetchUser(
    id: string,
    callback: (user: { id: string; name: string }) => void
  ): void;
  readonly evenListenerCalled: CodegenTypes.EventEmitter<KeyValuePair>;
}

export default TurboModuleRegistry.getEnforcing<Spec>("NativeShowCase");

Цей файл є єдиним джерелом істини; біндинги обох платформ генеруються з нього.

Налаштування та запуск codegen

Додайте блок codegenConfig до файлу package.json. Поле jsSrcsDir має збігатися з папкою ваших специфікацій, а javaPackageName визначає місце створення класів для Android:

"codegenConfig": {
  "name": "NativeShowCase",
  "type": "modules",
  "jsSrcsDir": "specs",
  "android": {
    "javaPackageName": "com.nativeshowcase"
  }
}

Потім створіть необхідні файли. Для Android запустіть завдання Gradle:

cd android && ./gradlew generateCodegenArtifactsFromSchema

Для iOS процес кодогенерації відбувається під час встановлення pods:

cd ios && pod install

Тепер у вас створені інтерфейси, заголовкові файли та допоміжний код для обох платформ.

Імплементація модуля на iOS

Відкрийте файл .xcworkspace у Xcode, створіть групу NativeShowCase та додайте клас на мові Objective-C під назвою RCTNativeShowCase. Перейменуйте файл .m у .mm: Turbo Modules потребують Objective-C++ для взаємодії з шаром JSI на C++.

Заголовок імпортує створений модуль та успадковує його від базового класу, створеного автоматично, водночас застосовуючи створений протокол:

#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN

@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>

@end

NS_ASSUME_NONNULL_END

Реалізація повертає назву модуля, передає React Native об’єкт JSI з функції getTurboModule та реалізує кожну методику, передбачену специфікацією. Функція toggleFlashLight вирішує обіцянку, fetchUser викликає копі-функцію, а haveCameraFlash негайно повертає результат, але запускає таймер, який генерує десять подій через створену методику emitEvenListenerCalled.

#import "RCTNativeShowCase.h"

@interface RCTNativeShowCase()
@property (nonatomic, strong) NSTimer *myTimer;
@property (nonatomic, assign) int count;
@end

@implementation RCTNativeShowCase

+ (NSString *)moduleName {
  return @"NativeShowCase";
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeShowCaseSpecJSI>(params);
}

- (void)toggleFlashLight:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
  resolve(@(false));
}

- (nonnull NSNumber *)haveCameraFlash {
  [self startTimer];
  return [NSNumber numberWithBool:true];
}

- (void)fetchUser:(nonnull NSString *)userId callback:(nonnull RCTResponseSenderBlock)callback {
  callback(@[@{@"key": userId, @"value": @"John"}]);
}


-(void)startTimer {
  if(self.myTimer) {
    [self stopTimer];
  }
  self.count = 0;
  self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(updateCount)
                                                userInfo:nil
                                                 repeats:YES];
}
- (void)updateCount {
  self.count += 1;
  [self emitEvenListenerCalled:@{@"key": @"count", @"value": @(_count)}];
  if (self.count == 10) {
    [self stopTimer];
  }
}
- (void)stopTimer {
  [self.myTimer invalidate];
  self.myTimer = nil;
}
@end

Зверніть увагу, що ця версія для iOS у функції fetchUser надсилає поля key/value, тоді як специфікація вимагає id та name; у реальному коді їх потрібно привести до узгодження, оскільки інструмент кодогенерації не перевіряє форму даних, які надсилаються копі-функціям, під час виконання.

Реалізація модуля на Android

Створіть пакет com.nativeshowcase, оголошений у codegenConfig. Клас модуля успадковує генерований NativeShowCaseSpec та перевизначає кожну методів: він перемикає флаг та обробляє Promise, запускає CountDownTimer, який звітує про залишковий час після кожного інтервалу, та створює мапу для копі-функції.

package com.nativeshowcase

import android.os.CountDownTimer
import android.widget.Toast
import com.facebook.fbreact.specs.NativeShowCaseSpec
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext

class NativeShowCaseModule(reactContext: ReactApplicationContext) :
    NativeShowCaseSpec(reactContext) {
    var timer: CountDownTimer? = null
    var lightOn = false
    override fun toggleFlashLight(promise: Promise?) {
        Toast.makeText(reactApplicationContext, "Let's turn on flash light", Toast.LENGTH_LONG)
            .show()
        lightOn = !lightOn
        promise?.resolve(lightOn)
    }

    override fun haveCameraFlash(): Boolean {
        if (timer != null) {
            timer?.cancel()
            timer = null
        }
        timer = object : CountDownTimer(10000, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                val eventData = Arguments.createMap().apply {
                    putString("key", "count")
                    putInt("value", (millisUntilFinished / 1000).toInt())
                }
                emitEvenListenerCalled(eventData)
            }

            override fun onFinish() {
                timer?.cancel()
                timer = null
            }
        }
        timer?.start()
        return true
    }

    override fun fetchUser(id: String?, callback: Callback?) {
        val eventData = Arguments.createMap().apply {
            putString("id", id)
            putString("name", "John")
        }
        callback.let { it?.invoke(eventData) }
    }

    companion object {
        const val NAME = "NativeShowCase"
    }

    override fun getName() = NAME
}

Клас пакета, створений на основі BaseReactPackage, вказує React Native, як інстанціювати модуль. Флаг isTurboModule = true у його ReactModuleInfo визначає шлях обробки через нову систему.

package com.nativeshowcase

import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class NativeShowCasePackage : BaseReactPackage() {
    override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
        if (name == NativeShowCaseModule.NAME) return NativeShowCaseModule(reactContext)
        return null
    }

    override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
        mapOf(
            NativeShowCaseModule.NAME to ReactModuleInfo(
                name = NativeShowCaseModule.NAME,
                className = NativeShowCaseModule.NAME,
                canOverrideExistingModule = false,
                needsEagerInit = true,
                isCxxModule = false,
                isTurboModule = true
            )
        )
    }
}

Нарешті, зареєструйте пакет у MainApplication.kt, додавши його до списку автопідключених пакетів:

class MainApplication : Application(), ReactApplication {
    override val reactHost: ReactHost by lazy {
        getDefaultReactHost(
            context = applicationContext,
            packageList =
                PackageList(this).packages.apply {
                    add(NativeShowCasePackage())
                },
        )
    }
    override fun onCreate() {
        super.onCreate()
        loadReactNative(this)
    }
}

Виклик модуля з React

Імпортуйте стандартний експорт специфікації та використовуйте його як звичайний об’єкт. Ефект підписується на емітер подій та повертає функцію для очищення, яка видаляє слухача під час зняття; одна кнопка викликає синхронний метод, інша — методи Promise та callback.

import { StatusBar, StyleSheet, useColorScheme, View, Button } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import NativeShowCase from './specs/NativeShowCase';
import { useEffect } from 'react';

function App() {
  const isDarkMode = useColorScheme() === 'dark';
  return (
    <SafeAreaProvider>
      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
      <AppContent />
    </SafeAreaProvider>
  );
}
function AppContent() {
  useEffect(() => {
    const listener = NativeShowCase.evenListenerCalled(pair => {
      console.log('Event received:', pair);
    });
    return () => listener.remove();
  }, []);
  return (
    <View style={styles.container}>
      <Button
        title="Have Camera Flash?"
        onPress={() => {
          console.log('Have Camera Flash:', NativeShowCase.haveCameraFlash());
        }}
      />
      <Button
        title="Toggle Flashlight"
        onPress={() => {
          NativeShowCase.toggleFlashLight()
            .then(isOn => console.log('Flashlight:', isOn))
            .catch(err => console.error(err));
          NativeShowCase.fetchUser('123', user => {
            console.log('Fetched user:', user);
          });
        }}
      />
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
});
export default App;

Використовуйте синхронні методи обережно: вони блокують потік JavaScript до моменту повернення результату від нативного коду, тому залиште їх для простих пошуків. Щодо альтернативного підходу до прив’язки, дивіться, як Nitro Modules порівнюються з Turbo Modules.

Ключові висновки

  • Специфікація TypeScript є контрактом; codegen на основі неї генерує інтерфейси для обох платформ.
  • Називайте файли специфікацій з префіксом Native та підтримуйте синхронність між codegenConfig, папками та пакетами.
  • Імплементації для iOS потребують файлів .mm; пакети для Android мають позначати модулі за допомогою isTurboModule.
  • Ретельно обирайте стиль виклику: синхронний для простих операцій читання, Promises або callbacks для більш складних завдань, емітери — для потоків даних.
  • Пов’язана література