Lesson 11-React Native Native Module Development

Native Module Development

Core Concepts of Native Modules

Why Native Modules Are Needed

// Example: Scenarios requiring platform-specific functionality
// 1. Accessing device sensors (e.g., Bluetooth, NFC)
// 2. Using platform-specific APIs (e.g., Android’s JobScheduler)
// 3. Integrating third-party native SDKs
// 4. Implementing high-performance computing or graphics processing

Android Native Module Development

Creating a Simple Native Module

Step 1: Create the Native Module Class

// android/app/src/main/java/com/example/app/ToastModule.java
package com.example.app;

import android.widget.Toast;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class ToastModule extends ReactContextBaseJavaModule {
    private final ReactApplicationContext reactContext;

    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    // Required method: Returns the module name
    @Override
    public String getName() {
        return "ToastModule";
    }

    // Method exposed to JavaScript
    @ReactMethod
    public void show(String message, int duration) {
        Toast.makeText(reactContext, message, duration).show();
    }
}

Step 2: Create the Package Manager Class

// android/app/src/main/java/com/example/app/ToastPackage.java
package com.example.app;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ToastPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new ToastModule(reactContext)); // Add our module
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList(); // No view managers needed
    }
}

Step 3: Register the Package in the Main Application

// android/app/src/main/java/com/example/app/MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
        new MainReactPackage(),
        new ToastPackage() // Add our package
    );
}

Step 4: Use in JavaScript

// ToastExample.js
import { NativeModules } from 'react-native';
const { ToastModule } = NativeModules;

export default {
  showToast(message, duration = ToastModule.SHORT) {
    ToastModule.show(message, duration);
  }
};

// Usage example
import ToastExample from './ToastExample';
ToastExample.showToast('Hello from Android Native!', ToastModule.LONG);

Adding Constants to JavaScript

// In ToastModule.java
@Override
public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put("SHORT", Toast.LENGTH_SHORT);
    constants.put("LONG", Toast.LENGTH_LONG);
    return constants;
}

iOS Native Module Development

Creating a Simple Native Module

Step 1: Create the Native Module Class

// ios/YourApp/ToastModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(ToastModule, NSObject)

// Method exposed to JavaScript
RCT_EXTERN_METHOD(show:(NSString *)message duration:(NSInteger)duration)

@end
// ios/YourApp/ToastModule.h
#import <React/RCTBridgeModule.h>

@interface ToastModule : NSObject <RCTBridgeModule>
@end

Implementation File (Using Objective-C++)

// ios/YourApp/ToastModule.mm
#import "ToastModule.h"
#import <UIKit/UIKit.h>

@implementation ToastModule

RCT_EXPORT_MODULE();

// Implement the exported method
RCT_EXPORT_METHOD(show:(NSString *)message duration:(NSInteger)duration) {
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Toast"
                                                            message:message
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
        [alertView show];
        
        // iOS 9+ Use UIAlertController
        if (@available(iOS 9.0, *)) {
            UIAlertController *alertController = [UIAlertController 
                alertControllerWithTitle:@"Toast"
                                 message:message
                          preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *okAction = [UIAlertAction 
                actionWithTitle:@"OK" 
                          style:UIAlertActionStyleDefault 
                        handler:nil];
            [alertController addAction:okAction];
            [[UIApplication sharedApplication].keyWindow.rootViewController 
                presentViewController:alertController 
                             animated:YES 
                           completion:nil];
        }
    });
}

@end

Step 2: Use in JavaScript

// ToastExample.js
import { NativeModules } from 'react-native';
const { ToastModule } = NativeModules;

export default {
  showToast(message, duration = 2000) {
    // iOS does not have predefined constants; handle manually
    ToastModule.show(message, duration);
  }
};

// Usage example
import ToastExample from './ToastExample';
ToastExample.showToast('Hello from iOS Native!');

Adding Constants to JavaScript

// In ToastModule.m
RCT_EXTERN_METHOD(getConstants:(RCTResponseSenderBlock)callback)
@implementation ToastModule

RCT_EXPORT_MODULE();

- (NSDictionary *)constantsToExport {
    return @{
        @"SHORT": @0,
        @"LONG": @1
    };
}

// Or use callback approach (older API)
RCT_EXTERN_METHOD(getConstants:(RCTResponseSenderBlock)callback) {
    callback(@[@{@"SHORT": @0, @"LONG": @1}]);
}

