How to build react-native bridge and get PDF Viewer

Maksym Rusynyk
DailyJS
Published in
6 min readApr 24, 2018

React Native allows to build native applications using JavaScript language and has vast amount of components and functionality available. But some components or features are not available by default. And sometimes it is necessary to improve the performance of some components. In these cases native implementation can be used.

This article describes how to make a bridge between JavaScript and a native implementation, and provides an example how to build a PDF Viewer for React Native to render PDF documents.

All sources are available in gitlab or as NPM package:

Getting started

First prepare a project. Open a terminal and run:

$ create-react-native-app react-native-PDFView
$ cd react-native-PDFView
$ yarn eject

Check that project works:

$ yarn run android

and open it in the IDE. I prefer Atom, therefore in terminal I can do:

$ atom .

Open Android Studio to work with Java sources.

Java implementation

In Android Studio:

  • create new package reactlibrary
  • create new Java classes PDFView, PDFViewManager and PDFViewPackage

PDFViewPackage

According to official documentation PDFViewPackage should implements ReactPackage and missing createNativeModules and createViewManagers methods should be added:

public class PDFViewPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(new PDFViewManager(reactContext));
}
}

Android Studio will highlight an error PDFViewManager cannot be applied to ..., which has to be fixed by implementing PDFViewManager.

PDFViewManager

PDFViewManager should extends SimpleViewManager<PDFView> and implements missing methods:

  • getName which - should return the name of the react class
  • createViewInstanceis a place where the viewer should be initialized

The code is:

public class PDFViewManager extends SimpleViewManager<PDFView> {
private static final String REACT_CLASS = "PDFView";
private PDFView pdfView = null;
private Context context;
public PDFViewManager(ReactApplicationContext context) {
this.context = context;
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public PDFView createViewInstance(ThemedReactContext context) {
if (pdfView == null) {
pdfView = new PDFView(context);
}
return pdfView;
}
}

with the error that PDFView should extend View, which has to be fixed by implementing PDFView.

PDFView

PDFView should extends android.view.View and the implementation is pretty simple:

public class PDFView extends android.view.View {
private ThemedReactContext context;
public PDFView(ThemedReactContext context) {
super(context);
this.context = context;
}
}

At this moment all classes are ready and PDFViewPackagecan be registered.

Register PDFViewPackage

In MainApplication.java there is the getPackages method. Right below MainReactPackage the PDFViewPackage package can be registered:

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new PDFViewPackage()
);
}

Java initial implementation is ready!

JavaScript implementation

In Atom create a new folder PDFView and create two files:

  • index.js
  • RNPDFView.js

PDFView/RNPDFView.js

This is main file where the PDFView react native component should be required and component interface has to be defined:

