Skip to content

mj-studio-playground/react-native-native-module-example

Repository files navigation

React Native Native Module, Native Component example

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).

  1. (No need New Arch) Plain old native module/component (this repository describes it) πŸ‘ˆ
  2. (No need New Arch, Template) Use JSI directly to communicate native(c++). Example: mmkv(JSI module package), vision-camera(JSI component package)
  3. (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.

References

Screenshots

Contents

Overview

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).


Framework versions

  • React: 16.13.1

  • React Native: 0.63.2

  • Android Gradle Plugin: 4.0.1

  • Kotlin: 1.4.0

  • Gradle: 6.2

  • Swift 5

0. Basic settings

Android

1. Configure Kotlin plugin

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 } ... }

iOS

1. Create swift, obj-c bridging header

{{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.

2. Create Objective-C extern bridge file

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

1. Native Module(Calculator)

The Calculator module has three different way to add two integer.

  • Promise
  • Callback
  • Event(Native -> JS)

React(JavaScript)

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);

Android(Kotlin)

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" } ... }

iOS(Swift, Obj-C)

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

2. Native Component(MyText)

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

React(JavaScript)

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);

Android(Kotlin)

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) }) } }

iOS(Swift, Obj-C)

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 

Contributors ✨

Thanks goes to these wonderful people (emoji key):


MJ Studio

πŸ’» πŸ“– πŸ’‘

This project follows the all-contributors specification. Contributions of any kind welcome!

About

The example project for React Native native module and native view module with Swift and Kotlin

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •