Note
At now 2024.3, it turns out that the New Architecture(including Turbo module, Fabric) is still ongoing. We can ask, is this native moudle example valid yet?
Sadly, yes.
At now, you have 3 options to implement native module(component).
- (No need New Arch) Plain old native module/component (this repository describes it) 👈
- (No need New Arch, Template) Use JSI directly to communicate native(c++). Example: mmkv(JSI module package), vision-camera(JSI component package)
- (Need New Arch, Turbo Module Guide, Fabric Guide) Use Turbo Module, Fabric.
New Architecture is still an experimental feature, and discussions are continuing. Many third-party libraries still leave behind code that uses plain old bridges, and updates are not progressing smoothly. The code in this example is still simple and useful when you simply want to use native functions of Android or iOS or wrap a view.
C++ code implementation using JSI supports Flow and Scaffolding Generation using TypeScript specifications through Codegen. However, constructing modules using the techniques described here is still used in Fabric and Turbo Modules. No, rather, it is very much used except for a little abstraction layer.
Moreover, using the Interop Layer introduced starting from RN 0.72 means that libraries compatible with our Bridge can still work properly in apps of the New Architecture.
This examples are good start point of your native library.
Tip
If you feel this example is useful, please give this repository a bright star! Any contributions are welcome anytime.
The React Native can use native features with React Native bridge. This native feature includes Native modules and Native components. The React Native Re-architecture with JSI is ongoing but knowing how to working native modules is valuable at this time.
I created two native modules Calculator
and MyText
. Calculator
is just a simple calculator class and MyText
is a simple AppCompatTextView
(Android) and UILabel
(iOS) wrapper view.
This sample is written with languages JavaScript
, Kotlin
(Android), and Swift
(iOS).
-
React:
16.13.1
-
React Native:
0.63.2
-
Android Gradle Plugin:
4.0.1
-
Kotlin:
1.4.0
-
Gradle:
6.2
-
Swift
5
app/build.gradle (module level)
When you create Kotlin file in React Native project first, configure Kotlin
button is shown. Click Yes.
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' // add kotlin-android plugin
...
build.gradle (project level)
buildscript {
ext.kotlin_version = '1.4.0' // this is added automatically
ext {
buildToolsVersion = "29.0.2"
minSdkVersion = 16
compileSdkVersion = 29
targetSdkVersion = 29
}
repositories {
google()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:4.0.1")
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // this is added automatically
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
...
}
{{Your-Project-Name}}-Bridging-Header.h
When you create Swift file in React Native project first, the bridge header configure dialog is shown. Click Yes.
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
You can check your bridging header file path in XCode.
NativeModules.m
#import "{{Your-Project-Name}}-Bridging-Header.h"
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
@interface RCT_EXTERN_MODULE(Calculator, NSObject) // inspect it later!
...
@end
The Calculator
module has three different way to add two integer.
- Promise
- Callback
- Event(Native -> JS)
Calculator.js
import {NativeModules, NativeEventEmitter} from 'react-native';
const NativeCalculator = NativeModules.Calculator;
const CalculatorEmitter = new NativeEventEmitter(NativeCalculator);
const EventName = NativeCalculator.EVENT_ADD_SUCCESS;
class Calculator {
native;
subscription;
constructor(native) {
this.native = native;
}
async addWithPromise(n1, n2) {
return await this.native.addWithPromise(n1, n2);
}
async addWithCallback(n1, n2, callback) {
this.native.addWithCallback(n1, n2, callback, (e) => {});
}
addWithListener(n1, n2) {
this.native.addWithListener(n1, n2);
}
addResultListener(listener) {
this.subscription = CalculatorEmitter.addListener(EventName, listener);
}
removeResultListener() {
this.subscription && this.subscription.remove();
}
}
export default new Calculator(NativeCalculator);
Calculator.kt
class CalculatorPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return mutableListOf(Calculator(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
return mutableListOf()
}
}
class Calculator(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "Calculator"
override fun getConstants() = mapOf(
"EVENT_ADD_SUCCESS" to EVENT_ADD_SUCCESS
)
@ReactMethod
fun addWithPromise(n1: Int, n2: Int, promise: Promise) = promise.resolve(n1 + n2)
@ReactMethod
fun addWithCallback(n1: Int, n2: Int, successCallback: Callback, failCallback: Callback) {
successCallback(n1 + n2)
}
@ReactMethod
fun addWithListener(n1: Int, n2: Int) =
reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(EVENT_ADD_SUCCESS, n1 + n2)
companion object {
const val EVENT_ADD_SUCCESS = "event_add_success"
}
}
MainApplication.kt
Add our ReactPackage
in getPackages()
method.
class MainApplication : Application(), ReactApplication {
override fun getReactNativeHost() = object : ReactNativeHost(this) {
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
override fun getPackages() = PackageList(this).packages.apply {
add(MyTextPackage())
add(CalculatorPackage()) // Here
}
override fun getJSMainModuleName() = "index"
}
...
}
Calculator.swift
import Foundation
@objc(Calculator)
class Calculator: RCTEventEmitter{
static let EVENT_ADD_SUCCESS = "event_add_success"
override func supportedEvents() -> [String]! {
return [Calculator.EVENT_ADD_SUCCESS]
}
@objc
override func constantsToExport() -> [AnyHashable: Any]!{
return ["EVENT_ADD_SUCCESS": Calculator.EVENT_ADD_SUCCESS]
}
@objc
static override func requiresMainQueueSetup() -> Bool{
return true;
}
@objc
func addWithPromise(_ first: Int, n2 second: Int, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock){
resolve(first + second)
}
@objc
func addWithCallback(_ first: Int, n2 second: Int, onSuccess: RCTResponseSenderBlock, onFail: RCTResponseSenderBlock){
onSuccess([first + second])
}
@objc
func addWithListener(_ first: Int, n2 second: Int){
self.sendEvent(withName: Calculator.EVENT_ADD_SUCCESS, body: first + second)
}
}
NativeModules.m
Add your module in Objective-C extern bridge file.
#import "{{Your-Project-Name}}-Bridging-Header.h"
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
@interface RCT_EXTERN_MODULE(Calculator, NSObject)
RCT_EXTERN_METHOD(
addWithPromise: (int)first
n2: (int)second
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
addWithCallback: (int)first
n2: (int)second
onSuccess: (RCTResponseSenderBlock)onSuccess
onFail: (RCTResponseSenderBlock)onFail
)
RCT_EXTERN_METHOD(
addWithListener: (int)first
n2: (int)second
)
@end
The MyText
native component has three features.
- pass
text
with prop - subscribe event from native component when text is changed with
onTextChanged
prop - manipluate directly with
ref
MyText.js
import React, {useRef, useImperativeHandle, useCallback} from 'react';
import {
requireNativeComponent,
UIManager,
findNodeHandle,
Platform,
} from 'react-native';
const COMPONENT_NAME = Platform.OS === 'ios' ? 'MyTextView' : 'MyText';
const NativeComponent = requireNativeComponent(COMPONENT_NAME);
const NativeViewManager = UIManager[COMPONENT_NAME];
const PROP_TEXT = 'textProp';
const COMMAND_SET_TEXT = 'setText';
const EVENT_ON_TEXT_CHANGED = 'onTextChanged';
const MyText = ({text, style, onTextChanged}, ref) => {
const nativeRef = useRef(null);
const manipulateTextWithUIManager = useCallback((text) => {
UIManager.dispatchViewManagerCommand(
findNodeHandle(nativeRef.current),
NativeViewManager.Commands[COMMAND_SET_TEXT],
[text],
);
}, []);
useImperativeHandle(
ref,
() => ({
setText: manipulateTextWithUIManager,
}),
[manipulateTextWithUIManager],
);
return (
<NativeComponent
ref={nativeRef}
style={[{height: 200}, style]}
{...{
[PROP_TEXT]: text,
[EVENT_ON_TEXT_CHANGED]: ({nativeEvent: {text}}) => onTextChanged(text),
}}
/>
);
};
export default React.forwardRef(MyText);
MyText.kt
class MyTextPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return mutableListOf()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return mutableListOf(MyTextManager())
}
}
class MyTextManager : SimpleViewManager<MyText>() {
override fun getName() = "MyText"
override fun createViewInstance(reactContext: ThemedReactContext) = MyText(reactContext)
// region Direct manipulation with ref
override fun getCommandsMap(): MutableMap<String, Int> {
return mutableMapOf(COMMAND_SET_TEXT to COMMAND_SET_TEXT_ID)
}
override fun receiveCommand(root: MyText, commandId: Int, args: ReadableArray?) {
if (commandId == COMMAND_SET_TEXT_ID) root.textProp = args!!.getString(0)!!
}
// endregion
/** props of custom native component */
@ReactProp(name = "textProp")
fun MyText.setText(value: String = "") {
textProp = value
}
// region Native -> JS prop event
override fun getExportedCustomDirectEventTypeConstants(): Map<String?, Any?>? {
return createExportedCustomDirectEventTypeConstants()
}
private fun createExportedCustomDirectEventTypeConstants(): Map<String?, Any?>? {
return MapBuilder.builder<String?, Any?>()
.put(EVENT_ON_TEXT_CHANGED, MapBuilder.of("registrationName", EVENT_ON_TEXT_CHANGED)).build()
}
// endregion
companion object {
private const val COMMAND_SET_TEXT = "setText"
private const val COMMAND_SET_TEXT_ID = 1
const val EVENT_ON_TEXT_CHANGED = "onTextChanged"
}
}
class MyText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
AppCompatTextView(context, attrs) {
init {
textSize = 48f
gravity = Gravity.CENTER
}
var textProp = ""
set(value) {
field = value
text = value
emitTextChangedEvent()
}
private fun emitTextChangedEvent() {
val reactContext = context as ReactContext
reactContext.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, MyTextManager.EVENT_ON_TEXT_CHANGED, Arguments.createMap().apply {
putString("text", textProp)
})
}
}
MyText.swift
import UIKit
@objc(MyTextViewManager)
class MyTextViewManager: RCTViewManager{
override func view() -> UIView! {
return MyTextView()
}
override static func requiresMainQueueSetup() -> Bool {
return true
}
override func constantsToExport() -> [AnyHashable : Any]! {
return [:]
}
@objc
func setText(_ node: NSNumber, text: String){
DispatchQueue.main.async {
let component = self.bridge.uiManager.view(forReactTag: node) as! MyTextView
component.textProp = text
}
}
}
fileprivate class MyTextView: UILabel {
@objc
var textProp: String = "" {
didSet {
self.text = self.textProp
self.onTextChanged?(["text": self.textProp])
}
}
@objc
var onTextChanged: RCTDirectEventBlock?
required init?(coder: NSCoder) {
fatalError("Not Implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
self.font = UIFont.systemFont(ofSize: 48)
self.textAlignment = .center
self.numberOfLines = 0
}
}
NativeModules.m
Add your module in Objective-C extern bridge file.
#import "{{Your-Project-Name}}-Bridging-Header.h"
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
@interface RCT_EXTERN_MODULE(MyTextViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(textProp, NSString)
RCT_EXPORT_VIEW_PROPERTY(onTextChanged, RCTDirectEventBlock)
RCT_EXTERN_METHOD(
setText: (nonnull NSNumber *)node
text: (NSString)text
)
@end
Thanks goes to these wonderful people (emoji key):
MJ Studio 💻 📖 💡 |
This project follows the all-contributors specification. Contributions of any kind welcome!