import { requireNativeComponent, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
const componentInterface = {
name: 'PDFView',
propTypes: {
...ViewPropTypes,
},
};
export default requireNativeComponent('PDFView', componentInterface);

At this moment only ViewPropTypes are used to check that the component is properly defined and can be used. All other properties will be added later.

PDFView/index.js

This file is optional, and can be used as wrapper component, to handle error/loading states, or to use flow types, etc:

/* @flow */
import React from 'react';
import RNPDFView from './RNPDFView';
type Props = {};class PDFView extends React.Component<Props, *> {
static defaultProps = {};
constructor(props: Props) {
super(props);
}
render() {
return <RNPDFView {...this.props} />;
}
}
export default PDFView;

App.js

The bridge is ready and can be imported and used:

import PDFView from './PDFView/index';...return (
<View style={styles.container}>
<PDFView style={styles.pdfView} />
</View>
);

Since it implements android.view.View in Java, it also should behave as a View react-native component. This can be checked by defining styles:

const styles = StyleSheet.create({
pdfView: {
height: 400,
width: 300,
backgroundColor: 'green',
},
...

Restart the app and you will get a green rectangle like this:

Styling the component

Actually that’s all for the bridge, except properties and callbacks. And if they are not required it is already possible to implement the component in Java.

PDF Viewer implementation

To implement PDF Viewer it is necessary to know:

  • resource — String value to define the resource to render. Can be url, filePath or base64 data
  • resourceType - String value to define the resource type

And of course get some feedback from native part to JavaScript:

  • onError - Callback function that has to be is invoked on error
  • onLoad - Callback function that has to be is invoked on load completed

Pass properties from JavaScript

In PDFView/RNPDFView.js extend the componentInterface.propTypes with the properties described above:

const componentInterface = {
name: 'PDFView',
propTypes: {
onError: PropTypes.func,
onLoad: PropTypes.func,
resource: PropTypes.string,
resourceType: PropTypes.string,
...ViewPropTypes,
},
};

Do the same in PDFView/index.js but using flow types:

type Props = {
onError: (Error) => void,
onLoad: () => void,
resource: string,
resourceType: 'url' | 'base64' | 'file',
};

And define method onError to pass only nativeEvent from the component:

onError: (error: Error) => void;  // Flow type definition
onError(event: any) {
this.props.onError(event && event.nativeEvent || new Error());
}

Do not forget to bind this in constructor:

constructor(props: Props) {
super(props);
this.onError = this.onError.bind(this);
}

And fix the render method:

render() {
const { onError, ...remainingProps } = this.props;
return <RNPDFView {...remainingProps} onError={this.onError} />;
}

Also default properties can be defined to simplify the usage of the component:

static defaultProps = {
onError: () => {},
onLoad: () => {},
resourceType: 'url',
};

The JavaScript implementation is finished now.

Get and use properties in Java

Switch to Android Studio and open PDFViewManager class. This is the place where all properties, defined in JavaScript, can be accessed. Define resource:

@ReactProp(name = "resource")
public void setResource(PDFView pdfView, String resource) {
pdfView.setResource(resource);
}

And the same for resourceType (Callbacks will be implement later):

@ReactProp(name = "resourceType")
public void setResourceType(PDFView pdfView, String resourceType) {
pdfView.setResourceType(resourceType);
}

Implementation in PDFView class will be:

private String resourceType;
private String resource;

public void setResource(String resource) {
this.resource = resource;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}

In PDFViewManager find the following methods:

  • onAfterUpdateTransaction this is the place where the component should be rendered after all properties are set:
@Override
public void onAfterUpdateTransaction(PDFView pdfView) {
super.onAfterUpdateTransaction(pdfView);
pdfView.render();
}
  • onDropViewInstance this is another method that is called when the React Native component is unmounted. The implementation can be the following:
@Override
public void onDropViewInstance(PDFView pdfView) {
super.onDropViewInstance(pdfView);
pdfView.onDrop();
}

Use JavaScript callbacks in Java

To use load complete, error callbacks it is necessary to pass this events. Open PDFView.java and define loadComplete and onError methods:

@Override
public void loadComplete(int numberOfPages) {
reactNativeEvent("onLoad", null);
}
@Override
public void onError(Exception ex) {
reactNativeEvent("onError", "error: " + t.getMessage());
}

and implement reactNativeEvent method. According to official documentation it should be:

private void reactNativeEvent(String eventName, String message) {
WritableMap event = Arguments.createMap();
event.putString("message", message);
ReactContext reactContext = (ReactContext) this.getContext();
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(this.getId(), eventName, event);
}

And in PDFViewManager

public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder
.builder()
.put(
"onLoad",
MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onLoad"))
)
.put(
"onError",
MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onError"))
)
.build();
}

The implementation of the bridge is ready! The only thing left to do is to implement PDFViewer.java. I used AndroidPdfViewer implementation and final solution you can find in gitlab (https://github.com/rumax/react-native-PDFView).

Results

The implementation of the bridge and PDF Viewer is ready. Define the document you would like to render:

<View style={styles.container}>
<PDFView
style={styles.pdfView}
onError={error => console.log('onError', error) }
onLoad={() => console.log('onLoad') }
resource="http://www.pdf995.com/samples/pdf.pdf" />
</View>

..reload the application and instead of green rectangle you will get PDF document:

PDF Viewer results

--

--