@end

Type Conversion and Data Passing

Basic Data Type Conversion

// Android Java type conversion example
@ReactMethod
public void processData(String stringParam, int intParam, boolean boolParam, Double doubleParam) {
    // Process parameters...
}

// iOS Objective-C type conversion example
RCT_EXPORT_METHOD(processData:(NSString *)stringParam 
                  intParam:(NSInteger)intParam 
                  boolParam:(BOOL)boolParam 
                  doubleParam:(double)doubleParam) {
    // Process parameters...
}

Array and Object Passing

// Android passing array
@ReactMethod
public void processArray(ReadableArray array) {
    for (int i = 0; i < array.size(); i++) {
        String item = array.getString(i);
        // Process array item...
    }
}

// Android passing object
@ReactMethod
public void processMap(ReadableMap map) {
    String name = map.getString("name");
    int age = map.getInt("age");
    // Process object...
}
// iOS passing array
RCT_EXPORT_METHOD(processArray:(NSArray *)array) {
    for (NSString *item in array) {
        // Process array item...
    }
}

// iOS passing dictionary
RCT_EXPORT_METHOD(processMap:(NSDictionary *)map) {
    NSString *name = map[@"name"];
    NSNumber *age = map[@"age"];
    // Process object...
}

Callback Function Handling

// Android callback example
@ReactMethod
public void fetchData(Callback successCallback, Callback errorCallback) {
    try {
        // Simulate data fetch
        String data = "Sample Data";
        successCallback.invoke(data);
    } catch (Exception e) {
        errorCallback.invoke(e.getMessage());
    }
}

// Usage example (JavaScript)
ToastModule.fetchData(
  (data) => console.log('Success:', data),
  (error) => console.error('Error:', error)
);
// iOS callback example
RCT_EXPORT_METHOD(fetchData:(RCTResponseSenderBlock)successCallback
                  errorCallback:(RCTResponseSenderBlock)errorCallback) {
    @try {
        // Simulate data fetch
        NSString *data = @"Sample Data";
        successCallback(@[data]);
    } @catch (NSException *exception) {
        errorCallback(@[exception.reason]);
    }
}

// Usage example (JavaScript)
ToastModule.fetchData(
  (data) => console.log('Success:', data),
  (error) => console.error('Error:', error)
);

Promise Support

// Android Promise example
@ReactMethod
public void fetchData(Promise promise) {
    try {
        // Simulate data fetch
        String data = "Sample Data";
        promise.resolve(data);
    } catch (Exception e) {
        promise.reject("FETCH_ERROR", e);
    }
}

// Usage example (JavaScript)
async function getData() {
  try {
    const data = await ToastModule.fetchData();
    console.log('Success:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}
// iOS Promise example
RCT_REMAP_METHOD(fetchData, fetchDataWithResolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
    @try {
        // Simulate data fetch
        NSString *data = @"Sample Data";
        resolve(data);
    } @catch (NSException *exception) {
        reject(@"FETCH_ERROR", exception.reason, nil);
    }
}

// Usage example (JavaScript)
async function getData() {
  try {
    const data = await ToastModule.fetchData();
    console.log('Success:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

Event Emission and Subscription

Android Event Emission

// 1. Create event emitter
public class ToastModule extends ReactContextBaseJavaModule {
    private final ReactApplicationContext reactContext;
    private final EventEmitter eventEmitter;

    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
        this.eventEmitter = new EventEmitter(reactContext);
    }

    // 2. Implement event emission method
    @ReactMethod
    public void startTimer() {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            eventEmitter.emit("TimerEvent", "Timer finished!");
        }, 5000);
    }

    // 3. Internal event emitter class
    private static class EventEmitter {
        private final ReactContext reactContext;

        EventEmitter(ReactContext reactContext) {
            this.reactContext = reactContext;
        }

        void emit(String eventName, String message) {
            reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, message);
        }
    }
}

// JavaScript side event subscription
import { NativeEventEmitter, NativeModules } from 'react-native';
const { ToastModule } = NativeModules;
const eventEmitter = new NativeEventEmitter(ToastModule);

useEffect(() => {
  const subscription = eventEmitter.addListener('TimerEvent', (message) => {
    console.log('Received event:', message);
  });

  // Start timer
  ToastModule.startTimer();

  return () => {
    subscription.remove(); // Clean up subscription
  };
}, []);

iOS Event Emission

// iOS event emission implementation
@implementation ToastModule {
    RCTEventEmitter *_eventEmitter;
}

RCT_EXPORT_MODULE();

- (instancetype)init {
    self = [super init];
    if (self) {
        _eventEmitter = [[RCTEventEmitter alloc] init];
    }
    return self;
}

// Required event emitter method
- (NSArray<NSString *> *)supportedEvents {
    return @[@"TimerEvent"];
}

- (void)startTimer {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self sendEventWithName:@"TimerEvent" body:@"Timer finished!"];
    });
}

// Export event emitter method
RCT_EXPORT_METHOD(startTimer) {
    [self startTimer];
}

@end

// JavaScript side event subscription (iOS)
import { NativeEventEmitter, NativeModules } from 'react-native';
const { ToastModule } = NativeModules;
const eventEmitter = new NativeEventEmitter(ToastModule);

useEffect(() => {
  const subscription = eventEmitter.addListener('TimerEvent', (message) => {
    console.log('Received event:', message);
  });

  // Start timer
  ToastModule.startTimer();

  return () => {
    subscription.remove(); // Clean up subscription
  };
}, []);

Debugging and Testing Native Modules

Android Debugging Tips

// 1. Set breakpoints in Android Studio
// 2. Use Logcat to view logs
Log.d("ToastModule", "show method called with message: " + message);

// 3. Debug React Native app
//    - Run react-native run-android
//    - Attach debugger in Android Studio

// 4. Verify native module registration
//    - Check getPackages() method in MainApplication.java
//    - Ensure ToastPackage is added to the returned list

iOS Debugging Tips

// 1. Set breakpoints in Xcode
// 2. Use NSLog to view logs
NSLog(@"ToastModule show method called with message: %@", message);

// 3. Debug React Native app
//    - Run react-native run-ios
//    - Attach debugger in Xcode

// 4. Verify native module registration
//    - Check getPackages method in MainApplication.m
//    - Ensure ToastPackage is added to the returned array

JavaScript Side Testing

// 1. Unit testing native modules (using Jest)
import { NativeModules } from 'react-native';
const { ToastModule } = NativeModules;

describe('ToastModule', () => {
  it('should have show method', () => {
    expect(typeof ToastModule.show).toBe('function');
  });

  // Note: Actual calls to native methods require mocking
  // since Jest cannot directly test native code
});

// 2. Integration testing
//    - Use React Native Testing Library to test components
//    - Mock native module behavior

// 3. E2E testing
//    - Use Detox for end-to-end testing

Performance Optimization and Best Practices

Performance Optimization Tips

// Android performance optimization example
@ReactMethod
public void processDataBatch(ReadableArray data, Promise promise) {
    // 1. Use background thread for time-consuming operations
    new Thread(() -> {
        try {
            // Simulate time-consuming processing
            Thread.sleep(1000);
            
            // Process data
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < data.size(); i++) {
                result.append(data.getString(i)).append("\n");
            }
            
            // Return result
            promise.resolve(result.toString());
        } catch (Exception e) {
            promise.reject("PROCESS_ERROR", e);
        }
    }).start();
}
// iOS performance optimization example
RCT_EXPORT_METHOD(processDataBatch:(NSArray *)data 
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
    // 1. Use GCD background queue for time-consuming operations
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @try {
            // Simulate time-consuming processing
            [NSThread sleepForTimeInterval:1.0];
            
            // Process data
            NSMutableString *result = [NSMutableString string];
            for (NSString *item in data) {
                [result appendFormat:@"%@\n", item];
            }
            
            // Return result
            resolve(result);
        } @catch (NSException *exception) {
            reject(@"PROCESS_ERROR", exception.reason, nil);
        }
    });
}

Advanced Topics

TurboModules (New Architecture)

// Android TurboModules example (requires React Native 0.68+)
// 1. Define interface
public interface ToastModuleSpec extends TurboModule {
    void show(String message, int duration);
}

// 2. Implement interface
public class ToastModule extends NativeToastModuleSpec implements ToastModuleSpec {
    // Implement methods...
}

// 3. Register module (using code generation)
// No manual registration needed in MainApplication.java
// Requires build.gradle configuration to enable TurboModules
// iOS TurboModules example
// 1. Define interface (using code generation)
// ToastModule.h
#import <React/RCTTurboModule.h>

