How to implement a React Native UI component method in Android
Asked Answered
V

2

8

It's clear to me that for react-native native modules we can use the @ReactMethod to export a method and call it from JSX, but how do we do the same thing in react-native native UI components?

In the documentation I only see @ReactProp being mentioned. If @ReactMethod is not working, how do I access a property of my native UI component from JSX then? (On iOS this can be done on native ui components with RCT_EXPORT_METHOD but on Android is something similar possible?)

Thank you.

Verleneverlie answered 22/11, 2017 at 17:25 Comment(0)
V
13

Ok I ended up creating a Module, and passing a UI Component reference on its constructor:

Here's my UI component:

public class RCTACCalendarManager extends ViewGroupManager<RCTACCalendar> {
    public static final String REACT_CLASS = "RCTACCalendar";
    private RCTACCalendar mCalendarInstance;

    public RCTACCalendarManager(ReactApplicationContext reactContext) {
        super();
    }

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    public RCTACCalendar createViewInstance(ThemedReactContext context) {
        mCalendarInstance = new RCTACCalendar(context);
        return mCalendarInstance;
    }

    public RCTACCalendar getCalendarInstance() { // <-- returns the View instance
        return mCalendarInstance;
    }
}

Here's the Module I created for that component:

public class RCTACCalendarModule extends ReactContextBaseJavaModule {
    private RCTACCalendar mCalendarInstance;

    public RCTACCalendarModule(ReactApplicationContext reactContext, RCTACCalendarManager calManager) {
        super(reactContext);
        if (calManager != null) {
            mCalendarInstance = calManager.getCalendarInstance();
        }
    }

    @Override
    public String getName() {
        return "ACCalendarManager";
    }

    @ReactMethod
    public void mySuperDuperFunction(Promise promise) {
        if (mCalendarInstance != null) {
            mCalendarInstance.mySuperDuperFunction(promise); // <-- Magic
        }
    }
}

and here's how I combine those two together in my Package declaration:

public class RCTACCalendarPackage implements ReactPackage {
    private RCTACCalendarManager mCalManager;

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        if (mCalManager == null) {
            mCalManager = new RCTACCalendarManager(reactContext);
        }
        return Arrays.<NativeModule>asList(new RCTACCalendarModule(reactContext, mCalManager));
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        if (mCalManager == null) {
            mCalManager = new RCTACCalendarManager(reactContext);
        }
        return Arrays.<ViewManager>asList(mCalManager);
    }
}

It works like a charm.

Verleneverlie answered 6/2, 2018 at 21:59 Comment(5)
I'm trying your method. Android can not export a method and then use NativeModules.YourNativeModule to get the method in js layer. It's really not cool!Dibri
Stellar answer. Would have taken me ages to work this out myself.Arbitral
public class RCTACCalendarModule ...... private RCTACCalendarManager calendarManager; public RCTACCalendarModule(ReactApplicationContext reactContext, RCTACCalendarManager calManager) { super(reactContext); calendarManager = calManager; }........ @ReactMethod public void mySuperDuperFunction(Promise promise) { calendarInstance = calendarManager. getCalendarInstance() if (calendarManager != null) { calendarInstance.mySuperDuperFunction(promise); } }Kelsiekelso
Genius! I have struggled for a whole day trying to make it work through UIManager. Using your way, I have been able to implement it in 5 minutes without any issue. Thank you! One thing though. I'm not caching a pointer to the view, but to the viewManager. In my case view is nil at the time when Module is initialised.Interval
Your solution allows only one instance of the view at the same time. IMHO the answer by Nick is better as it is allowing several instances.Plasty
A
10

The existing answer works great if you only need to support Android, but I found it didn't work when I was trying to integrate with iOS as well. I burnt quite a lot of time trying to wrangle this method into iOS, so I'll recommend what I came up with instead: using the UIManager that comes with react-native.

React Native Component

// ComponentWithNativeFunctionality.js

import {UIManager, findNodeHandle} from 'react-native';

class ComponentWithNativeFunctionality extends React.Component {
  const myRef = React.createRef();

  functionToCall = () => {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.myRef.current),
      "nameOfFunctionToCallInNativeLand",
      [/* additional arguments */]
    );
  }

  render() {
    return <NativeComponentView ref={this.myRef} />
  }
}

Android

// YourViewManager.java

public class YourViewManager extends SimpleViewManager<YourView> {
    // ...

    @Override
    public void receiveCommand(@NonNull YourView view, String commandId, @Nullable ReadableArray args) {
        super.receiveCommand(view, commandId, args);
            switch (commandId) {
                case "nameOfFunctionToCallInNativeLand":
                    view.nameOfFunctionToCallInNativeLand();
                    break;
            }
        }
    }
}

iOS (with Swift)

  1. Add #import "React/RCTUIManager.h" to your Bridging-Header.h
// YourViewManager.m

@interface RCT_EXTERN_MODULE(YourViewManagerClass, RCTViewManager)

//...

RCT_EXTERN_METHOD(
    nameOfFunctionToCallInNativeLand: (nonnull NSNumber *)node
)

@end
// YourViewManagerClass.swift

@objc(YourViewManagerClass)
class YourViewManagerClass: RCTViewManager {
    @objc func nameOfFunctionToCallInNativeLand(_ node: NSNumber) -> Void {
        DispatchQueue.main.async {
            let component = self.bridge.uiManager.view(
              forReactTag: node
            ) as! MisnapCameraView
            component.nameOfFunctionToCallInNativeLand()
        }
    }
}

Another note: you can't pass in a Promise like you can with modules. You will have to pass in a unique ID generated in JS, and then once your action is done, fire an event to bubble back the result to JS with the ID attached to the event.

Aceto answered 8/4, 2020 at 18:51 Comment(2)
This is IMHO superior to the accepted answer as it allows two instances of the same View.Plasty
I was doing a deep dive into ReactNative and I can say that this is the correct approach, dispatchViewManagerCommand has some internal logic to validate that the view has been initialized. When I tried to use the same ViewManager as a module on Android I ran into situations where the view wasn't initialized sometimes.Auxiliaries

© 2022 - 2024 — McMap. All rights reserved.