Last active
November 15, 2024 13:32
-
-
Save chourobin/f83f3b3a6fd2053fad29fff69524f91c to your computer and use it in GitHub Desktop.
React Native Bridging Cheatsheet
// index.ios.js
videoPlayer.seekTo(100, (error, someData) => {
if (error) {
console.error(error)
} else {
console.log(someData)
}
})
// index.android.js
videoPlayer.seekTo(
100,
(msg) => {
console.error(msg)
},
(someData) => {
console.log(someData)
}
)
RCT_EXPORT_METHOD(seekTo:(double)time callback:(RCTResponseSenderBlock)callback)
{
NSArray *someData;
callback(@[[NSNull null], someData]); // (error, someData) in js
}
@ReactMethod
public void seekTo(double time, Callback errorCallback, Callback successCallback) {
try {
successCallback.invoke(someData);
} catch (Exception e) {
errorCallback.invoke(e.getMessage());
}
}
Keep in mind:
- Events share namespace (so come up with an app-wide naming convention)
// VideoPlayer.js
class VideoPlayer extends React.PureComponent {
_onEnd = (event) => {
if (!this.props.onEnd) {
return;
}
this.props.onEnd(event.nativeEvent)
}
render() {
// Re-assign onEnd to the private _onEnd and store it in `nativeProps`
const nativeProps = {
...this.props,
onEnd: this._onEnd,
}
return (
<RCTVideo
{...nativeProps}
/>
)
}
}
const RCTVideo = requireNativeComponent('RCTVideo', VideoPlayer)
VideoPlayer.propTypes = {
/**
* Callback that is called when the current player item ends.
*/
onEnd: PropTypes.func,
}
Notes: Bubbling events are like DOM events so that a parent component can capture an event fired by its child. Generally these are UI-related, like "the user touched this box". Direct events are not bubbled and are intended for more abstract events like "this image failed to load".
// VideoPlayer.h
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
@interface VideoPlayer : UIView
// or RCTBubblingEventBlock
@property (nonatomic, copy) RCTDirectEventBlock onEnd;
@end
// VideoPlayerManager.m (inherits from RCTViewManager)
@implementation VideoPlayerManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(onEnd, RCTDirectEventBlock)
- (UIView *)view
{
...
}
/*
* `VideoPlayerManager` acts as the delegate of all of the `VideoPlayer` views. This is just one
* pattern and it's perfectly fine to call `onEnd` from the `VideoPlayer` directly.
*/
- (void)onEnd:(VideoPlayer *)videoPlayer
{
if (!videoPlayer.onEnd) {
return;
}
videoPlayer.onEnd(@{ @"some-data" : @1 });
}
@end
Define a custom event mapping by overriding getExportedCustomDirectEventTypeConstants
in the
manager class:
// VideoPlayerManager.java
@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onEnd",
MapBuilder.of("registrationName", "onEnd")
);
}
Dispatch the event:
// VideoPlayerView.java
private void dispatchOnEnd() {
WritableMap event = Arguments.createMap();
...
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(),
"onEnd",
event
);
}
Keep in mind:
- Events share namespace (so come up with an app-wide naming convention)
import { NativeEventEmitter, NativeModules } from 'react-native'
const videoPlayer = NativeModules.VideoPlayer
const videoPlayerEmitter = new NativeEventEmitter(VideoPlayer)
const subscription = videoPlayerEmitter.addListener('video-progress', (data) => console.log(data.progress))
// Don't forget to unsubscribe, typically in `componentWillUnmount`
subscription.remove()
Important Note: Don't send events if there are no listeners attached. You will get a warning about spending unneccessary resources. Override
-startObserving
and-stopObserving
like in the example below:
// VideoPlayer.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>
@end
// VideoPlayer.m
#import "VideoPlayer.h"
@implementation VideoPlayer
{
BOOL _hasListeners;
}
RCT_EXPORT_MODULE();
- (NSArray<NSString *> *)supportedEvents
{
return @[@"video-progress"];
}
// Will be called when this module's first listener is added.
- (void)startObserving
{
_hasListeners = YES;
}
// Will be called when this module's last listener is removed, or on dealloc.
- (void)stopObserving
{
_hasListeners = NO;
}
- (void)onVideoProgress
{
CGFloat progress = ...;
if (_hasListeners) {
[self sendEventWithName:@"video-progress" body:@{ @"progress": @(progress) }];
}
}
@end
private void onVideoProgress() {
WriteableMap params = Arguments.createMap();
...
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("video-progress", params);
}
async function loadVideo() {
try {
var videoLoaded = await VideoPlayer.loadVideo();
this.setState(videoLoaded: videoLoaded)
} catch (e) {
console.error(e)
}
}
loadVideo()
RCT_REMAP_METHOD(loadVideo, loadVideoWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
BOOL videoLoaded = ...; // could be any data type listed under https://facebook.github.io/react-native/docs/native-modules-ios.html#argument-types
if (videoLoaded) {
resolve(videoLoaded);
} else {
NSError *error = ...
reject(@"error", @"error description", error);
}
}
@ReactMethod loadVideo(Promise promise) {
try {
Boolean videoLoaded = ...; // could be any data type listed under https://facebook.github.io/react-native/docs/native-modules-android.html#argument-types
if (videoLoaded) {
promise.resolve(videoLoaded);
}
} catch (Exception e) {
promise.reject(ERROR, e);
}
}
render() {
<VideoPlayer loop={false} />
}
VideoPlayer.propTypes = {
loop: PropTypes.bool,
}
VideoPlayer.defaultProps = {
loop: false,
}
// BMEVideoManager.m
RCT_EXPORT_VIEW_PROPERTY(loop, BOOL);
@ReactProp(name = "loop")
public void setLoop(Boolean loop) {
...
}
import { NativeModules } from 'react-native'
const videoPlayer = NativeModules.VideoPlayer
videoPlayer.seekTo(100)
Keep in mind:
RCT_EXPORT_METHOD
exposes the name of the method up to the first colon in js land.- When many methods share the same name, use
RCT_REMAP_METHOD
to define a different name for js and map it to the native method. ie.RCT_REMAP_METHOD(differentMethodName, methodName:(BOOL)arg1 arg2:(BOOL)arg2)
.
// VideoPlayer.h
#import <React/RCTBridgeModule.h>
@interface VideoPlayer : NSObject <RCTBridgeModule>
@end
@implementation VideoPlayer
RCT_EXPORT_MODULE(); // or RCT_EXPLORT_MODULE(AwesomeVideoPlayer) -- custom name
RCT_EXPORT_METHOD(seekTo:(double)time)
{
// seek to time
}
@end
// VideoPlayer.java
package com.beme.react
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class VideoModule extends ReactContextBaseJavaModule {
public VideoModule(ReactApplicationContext reactContext) {
super(reactContext)
}
@Override
public String getName() {
return "VideoPlayer";
}
@ReactMethod
public void seekTo(double time) {
// seek to time
}
}
Register the module
// VideoPackage.java
package com.beme.react
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
public class VideoPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new VideoModule(reactContext));
return modules;
}
}
// MainApplication.java
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new VideoPackage()); // <-- Add this line with your package name.
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for this!!! I'm still trying to understand why we need to use RCT_REMAP_METHOD for promises - can someone explain that in more detail for me?