@interface ToastModule : NSObject <RCTTurboModule>
- (void)show:(NSString *)message duration:(NSInteger)duration;
@end

// 2. Implement interface
// ToastModule.mm
@implementation ToastModule
RCT_EXPORT_MODULE()

- (void)show:(NSString *)message duration:(NSInteger)duration {
    // Implementation...
}
@end

// 3. Configuration (requires React Native 0.68+)
// Enable TurboModules in Podfile

JSI (JavaScript Interface)

// Example: Using JSI for high-performance native modules
#include <jsi/jsi.h>
#include <string>

using namespace facebook;

void installJSIModule(jsi::Runtime &runtime) {
    auto showToast = [](jsi::Runtime &runtime, 
                       const jsi::Value &thisValue,
                       const jsi::Value *arguments,
                       size_t count) -> jsi::Value {
        if (count < 1 || !arguments[0].isString()) {
            return jsi::Value::undefined();
        }
        
        std::string message = arguments[0].asString(runtime).utf8(runtime);
        // In real applications, this can call native APIs to show Toast
        __android_log_print(ANDROID_LOG_DEBUG, "JSIModule", "Message: %s", message.c_str());
        
        return jsi::Value::undefined();
    };
    
    runtime.global().setProperty(
        runtime,
        "nativeShowToast",
        jsi::Function::createFromHostFunction(
            runtime,
            jsi::PropNameID::forAscii(runtime, "nativeShowToast"),
            1,
            showToast
        )
    );
}

Native UI Component Development

// Android native UI component example
public class CustomViewManager extends SimpleViewManager<View> {
    @Override
    public String getName() {
        return "CustomView";
    }

    @Override
    protected View createViewInstance(ThemedReactContext reactContext) {
        return new View(reactContext);
    }
}
// iOS native UI component example
@implementation CustomViewManager

RCT_EXPORT_MODULE()

- (UIView *)view {
    return [[UIView alloc] init];
}

@end

Summary of Native Module Development

React Native native module development serves as a bridge between JavaScript and native platform capabilities, enabling developers to access platform-specific functionality and achieve high-performance operations. Below is a summary of key points:

  1. Core Concepts:
    • Native modules allow JavaScript to call native code (Android Java/Objective-C/Swift).
    • Used for accessing platform-specific APIs, integrating third-party SDKs, or implementing high-performance features.
    • Requires establishing a communication bridge between JavaScript and native code.
  2. Development Process:
    • Android:
      1. Create a module class extending ReactContextBaseJavaModule.
      2. Implement getName() to define the module name.
      3. Use @ReactMethod to expose methods to JavaScript.
      4. Create a ReactPackage and register the module.
    • iOS:
      1. Create a class implementing the RCTBridgeModule protocol.
      2. Use RCT_EXPORT_MODULE() macro to define the module.
      3. Use RCT_EXPORT_METHOD macro to expose methods.
      4. Register the module in MainApplication.m.
  3. Data Passing:
    • Supports basic data types (String, Number, Boolean).
    • Supports array and object passing.
    • Uses callbacks or Promises for asynchronous operations.
    • Uses event emitters for JavaScript to subscribe to native events.
  4. Performance Optimization:
    • Execute time-consuming operations in background threads.
    • Minimize frequent JavaScript-native communication.
    • Use batch processing to reduce bridge calls.
    • Consider TurboModules or JSI for improved performance.
  5. Debugging and Testing:
    • Use platform-specific debugging tools (Android Studio, Xcode).
    • Perform unit testing on the JavaScript side (mocking native modules).
    • Conduct integration testing to verify interactions.
    • Consider end-to-end testing for complete workflows.
  6. Best Practices:
    • Follow platform-specific design patterns.
    • Provide clear documentation and examples.
    • Handle errors and exceptions gracefully.
    • Ensure version compatibility and backward compatibility.
    • Optimize memory usage and resource management.
  7. Advanced Topics:
    • TurboModules (new architecture) offer better type safety and performance.
    • JSI enables high-performance modules at the C++ layer.
    • Native UI component development extends React Native’s UI capabilities.
    • Sharing native code libraries improves development efficiency.

By mastering native module development, developers can leverage React Native’s cross-platform advantages while accessing platform-specific features when needed, building feature-rich, high-performance mobile applications.

Share your love