How to build react-native bridge and get PDF Viewer
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:
- NPM package react-native-view-pdf (https://www.npmjs.com/package/react-native-view-pdf)
- Gitlab sources rumax/react-native-PDFView (https://github.com/rumax/react-native-PDFView)
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
andPDFViewPackage
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 classcreateViewInstance
is 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 PDFViewPackage
can 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:
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: