# Bridge Existing iOS & Android Apps with React Native
## Today
- Our path to React Native
- Overview of Embedded React Native
- Problems
- Solutions
- Problems
- Solutions
- Thoughts on our future
## We Needed
- A screen rewrite existing in iOS/Android companion apps
- Nearly double the screens in those apps
- On both platforms
- Within ~3 weeks
## What About React Native?
- Consolidate UI
- Double developer resources
- Leverage Yoga (_flexbox_) to fit our design
- Remotely affect UI/functionality?
Demo
Which parts are React Native?
## Where Is Best?
- Leave existing code intact
- New UIViewControllers, Activities
- New UIViews, Fragments
## Where Is Trouble?
- Installation will [probably] be a pain
- Days in our case
- Heavy API integration within the UI
- Slows integration
- Support of very old Android devices
- Erodes features & responsiveness
## The Installation Process
- Generally able to
- [Follow docs on facebook.github.io][0]
- But...
[0]: http://facebook.github.io/react-native/releases/0.46/docs/integration-with-existing-apps.html
## Separate Repos
- To leverage React testing
- To accomodate separate languages/structures
- To allow for separate release cycles
~~~
~/src/
|_ ios-field-staff
|_ android-field-staff
|_ rn-field-staff
~~~
## iOS Installation
- Clone iOS
- Install latest cocoapods
- Clone RN
- Install node modules
- Symlink to iOS repo
- Start RN bundle server
- Run iOS simulator
## Android Installation
- Clone Android
- Update and gradle install
- Clone RN
- Install node modules
- Symlink to Android repo
- Start RN bundle server
- Run Android simulator
## Embedded React Native
An overview of React Native and native communication
~~~
Props
+----------------+ <----------------+
| | |
| React Native | |
| View | +--------+---------+
| | | |
+-------+--------+ | Native Control |
| | |
| +----------+ +------------------+
+--> | |
| Native |
| Bridge |
| |
+----------+
~~~
## Where Is Your Time Spent?
- 1/3 in React Native
- 1/3 building bridges
- 1/3 massaging props
## Loading RN Views
- Construct with the native lifecycle in mind
- Build a dedicated function to create props
- With empty state as default
## Loading iOS RN View
Instantiate the RN view with [bare] props
~~~swift
class RunningLateViewController: UIViewController {
var runningLateViewRN: RCTRootView?
var runningLateViewRNURL = "an URL or bundle"
override func loadView() {
super.loadView()
self.runningLateViewRN = RCTRootView(
bundleURL: self.runningLateViewRNURL,
moduleName: "RunningLate",
initialProperties: self.buildProps()
as [NSObject : AnyObject],
launchOptions: nil
)
self.view = self.runningLateViewRN!
}
// ...
}
~~~
## Loading iOS RN View, Continued
Asynchronous loading of new props
~~~swift
//...
class RunningLateViewController: UIViewController {
// ...
func buildProps(cleaning) -> NSDictionary {
// build our props here...
}
func fetchAPICall() {
// call our api, then...
DispatchQueue.main.async() {
(self.view as! RCTRootView).appProperties =
self.buildProps() as [NSObject : AnyObject]
}
}
}
~~~
## Loading Android RN View
Instantiate the RN view with [bare] props
~~~java
public class RNRunningLateActivity extends AppCompatActivity {
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rn_running_late);
mReactInstanceManager = ReactInstanceManagerAPI.getInstance()
.buildReactInstanceManager("running-late.shared.bundle",
"running-late.shared.js");
mReactRootView = (ReactRootView) findViewById(R.id.react_root);
mReactRootView.startReactApplication(mReactInstanceManager,
"RunningLateShared", this.buildProps(null));
}
// ...
}
~~~
## Loading Android RN View, Continued
Asynchronous loading of new props
~~~java
public class RNRunningLateActivity extends AppCompatActivity {
// ...
private Bundle buildProps(JSONArray data) {
// Build props...
}
void fetchAPICall() {
// call api, then...
Bundle props = buildProps(data);
mReactRootView.setAppProperties(props);
}
// ...
}
~~~
## Loading Android RN View, Continued
Start asynchronous props in native lifecycle
~~~java
public class RNRunningLateActivity extends AppCompatActivity {
// ...
@Override
protected void onResume() {
super.onResume();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostResume(this, this);
}
fetchAPICall();
}
@Override
protected void onPause() {
super.onPause();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostPause(this);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostDestroy();
}
}
@Override
public void invokeDefaultOnBackPressed() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onBackPressed();
} else {
super.onBackPressed();
}
}
}
~~~
## Example RN View
Such lovely, concise UI logic
~~~js
export default class RunningLateShared
extends React.PureComponent {
// ...
render() {
return (
<View style={styles.container}>
<Text style={styles.shiftTime}>
{formatTime(this.props.shift.start)}
</Text>
{this.renderAvatar(styles.staffImageWrapTop)}
<View style={styles.noteView}>
{!this.state.submitted &&
// ...
}
{this.state.submitted &&
// ...
}
</View>
</View>
);
}
}
~~~
## Building Bridges
- Get in.
- Get out.
- Embrace the _single direction_ of props
## Example React Native Bridge
Always check for bridge methods
~~~js
import { NativeModules } from 'react-native';
const bridge = NativeModules.CurrentCleaningBridge;
const isImplemented = (name) => {
if (!bridge || !bridge[name]) {
console.log(name, 'bridge not implemented');
return false;
}
return true;
}
export const componentDidMount = () => {
if (!isImplemented('componentDidMount')) return;
bridge.componentDidMount();
};
~~~
## Example React Native Bridge With Args
Always fall back to default args
~~~js
import { NativeModules } from 'react-native';
const bridge = NativeModules.CurrentCleaningBridge;
export const onSubmit = (note) => {
if (!isImplemented('onSubmit')) return;
bridge.onSubmit(note || '');
};
export const onRootLayout = (x, y, width, height) => {
if (!isImplemented('onRootLayout')) return;
bridge.onRootLayout(x || 0, y || 0, width || 0, height || 0);
};
~~~
## Example React Native Bridge Usage
Call the bridge as you would HTTP
~~~js
export default class Foo extends React.PureComponent {
_onLayout = (event) => {
const { x, y, width, height } =
event.nativeEvent.layout;
bridge.onRootLayout(x, y, width, height);
}
render() {
return (
<View onLayout={this._onLayout}>
</View>
}
}
~~~
## Example Swift iOS Bridge
- Leverage Swift
- Expose modules in Objective-C
## Obj-C Bridge Implementation
Decalre visisble methods and args
~~~objc
#import "CurrentCleaningBridgingHeader.h"
#import "React/RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(CurrentCleaningBridge, NSObject)
RCT_EXTERN_METHOD(onSubmit:(NSString * __nonnull)note)
RCT_EXTERN_METHOD(onRootLayout:(NSNumber * __nonnull)x y:(NSNumber * __nonnull)y width:(NSNumber * __nonnull)width height:(NSNumber * __nonnull)height)
RCT_EXTERN_METHOD(componentDidMount)
@end
~~~
## Obj-C Bridge Header
Defer to Swift auto-gen headers
~~~objc
// Look to the Swift definition...
#ifndef CurrentCleaningBridgingHeader_h
#define CurrentCleaningBridgingHeader_h
#import "React/RCTBridge.h"
#import "React/RCTBridgeModule.h"
#import "React/RCTEventDispatcher.h"
#endif
~~~
## Swift Bridge
Handle basic message passing
~~~swift
import Foundation
import Firebase
@objc(CurrentCleaningBridge)
class CurrentCleaningBridge: NSObject {
@objc func componentDidMount() -> Void {
// good for nothing...
}
@objc func onRootLayout(_ x: NSNumber, y: NSNumber, width: NSNumber, height: NSNumber) -> Void {
RNEmbeddedViews.shared()
.triggeredLayout(RootView: "CurrentCleaning",
x: x, y: y, width: width, height: height)
}
}
~~~
## Swift Bridge, Continued
Delegate actual logic to other objects
~~~swift
@objc(CurrentCleaningBridge)
class CurrentCleaningBridge: NSObject {
//...
@objc func onSubmit(_ note: NSString) -> Void {
FIRAnalytics.logEvent(withName: Constant.Analytics.Events.updateCheckinNote,
parameters: [:])
StaffAPIClient.sharedClient()
.updateCurrentCheckinNote(note: note as String)
{ (checkin, error) in
if (error != nil) {
// Handle err....
}
}
}
}
~~~
## Example Java Android Bridge
Declare namespace and accept context
~~~java
// import ...
public class CurrentCleaningBridge extends ReactContextBaseJavaModule {
private static final String TAG = "CurrentCleaningBridge";
public CurrentCleaningBridge(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "CurrentCleaningBridge";
}
}
~~~
## Example Java Android Bridge, Continued
Handle basic message passing
~~~java
public class CurrentCleaningBridge extends ReactContextBaseJavaModule {
// ...
@ReactMethod
public void componentDidMount() {
// good for nothing...
}
@ReactMethod
public void onRootLayout(float x, float y, float width, float height) {
CurrentCleaningSingleton.getInstance()
.onRootLayout("CurrentCleaning", x, y, width, height);
}
}
~~~
## Example Java Android Bridge, Continued
Delegate actual logic to other objects
~~~java
public class CurrentCleaningBridge extends ReactContextBaseJavaModule {
// ...
@ReactMethod
public void onSubmitNote(String note) {
StaffAPI.getInstance()
.updateCheckinNote(getReactApplicationContext(), note,
new APICallbackInterface() {
@Override
public void onResponse(VolleyError error, JSONObject response) {
if (error != null) {
// handle error...
}
}
});
}
}
~~~
## Communicating to Parent View
- Special bridge case
- Bridges are ad-hoc instances
- How to tell a parent of variable resize?
## RN Bridge Singleton
A way to communicate back to our parent
~~~
Props
+----------------+ <----------------+
| | |
| React Native | |
| View | +--------+---------+
| | | |
+-------+--------+ | Native Control |
| | |
| +----------+ +------------------+
+--> | |
| Native | ^
| Bridge | |
| +-----------+
+----------+ Bridge Singleton
~~~
## Example Java Embedded View Singleton
Declare interface and create singleton
~~~java
public interface RNEmbeddedViewInterface {
void onRootLayout(String rootView, float x, float y, float width, float height);
}
public class RNEmbeddedViewSingleton {
private static final RNEmbeddedViewSingleton ourInstance
= new RNEmbeddedViewSingleton();
public static RNEmbeddedViewSingleton getInstance() {
return ourInstance;
}
private RNEmbeddedViewSingleton() {
}
// ...
}
~~~
## Example Java Embedded View Singleton
Handle listener registration
~~~java
// ...
public class RNEmbeddedViewSingleton {
// ...
private RNEmbeddedViewInterface mListener;
public void registerListener(RNEmbeddedViewInterface listener) {
this.mListener = listener;
}
public void removeListener(RNEmbeddedViewInterface listener) {
if (mListener == listener) {
this.mListener = null;
}
}
// ...
}
~~~
## Example Java Embedded View Singleton, Continued
Call listener with bridge method
~~~java
// RNEmbeddedViewSingleton.getInstance().registerListener(this);
public class RNEmbeddedViewSingleton {
//...
public void onRootLayout(String rootView, float x, float y, float width, float height) {
if (mListener == null) {
return;
}
mListener.onRootLayout(rootView, x, y, width, height);
}
}
~~~
## Example Swift Embedded View Singleton
Declare interface and class
~~~swift
struct RNViewLayout {
let x: NSNumber
let y: NSNumber
let width: NSNumber
let height: NSNumber
}
protocol RNEmbeddedViewsDelegate {
func onLayout(RootView: NSString, dimensions: RNViewLayout)
}
class RNEmbeddedViews: NSObject {
// ...
}
~~~
## Example Swift Embedded View Singleton, Continued
Configure delegate registration
~~~swift
// ...
class RNEmbeddedViews: NSObject {
var delegate: RNEmbeddedViewsDelegate?
class func shared() -> RNEmbeddedViews {
struct Singleton {
static let instance = RNEmbeddedViews()
}
return Singleton.instance
}
// ...
// RNEmbeddedViews.shared().delegate = self
}
~~~
## Example Swift Embedded View Singleton, Continued
Call delegate with bridge method
~~~swift
// ...
class RNEmbeddedViews: NSObject {
// ...
func triggeredLayout(RootView: NSString, x: NSNumber, y: NSNumber, width: NSNumber, height: NSNumber) {
let dimensions = RNViewLayout(x: x, y: y, width: width, height: height)
self.delegate?.onLayout(RootView: RootView, dimensions: dimensions)
}
}
~~~
## Finding Platform Differences
- Custom navigation control
- Text inputs
- Tactile inputs
- Accessibility features
- Some flexbox layouts
- Overlapping fails on Android
- Conflicts with Flat UI
## Juggling Platform Differences
- Platform wrappers with shared.js within
~~~
____________ ___________
| android.js | | ios.js |
| | | |
| loads ____|_|____ loads |
|_______| shared.js |______|
| |
| Main UI |
|___________|
~~~
## Debugging Crash Reports
- iOS has been easy
- Pain & confusion
- `Caused by: java.lang.IllegalArgumentException:`
- Serialization too large?
- Bad memory cleanup?
## Future Requests
- Break down the debug wall
- Better docs on embedded usage
- Reproducible installations
- Upgrade paths
- Ability to pre-render views
## Future Prospects
- All apps in React Native?
- Is native development still relevant?
- What will our next app contain?
_We are not enemies, but friends. We must not be enemies._
## Get Started
- Create a RN repo
- Link to your native app(s)
- Choose a simple view/fragment
- Rewrite it in React
- Pass it props
- Build a bridge
- Repeat!
## Thanks!
https://www.andjosh.com/presents
https://officeluv.github.io