React Native Bridge for Android and iOS — UI Component

React Native Bridge for Android and iOS — UI Component

This is Part 2 of React Native Bridge tutorial, Part 1 focused on bridging Native classes to React. The tutorial can be founder here.

In this tutorial, we will focus on creating cross-platform React Native component which will work in both Android and iOS.

In Part 1 of React Native Bridge, I started with iOS and then explained Android. This time I will start with Android and then explain ios. (Just to be natural 😉). But in the end, the same React UI component will work on iOS and Android.

The code of this article can be found here -> https://github.com/nalwayaabhishek/LightViewApp

Create the LightApp

To better understand Native Module we will create a simple LightViewApp example using react-native CLI.

$ react-native init LightViewApp$ cd LightApp

Next, we will create a component in Swift for iOS and Java for Android, and this will be used this app in a React component. This will be cross-platform example and the same React code will work in both iOS and Android.

As we have created a basic skeleton of the project, next we have divided this article into two sections:

Section 1 — Native Bridge UI component in Android

Section 2 — Native Bridge UI component in iOS

Section 1 — Native Bridge UI component in Android

In this section, we will focus on Android and create a bridge between Swift/Objective C and your React Component. It has these three steps:

Step 1) Create Bulb UI component

Step 2) Passing props to component

Step 3) Passing the Native state to React component

Step 1) Create a Bulb View component

To get started we will create a BulbView class in swift which will inherit from Android Button class. And then use this in our react code as component <Bulb />.

Open Android Studio and click on Open an existing Android Studio project and then select the android folder inside our LightViewApp. Once all gradle dependency is downloaded, create a Java Class BulbView.java as shown:

Once the file is created, update the following code in BulbView.java:

package com.lightviewapp;import android.content.Context;import android.graphics.Color;import android.util.AttributeSet;import android.view.View;import android.widget.Button;import com.facebook.react.bridge.Arguments;import com.facebook.react.bridge.ReactContext;public class BulbView extends Button {public BulbView(Context context) {super(context);this.setTextColor(Color.BLUE);this.setText("This button is created from JAVA code");    }public BulbView(Context context, AttributeSet attrs) {super(context, attrs);    }public BulbView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);    }}

We have created a BulbView java class which is inherited from Button, and overwrite the constructor. We can customize Button color and button text.

Next, we will create a BulbManager to expose and update the following code in BulbManager.java:

package com.lightviewapp;import com.facebook.react.uimanager.SimpleViewManager;import com.facebook.react.uimanager.ThemedReactContext;public class BulbManager extends SimpleViewManager<BulbView> {    @Overridepublic String getName() {return "Bulb";    }    @Overrideprotected BulbView createViewInstance(ThemedReactContext reactContext) {return new BulbView(reactContext);    }}

We have inherited BulbManager class from SimpleViewManager . A SimpleViewManager is a React Native interface responsible for instantiating and updating views in the app. The SimpleViewManager is a generic class that uses our view.

We always need a Custom Manager class which should be inherited from SimpleViewManager or its parent class ViewManager

And in every Manager class, there should be always these two things:

  • A method name getName, which return string name, which will be exposed to JavaScript
  • Implement createViewInstance(ThemedReactContext reactContext)method in which we create an instance of the component and return the object.

Manager class will be also used in the next section, where we will pass props from React Component.

Next step is to register the module, if a module is not registered it will not be available from JavaScript. Create a file by clicking on Menu File -> New -> Java Class and the file name as BulbPackage and then click OK. And then add the following code to BulbPackage.java

package com.lightviewapp;import com.facebook.react.ReactPackage;import com.facebook.react.bridge.NativeModule;import com.facebook.react.bridge.ReactApplicationContext;import com.facebook.react.uimanager.ViewManager;import java.util.ArrayList;import java.util.Collections;import java.util.List;public class BulbPackage implements ReactPackage  {    @Overridepublic List<NativeModule>    createNativeModules(ReactApplicationContext reactContext) {return Collections.emptyList();    }    @Overridepublic List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {return Collections.<ViewManager>singletonList(new BulbManager()        );    }}

We need to Override createNativeModules function and add the Bulb object to modules array. If this is not added here then it will not be available in JavaScript.

BulbPackage package needs to be provided in the getPackages method of the MainApplication.java file. This file exists under the android folder in your react-native application directory. Update the following code in android/app/src/main/java/com/LightViewApp /MainApplication.java

public class MainApplication extends Application implements ReactApplication {...    @Override    protected List<ReactPackage> getPackages() {      return Arrays.<ReactPackage>asList(          new MainReactPackage(),  new BulbPackage()      );    }....}

Now let’s update JavaScript code and access <Bulb> component from our React component. To do so open App.js and update with the following code:

import React, {Component} from 'react';import {StyleSheet, Text, View, requireNativeComponent} from 'react-native';
const Bulb = requireNativeComponent("Bulb")
export default class App extends Component {
render() {
 return (
   <View style={styles.container}>
     <View style={styles.top} />
     <Bulb style={ styles.bottom } />
  </View>
);}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
},
top: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
bottom: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
});

Now run the react native app:

Woo 💥 we can see <Bulb> has rendered a UI button which we have created in Java. The size and position of the button is decided from stylesheet which we have defined in React component.

Step 2) Passing props to component

In this section, we will extend Bulb component to accept props. We will add a props isOn to Bulb UI component, which will change the color of the button, we created in the last section.

<Bulb style={ styles.bottom } isOn={true}/>

First let's handle our button for accepting clicks using OnClickListener. OnClickListener registers a callback to be invoked when we do some operation on Button and onClick will be called when clicked a button. On clicking of button, we change the value of isOn. Which then will change the color and text of the button.

Open BulbView and update the following code:

package com.lightviewapp;import android.content.Context;import android.graphics.Color;import android.util.AttributeSet;import android.view.View;import android.widget.Button;import com.facebook.react.bridge.Arguments;import com.facebook.react.bridge.ReactContext;import com.facebook.react.bridge.WritableMap;import com.facebook.react.uimanager.events.RCTEventEmitter;public class BulbView extends Button {    public Boolean isOn = false;    public void setIsOn (Boolean initialBulbStatus){isOn = initialBulbStatus;        updateButton();    }
    public BulbView(Context context) {        super(context);        this.setTextColor(Color.BLUE);        this.setOnClickListener(changeStatusListener);        updateButton();    }    public BulbView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public BulbView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    private OnClickListener changeStatusListener = new OnClickListener() {        public void onClick(View v) {isOn = !isOn;            updateButton();        }    };    private void updateButton() {        if (isOn) {            setBackgroundColor(Color.YELLOW);            setText("Switch OFF");        } else {            setBackgroundColor(Color.BLACK);            setText("Switch ON");        }    }}

Next we will accept isOn prop from JavaScript and assign the value of isOn to our class Bulb. Open BulbManager.java and add the following code:

public class BulbManager extends SimpleViewManager<BulbView> {
....
@ReactProp(name="isOn")public void setBulbStatus(BulbView bulbView, Boolean isOn) {   bulbView.setIsOn(isOn);}
....}

And then update the react code in App.js:

<Bulb style={ styles.bottom }  isOn={true}/>

Now run the app in Android simulator:

We can see the state of Bulb is passed to Android. You can try to change the value of isOn=false and try to refresh the screen.

Passing the Native state to React component

Now let’s add the Bulb Status(ON or OFF) value to our React screen. To do so we will add _onStatusChange function to Bulb component and this will call when the button status is change. We will pass the function in component like this:

<Bulb style={ styles.bottom }  isOn={this.state.isOn} onStatusChange={this._onStatusChange} />

Lets get started and update BulbView.js

package com.lightviewapp;...
import com.facebook.react.bridge.Arguments;import com.facebook.react.bridge.ReactContext;import com.facebook.react.bridge.WritableMap;import com.facebook.react.uimanager.events.RCTEventEmitter;public class BulbView extends Button {....    private void changeStatus() {WritableMap event = Arguments.createMap();        event.putBoolean("isOn", isOn);        ReactContext reactContext = (ReactContext)getContext();        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(                getId(),                "statusChange",                event);        if (isOn) {            setBackgroundColor(Color.YELLOW);            setText("Switch OFF");        } else {            setBackgroundColor(Color.BLACK);            setText("Switch ON");        }    }...}

When a native event occurs the native code issue an event to the JavaScript representation of the View, and the two views are linked with the value returned from the getId() method.

To map the statusChange event name to the onStatusChange callback prop in JavaScript, register it by overriding the getExportedCustomBubblingEventTypeConstants method in your ViewManager. So lets update BulbManager.java

....import com.facebook.react.common.MapBuilder;import java.util.Map;public class BulbManager extends SimpleViewManager<BulbView> {...    @Override    public Map getExportedCustomBubblingEventTypeConstants() {        return MapBuilder.builder()                .put(                        "statusChange",                        MapBuilder.of(                                "phasedRegistrationNames",                                MapBuilder.of("bubbled", "onStatusChange")))                .build();    }}

Finally let's create _onStatusChange function in JavaScript, which will be called once the button is clicked. Update the React code in App.js:

import React, {Component} from 'react';
import {StyleSheet, Text, View, requireNativeComponent} from 'react-native';
const Bulb = requireNativeComponent("Bulb")
export default class App extends Component {
constructor(props) {
  super(props);
  this._onStatusChange = this._onStatusChange.bind(this);
  this.state = { isOn: false };
}
_onStatusChange = e => {
  this.setState({ isOn: e.nativeEvent.isOn});
}
render() {
return (
<View style={styles.container}>
<View style={styles.top} >
  <Text>This state of Bulb come from Native Code to JavaScript</Text>
  <Text>{this.state.isOn ? "Bulb is On" : "Bulb is Off"}</Text>
</View>
<Bulb style={ styles.bottom }  isOn={this.state.isOn} onStatusChange={this._onStatusChange} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
},
top: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
bottom: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
});

