- PVSM.RU - https://www.pvsm.ru -
Всем привет! В конце весны 2024 новая архитектура React Native вышла в бета‑версию [1]. Хотя команда React Native пока не рекомендует использовать её в продакшн‑приложениях, многие библиотеки уже адаптированы для работы с ней или находятся на пути к полноценной интеграции. React Native всегда предоставлял возможность интеграции с нативным кодом, а новая архитектура делает этот процесс ещё более эффективным и гибким.
В этой статье я хочу поделиться своим опытом интеграции SwiftUI компонента с использованием Fabric и базы данных Realm с помощью Turbo Modules. Всё это я реализовал на примере iOS‑приложения, которое показывает список популярных фильмов, позволяет добавлять их в избранное, просматривать список избранного и удалять из него.
Само приложение достаточно объёмное, поэтому в данной статье я затрону лишь ключевые моменты, касающиеся интеграции. Мы не будем углубляться в детали реализации нативных компонентов, а сосредоточимся на процессе интеграции с React Native. Ссылку на репозиторий с приложением я оставлю в конце статьи.
Fabric — новая система рендеринга в React Native, подробнее можно прочитать здесь [2].
Turbo Module — это следующая эволюция нативных модулей в React Native, которая предоставляет дополнительные преимущества. В частности, Turbo Modules используют JSI (JavaScript Interface), интерфейс для нативного кода, который обеспечивает более эффективное взаимодействие между нативным и JavaScript кодом по сравнению мостом (Bridge).
Codegen — это инструмент, используемый в новой архитектуре React Native для автоматической генерации кода на основе определённых с помощью TypeScript / Flow интерфейсов.
Итак, начнём с обзора основного функционала приложения и мест, где используются нативные модули.
Демо: https://vimeo.com/1013777959?share=copy [3]
Главный экран — мы загружаем фильмы и показываем их пользователю в виде бесконечно скролящегося списка. Запрос выполняется на стороне JS, мы передаём статус загрузки и полученные данные в компонент MovieListView, который реализован на SwiftUI.
При нажатии на фильм мы можем перейти на экран с более подробной информацией о фильме, который полностью реализован нативно, но данные для этого мы всё равно запрашиваем на стороне JS, а затем передаём в тот же компонент. Также на главном экране мы используем функционал нативного модуля favourite-movies-storage, который отвечает за запись и чтение в базу данных Realm. Вся коммуникация также происходит через JS слой.
Экран списка избранных фильмов — это самый обычный Flatlist, но данные для него мы берем из базы данных Realm, используя всё тот же модуль favourite-movies-storage.
Есть несколько способов создать нативный модуль на новой архитектуре. Мы можем пойти и вручную написать конфигурацию для Codegen, как описано здесь [4] для Fabric компонента, или здесь [5] для Turbo Module.
Также мы можем использовать инструмент react-native-builder-bob [6], который создаст нам примитивный компонент или турбомодуль, который мы потом можем использовать как отправную точку для реализации своего функционала.
Я использовал последний подход. С помощью Bob мы можем создать как локальный модуль, так и библиотеку. В моем случае это был локальный модуль. Для этого в корне проекта я выполнил следующую команду:
npx create-react-native-library@latest favourite-movies-storage
После этого нужно ввести данные о конфигурации нашей библиотеки. Я создавал нативные модули без обратной совместимости со старой архитектурой.
Данную команду нужно выполнить дважды: первый раз для компонента, второй для турбомодуля. Но я хотел иметь весь нативный функционал, касающийся списка фильмов, в одном модуле, поэтому после создания я произвёл ряд манипуляций, чтобы объединить их в один модуль.
В итоге я получил следующую структуру:
Папку Android мы игнорируем.
package.json — представляет собой описание нашей библиотеки. Здесь стоит обратить внимание на поле codegenConfig:
"codegenConfig": {
"name": "RNMovieList",
"type": "all",
"jsSrcsDir": "src"
}
Здесь мы описываем название нашего модуля, папку, где лежит наш JS код, и тип модуля.
Если мы хотим иметь Fabric компонент и Turbo Module в одном месте, то он должен быть all.
react-native-movie-list.podspec — конфигурация CocoaPods модуля. Здесь есть несколько ключевых моментов:
Файлы исходного кода:
s.source_files = "ios/**/*.{h,m,mm,swift}"
По умолчанию там нет Swift, поэтому это нужно добавить вручную.
Здесь же мы добавляем зависимости для нашего модуля. Помимо стандартных, я добавил Realm и SwiftUIIntrospect:
s.dependency "SwiftUIIntrospect"
s.dependency "RealmSwift", '~> 10'
Сначала идём в index.tsx. Здесь нас интересуют две строки, которые экспортируют наш компонент и связанные с ним типы за пределы модуля:
export {default as MovieListView} from './MovieListViewNativeComponent';
export * from './MovieListViewNativeComponent';
Конфигурация нашего компонента происходит в файле MovieListViewNativeComponent.ts.
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type {ViewProps} from 'react-native';
import {
Double,
WithDefault,
DirectEventHandler,
Int32,
} from 'react-native/Libraries/Types/CodegenTypes';
type Movie = {
readonly id: Int32;
readonly title: string;
readonly url: string;
readonly movieDescription: string;
readonly rating: Double;
};
type Genre = {
id: Int32;
name: string;
};
type MovieDetails = {
readonly id: Int32;
readonly title: string;
readonly posterURL: string;
readonly overview: string;
readonly genres: Genre[];
readonly rating: Double;
readonly isFavourite: boolean;
};
export type OnMoviePressEventData = {
readonly movieID: Int32;
};
export type OnMovieAddedToFavorites = OnMoviePressEventData;
export type OnMovieRemovedFromFavorites = OnMovieAddedToFavorites;
export type OnMovieInteractionCallback =
DirectEventHandler<OnMoviePressEventData>;
type NetworkStatus = WithDefault<'loading' | 'success' | 'error', 'loading'>;
interface NativeProps extends ViewProps {
readonly movies: Movie[];
readonly onMoviePress: DirectEventHandler<OnMoviePressEventData>;
readonly onMovieAddedToFavorites: DirectEventHandler<OnMovieAddedToFavorites>;
readonly onMovieRemovedFromFavorites: DirectEventHandler<OnMovieRemovedFromFavorites>;
readonly movieListStatus?: NetworkStatus;
readonly movieDetailsStatus?: NetworkStatus;
readonly movieDetails?: MovieDetails;
readonly onMoreMoviesRequested: DirectEventHandler<null>;
}
export default codegenNativeComponent<NativeProps>('MovieListView');
Здесь мы должны описать пропсы, которые будет принимать наш компонент, и именно на основе этих типов Codegen будет генерировать нашу нативную часть, что и происходит на последней строке файла.
На этом конфигурация нашего модуля на стороне JS завершена, всё оказалось достаточно просто. Теперь перейдём к нативной части.
Нас интересуют следующие файлы: MovieListView.h, MovieListView.mm, MovieListViewManager.mm.
MovieListView.h — заголовочный файл, определяющий интерфейс для нашего компонента. Мы могли бы добавить здесь методы, которые можем вызвать на вью, но в нашем случае он пуст. Помимо этого здесь мы импортируем файл заголовка react_native_movie_list-Swift.h, который содержит интерфейсы для Swift кода, доступного нам в Objective-C.
// This guard prevents this file from being compiled in the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
#import "react_native_movie_list-Swift.h"
#ifndef MovieListViewNativeComponent_h
#define MovieListViewNativeComponent_h
NS_ASSUME_NONNULL_BEGIN
@interface MovieListView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END
#endif /* MovieListViewNativeComponent_h */
#endif /* RCT_NEW_ARCH_ENABLED */
MovieListViewManager.mm — это менеджер нашего компонента, React Native использует его во время выполнения, чтобы зарегистрировать модуль, доступный в JS. Самым важным здесь является вызов метода RCT_EXPORT_MODULE, который и регистрирует наш модуль.
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import "RCTBridge.h"
@interface MovieListViewManager : RCTViewManager
@end
@implementation MovieListViewManager
RCT_EXPORT_MODULE(MovieListView)
@end
MovieListView.mm — файл реализации нашего компонента, здесь происходит основная работа по созданию компонента. Сам файл достаточно объёмный и содержит много вспомогательного кода, поэтому я затрону лишь основные методы, которые отвечают за интеграцию.
#import "MovieListView.h"
#import <react/renderer/components/RNMovieList/ComponentDescriptors.h>
#import <react/renderer/components/RNMovieList/EventEmitters.h>
#import <react/renderer/components/RNMovieList/Props.h>
#import <react/renderer/components/RNMovieList/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
#import "React/RCTConversions.h"
using namespace facebook::react;
@interface MovieListView () <RCTMovieListViewViewProtocol>
@end
@implementation MovieListView {
MovieListViewController *_movieListViewController;
UIView *_view;
}
+ (ComponentDescriptorProvider)componentDescriptorProvider {
return concreteComponentDescriptorProvider<MovieListViewComponentDescriptor>();
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
static const auto defaultProps = std::make_shared<const MovieListViewProps>();
_props = defaultProps;
_movieListViewController = [MovieListViewController createViewController];
}
return self;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if (self.window) {
[self setupView];
}
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
const auto &oldViewProps = *std::static_pointer_cast<MovieListViewProps const>(_props);
const auto &newViewProps = *std::static_pointer_cast<MovieListViewProps const>(props);
[self updateMovieListAndStatusIfNeeded:oldViewProps newProps:newViewProps];
[self updateMovieDetailsStatusAndMovieDetilsIfNeeded:oldViewProps newProps:newViewProps];
[self setupEventHandlers];
[super updateProps:props oldProps:oldProps];
}
Class<RCTComponentViewProtocol> MovieListViewCls(void) {
return MovieListView.class;
}
Сначала отметим, что компонент должен реализовывать протокол RCTMovieListViewViewProtocol, который был сгенерирован с помощью Codegen.
(ComponentDescriptorProvider)componentDescriptorProvider — метод, который используется Fabric для получения дескриптора, необходимого для создания экземпляра нашего компонента.
Также стоит обратить внимание на объявление переменной экземпляра:
@implementation MovieListView {
MovieListViewController *_movieListViewController;
}
Этот контроллер отвечает за создание, взаимодействие и обмен данными с нашим SwiftUI вью.
initWithFrame — метод, объявленный в интерфейсе UIView, который является базовым классом для всех вью‑компонентов в UIKit. Он инициализирует новый экземпляр UIView с указанным размером и положением (передаваемым в параметре CGRect frame. В нашем случае, в методе initWithFrame происходит не только инициализация вью с заданными размерами, но и создание MovieListViewController, который управляет SwiftUI компонентом. Помимо этого, здесь мы создаем и устанавливаем пропсы по умолчанию.
didMoveToWindow — это метод жизненного цикла UIView. Он вызывается, когда наше вью добавляется в иерархию, прикреплённую к окну, когда удаляется из него и когда перемещается в другое окно. Когда вью удаляется, то self.window будет равен nil. Также данный метод вызывает setupView, который в свою очередь устанавливает constraints для вью, содержащего наш SwiftUI компонент. Также нам важно добавить *_movieListViewController* в иерархию вью контроллеров, так как из нашего SwiftUI компонента мы можем перейти на новый экран в виде модального окна (Sheet), в котором можно увидеть больше деталей о выбранном фильме.
updateProps — это метод, который вызывается Fabric каждый раз, когда в JavaScript изменяется любой из пропсов. Этот метод обеспечивает синхронизацию состояния между JavaScript и нативным кодом, передавая обновленные значения свойств в нативную часть компонента. Здесь переданные параметры приводятся к нужному типу, соответствующему ожидаемым пропсам компонента (в нашем случае это MovieListViewProps). Затем эти параметры используются для обновления нативного компонента при необходимости. Важно отметить, что метод суперкласса [super updateProps] должен быть вызван в самом конце метода updateProps. Если этот вызов сделать раньше или не сделать вовсе, структуры props и oldProps будут содержать одни и те же значения, что лишит возможности сравнить старые и новые значения свойств. Помимо этого здесь вызывается метод setupEventHandlers, который отвечает за создание коллбеков, которые позже передаются в SwiftUI компонент.
Разберём один из них:
- (void)onMovieAddedToFavorites:(NSInteger)movieId {
if (_eventEmitter != nullptr) {
auto emitter = std::dynamic_pointer_cast<const facebook::react::MovieListViewEventEmitter>(_eventEmitter);
if (emitter) {
emitter->onMovieAddedToFavorites(facebook::react::MovieListViewEventEmitter::OnMovieAddedToFavorites{static_cast<int>(movieId)});
}
}
}
Здесь происходит отправка события в JS, когда фильм добавляется в избранное. Сначала проверяется, существует ли объект _eventEmitter, который отвечает за отправку событий. Затем выполняется приведение _eventEmitter к типу MovieListViewEventEmitter, который был сгенерирован Codegen на основе типов, о которых мы говорили в начале секции. Если приведение успешно, создается событие с идентификатором добавленного фильма, которое отправляется в React Native через вызов метода onMovieAddedToFavorites. Если всё прошло успешно, то будет вызван нужный коллбек на стороне JS.
MovieListViewCls — это статический метод, используемый для получения правильного экземпляра класса MovieListView во время выполнения, что позволяет React Native корректно идентифицировать и рендерить этот нативный компонент.
Это были основные моменты, касающиеся интеграции Fabric компонента. Реализацию самого нативного компонента мы рассматривать не будем.
Далее рассмотрим основные моменты интеграции Turbo Module на примере нашего модуля работы с базой данных.
Здесь всё также начинается с описания нашего модуля на TypeScript, чтобы Codegen смог сгенерировать нативные интерфейсы для нас.
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export interface FavouriteMovie {
id: number;
url: string;
title: string;
rating: string;
}
export interface Spec extends TurboModule {
getFavouriteMovies(): FavouriteMovie[];
addFavouriteMovie(movie: FavouriteMovie): Promise<FavouriteMovie[]>;
removeFavouriteMovie(movieId: number): Promise<FavouriteMovie[]>;
removeAllFavouriteMovies(): Promise<void>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('FavouriteMoviesStorage');
Сначала мы должны создать интерфейс для нашего модуля, который должен наследоваться от интерфейса TurboModule и называться Spec. Здесь мы описываем 4 метода, которые мы хотим реализовать. Примечательно, что метод getFavouriteMovies является синхронным. Это было возможно и в старой архитектуре, но имело свои недостатки и было не рекомендовано [7] для использования.
В конце мы вызываем
TurboModuleRegistry.getEnforcing<Spec>('FavouriteMoviesStorage')
Мы делаем это, для того чтобы получить нативный модуль FavouriteMoviesStorage, если он доступен. И на этом спецификация модуля окончена.
Далее разберёмся с тем, что у нас происходит в нативной части.
Здесь нас интересуют два ключевых для интеграции файла: FavouriteMoviesStorage.h и FavouriteMoviesStorage.mm.
FavouriteMoviesStorage.h — здесь мы объявляем интерфейс, который наследуется от NSObject и реализует протокол NativeFavouriteMoviesStorageSpec, сгенерированный для нас с помощью Codegen. Если новая архитектура не включена, то данный код скомпилирован не будет.
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNMovieList/RNMovieList.h"
#import "react_native_movie_list-Swift.h"
@interface FavouriteMoviesStorage : NSObject <NativeFavouriteMoviesStorageSpec>
@end
#endif
FavouriteMoviesStorage.mm — именно в этом файле происходит реализация всей логики нашего модуля базы данных.
#import "FavouriteMoviesStorage.h"
#import "react_native_movie_list-Swift.h"
@implementation FavouriteMoviesStorage
RCT_EXPORT_MODULE()
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeFavouriteMoviesStorageSpecJSI>(params);
}
......
@end
Самым важным здесь является вызов RCT_EXPORT_MODULE(), который делает наш модуль доступным на стороне JS.
Метод getTurboModule получает экземпляр нашего Turbo Module, чтобы его методы могли вызываться со стороны JS. Этот метод определён и обязателен в файле FavouriteMoviesStorageSpec.h, который был сгенерирован ранее с помощью Codegen.
Далее рассмотрим примеры реализации методов работы с базой данных.
- (NSArray<NSDictionary *> *)getFavouriteMovies {
NSArray *movies = [[FavouriteMoviesManager shared] fetchAllFavouriteMoviesAsList];
NSMutableArray *result = [NSMutableArray array];
for (IntermediateFavouriteMovie *movie in movies) {
[result addObject:[self dictionaryFromFavouriteMovie:movie]];
}
return result;
}
Это синхронный метод. Он ничего не принимает и возвращает нам массив фильмов. Он вызывает метод fetchAllFavouriteMoviesAsList, после чего конвертирует данные в ожидаемый формат и возвращает их. Реализацию FavouriteMoviesManager мы рассматривать не будем, но там нет ничего примечательного, просто обращение к Realm и получение списка фильмов.
Теперь рассмотрим метод для удаления всех фильмов из избранного — removeAllFavouriteMovies.
- (void)removeAllFavouriteMovies:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[[FavouriteMoviesManager shared] removeAllFavouriteMoviesOnSuccess:^{
resolve(@YES);
} onError:^(NSError * _Nonnull error) {
reject(@"remove_all_favourite_movies_error", error.localizedDescription, error);
}];
}
Этот метод принимает два параметра: resolve и reject, так как в спецификации мы указали, что данный метод возвращает Promise. Когда вызывается removeAllFavouriteMovies, он передает два блока — onSuccess и onError — в метод removeAllFavouriteMoviesOnSuccess класса FavouriteMoviesManager. Если операция удаления всех избранных фильмов проходит успешно, вызывается блок onSuccess, который активирует resolve с параметром @YES, завершая промис успешно. Если же происходит ошибка, вызывается блок onError, который активирует reject с описанием ошибки, что резолвит промис с ошибкой.
Остальные методы работают по схожему принципу, поэтому рассматривать их подробно смысла нет, так как ничего нового мы там не увидим.
Это основные моменты, касающиеся интеграции нативного модуля базы данных с использованием Turbo Modules в новой архитектуре React Native. Как я упоминал ранее, мы не будем углубляться в детали нативной реализации, поскольку основная цель статьи — показать процесс интеграции.
Использование новой архитектуры для реализации нативных модулей, как показано на примере этого приложения, — это вполне выполнимая задача. Конечно, потребуется некоторое время, чтобы привыкнуть к синтаксису C++, разобраться в нюансах сборки и особенностях работы, особенно если у вас не было опыта с этим языком. Однако эти усилия оправданы. Новая архитектура предлагает множество преимуществ, особенно при передаче больших объёмов данных между JavaScript и нативным кодом. С турбомодулями мы можем использовать синхронные методы, например, для доступа к данным. Кроме того, новая архитектура позволяет эффективно применять нативные UI‑компоненты. Например, в моём случае список фильмов на SwiftUI работал гораздо лучше «из коробки», чем FlatList, встроенный в RN. Даже при том, что моя реализация далека от оптимальной, так как происходит достаточно много копирования и создания новых объектов, для того, чтобы конвертировать данные для работы со SwiftUI. Мы могли бы использовать UIKit и UITableView, что могло бы решить некоторые из проблем. Но это выходит за рамки данной статьи
Это всё, чем я хотел поделиться. Надеюсь, данная статья была вам полезна. Спасибо за внимание!
Ссылка на репозиторий: https://github.com/tikhonNikita/movieApp [8]
Автор: Pofofwar
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/react-native/398308
Ссылки в тексте:
[1] бета‑версию: https://github.com/reactwg/react-native-new-architecture/discussions/189
[2] здесь: https://reactnative.dev/architecture/fabric-renderer
[3] https://vimeo.com/1013777959?share=copy: https://vimeo.com/1013777959?share=copy
[4] здесь: https://github.com/reactwg/react-native-new-architecture/blob/main/docs/fabric-native-components.md
[5] здесь: https://github.com/reactwg/react-native-new-architecture/blob/main/docs/turbo-modules.md
[6] react-native-builder-bob: https://callstack.github.io/react-native-builder-bob
[7] не рекомендовано: https://reactnative.dev/docs/native-modules-ios#synchronous-methods
[8] https://github.com/tikhonNikita/movieApp: https://github.com/tikhonNikita/movieApp
[9] Источник: https://habr.com/ru/articles/847768/?utm_source=habrahabr&utm_medium=rss&utm_campaign=847768
Нажмите здесь для печати.