Now run the app in Android Simulator

Woo we can see that in the top section the state of Bulb is reflected.

Section 2 — Native Bridge UI component in iOS

In this section, we will make the same Javascript code which we used for Android to work with iOS. This time we will create Bulb View class in Swift and expose the same implementation in Javascript.

To get started we will create a BulbView class in swift, which will have a class variable isOn and a few other functions. And then we will access this swift class from Javascript as a component. Let's start by opening LightViewApp.xcodeproj file in ios folder. It should open Xcode with your ios code.

Once you open the project in Xcode, then create a new Swift file BulbView.swift as shown:

We have also clicked on Create Bridging Header which will create a file LightViewApp-Bridging-Header.h This will help to communicate between Swift and Objective C code. Remember that in a project we have only one Bridge Header file. So if we add new files, we can reuse this file.

Update following code in LightViewApp-Bridging-Header.hfile:

#import "React/RCTBridgeModule.h"
#import "React/RCTViewManager.h"
#import "React/RCTEventEmitter.h"

RCTBridgeModule will provide an interface to register a bridge module.

Next, we will update BulbView.swiftwith the following code:

import UIKit
class BulbView: UIView {
@objc var onStatusChange: RCTDirectEventBlock?
@objc var isOn: Bool = false  {
didSet {
button.setTitle(String(describing: isOn ? "Switch Off" : "Switch On"), for: .normal)
button.backgroundColor = isOn ? .yellow : .black
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init has not been implemented")
}
lazy var button: UIButton = {
let button = UIButton.init(type: UIButton.ButtonType.system)
button.titleLabel?.font = UIFont.systemFont(ofSize: 20)
button.autoresizingMask = [.flexibleWidth, .flexibleHeight]
button.addTarget(
self,
action: #selector(changeBulbStatus),
for: .touchUpInside
)
return button
}()
@objc func changeBulbStatus() {
isOn = !isOn as Bool
onStatusChange!(["isOn": isOn])
}
}

We have created a Bulb View which is inherited from UIButton. We have also created a variable isOn which define the color and text of the button. Clicking on the button will call changeBulbStatus method.

Now create Bulb.swift as Swift file:

And update the following code

import Foundation
@objc(Bulb)
class Bulb: RCTViewManager {
override func view() -> UIView! {
  return BulbView()
}
}

We have created Bulb class which is inherited from RCTViewManager. The root class of most View class in React Native hierarchies is RCTViewManager, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects. We can see that we have used @objc before a function and class, this will make that class, function or object available to Objective C

The @objc attribute makes your Swift API available in Objective-C and the Objective-C runtime.

Now create a new file from File -> New -> File and select Objective-C file and then name the file as Bulb.m and update following code:

#import "React/RCTViewManager.h"
@interface RCT_EXTERN_MODULE(Bulb, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(isOn, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onStatusChange, RCTDirectEventBlock)
@end

React Native will not expose any function of Bulb to React JavaScript unless explicitly done. To do so we have used RCT_EXPORT_METHOD() macro. So we have exposed Bulb class and property to our Javascript code. Since Swift object is converted to Javascript object, there is a type of conversation. RCT_EXPORT_METHOD supports all standard JSON object types:

  • NSString to string
  • NSInteger, float, double, CGFloat, NSNumber to number
  • BOOL to boolean
  • NSArray to array
  • NSDictionary to object with string keys and values of any type from this list
  • RCTResponseSenderBlock to function

Important Note: If you have skipped the Android part of this post, then you need to copy the React code in App.js.

Now run the application in iOS Simulator:

Now let’s fix the warning shown at the bottom of the simulator and in browser console:

Module Bulb requires main queue setup since it overrides `init` but doesn’t implement `requiresMainQueueSetup`. In a future release React Native will default to initializing all native modules on a background thread unless explicitly opted-out of.

To understand it better let’s understand about all the thread React Native runs:

  • Main thread: where UIKit work
  • Shadow queue: where the layout happens
  • JavaScript thread: where your JS code is actually running

Every native module has its own GCD Queue unless it specifies otherwise. Now since this Native module will run on a different thread and our main thread is depended on it, it’s showing this warning. And to make this code to run on MainQueue, open Bulb.swift and add this function.

@objc
static func requiresMainQueueSetup() -> Bool {
return true
}

You can explicitly mention return false to run this in separate threat.

Woo!! We have created a Native Bridge UI which is Cross Platform. If you want to read about Native Bridge out here. Hope you find this useful. Please share your thoughts in comments. If you liked the post, please share and give few claps👏

React Native Bridge for Android and iOS — UI Component was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.

Publication date: 
01/11/2019 - 12:16