In this article, we will learn how to convert our Android Java View components to React Native. This is especially interesting if we need to use a Java UI library or implement a custom View component in our React Native app.
React native offers two binding options for Native Android.
Native Modules
Native UI Components
The first one calls a native operation from the Javascript side, e.g. establishes a Bluetooth connection, reads a value from a sensor, etc.
The second one (the one covered here) is for UI components, Views, or Custom Views. This includes any UI widget we might want to use in our React Native app, such as any autocomplete widget we find or a custom implementation of the View class that we have.
Unless we have an existing React Native project, the first step will be to init our app. In our case, this will be the material_calendarview
project.
Look for the MainApplication.java
file inside the android->app->src->main->java->com->[project name]
folder.
Now, we can create a ViewManager
inside that package, or we can create a new package in that directory. A ViewManager
is a React Native interface responsible for instantiating and updating views in our apps.
For our example, I decided to use the library https://github.com/prolificinteractive/material-calendarview
First, we create the MaterialCalendarViewManager
that extends the SimpleViewManager
, the easier ViewManager
implementation. The SimpleViewManager
is a generic class that receives the View we want to implement – we can use ImageView
, EditView
, TextView
, OurCustomImplementationView
, etc.
When preparing our initial setup for the component, we need to implement the methods getName, which will return a String constant we use to get the manager from React Native, and createViewInstance
. In this case, for the Material CalendarView, we set the default date to be today, using the Date
class in Java.
public class MaterialCalendarViewManager extends SimpleViewManager<MaterialCalendarView> {
public static final String REACT_CLASS = "MaterialCalendarView";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected MaterialCalendarView createViewInstance(ThemedReactContext reactContext) {
MaterialCalendarView materialCalendarView = new MaterialCalendarView(reactContext);
materialCalendarView.setSelectedDate(new Date());
return materialCalendarView;
}
}
Now, we need to register our ViewManager
using a Package Module, so we can call it from React Native. We only need to create a file and implement the ReactPackage
interface.
In the createViewManagers
method, we instantiate our MaterialCalendarViewManager
or any implementation we want to expose to React Native.
public class MaterialCalendarViewPackage implements ReactPackage {
@Override
public List<NativeModule>
createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager>
createViewManagers(ReactApplicationContext reactContext) {
return Collections.<ViewManager>singletonList(
new MaterialCalendarViewManager()
);
}
}
We are almost done with the basic setup for the Java side. Now, we should look for the MainApplication.java
we get from the react-native init
command.
We edit this file and add our new package module to the getPackages
method. Thus, the base Java side changes should be ready.
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MaterialCalendarViewPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
Let us go the JS side. Now, we can create a JS file and implement the requireNativeComponent
function, which receives two parameters. The first one, the String
name, we set in the ViewManager
in step 1. The second one is an object (iface
) that describes the component or a custom wrapper component.
// MaterialCalendarView.js
import { PropTypes } from 'react';
import { requireNativeComponent, View } from 'react-native';
var iface = {
name: 'MaterialCalendarView',
PropTypes: {
...View.propTypes // include the default view properties
}
}
module.exports = requireNativeComponent('MaterialCalendarView', iface);
The name property will be a friendly name for debugging and the propTypes the property we set for the view. At this time, we do not have any custom properties yet, but we always implement all the react-native View
package default properties (line 9), since they contain all of the CSS and layout props we use in our React Native widgets. Example: Flexbox
.
Since we work in a dynamic world, we always need the option to set custom options for any component. This is also the case for our calendar – we want the option to set a custom date. Let us add some properties to set the day, month and year.
First, let us return to our MaterialCalendarViewManager.java
file. We can implement the setters using the @ReactProp
decorator and set the name we want to use in our JSX files.
For the second parameter, only the following types of values are currently supported: boolean
, int
, float
, double
, String
, Boolean
, Integer
, ReadableArray
, ReadableMap
.
@ReactProp(name = "day")
public void setDay(MaterialCalendarView view, int day) {
Calendar cal = view.getSelectedDate().getCalendar();
cal.set(Calendar.DAY_OF_MONTH, day);
Date dateRepresentation = cal.getTime();
view.setSelectedDate(dateRepresentation);
}
@ReactProp(name = "month")
public void setMonth(MaterialCalendarView view, int month) {
Calendar cal = view.getSelectedDate().getCalendar();
cal.set(Calendar.MONTH, month - 1);
Date dateRepresentation = cal.getTime();
view.setSelectedDate(dateRepresentation);
}
@ReactProp(name = "year")
public void setYear(MaterialCalendarView view, int year) {
Calendar cal = view.getSelectedDate().getCalendar();
cal.set(Calendar.YEAR, year);
Date dateRepresentation = cal.getTime();
view.setSelectedDate(dateRepresentation);
}
We only need to add our new props to the iface
description object.
// MaterialCalendarView.js
import { PropTypes } from 'react';
import { requireNativeComponent, View } from 'react-native';
var iface = {
name: 'MaterialCalendarView',
PropTypes: {
day: PropTypes.number,
month: PropTypes.number,
year: PropTypes.number,
...View.propTypes // include the default view properties
}
}
module.exports = requireNativeComponent('MaterialCalendarView', iface);
Now, we want to know when a custom date is set on our calendar. The Material CalendarView has the setOnDateChangedListener
we can subscribe to for all the calendar changes, but how do we send these changes to our React Native app?
First, let us go to our MaterialCalendarViewManager.java
file. We will add the onReceiveNativeEvent
method that allows us to send a message to the JS side, using the RCTEventEmitter
.
We can use this to publish to the Javascript Side reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
materialCalendarView.getId()
, "topChange",
event
);
The first parameter of the receiveEvent
method is the view id or targetTag
The second parameter is the eventName
– in this case, topChange
will be our onChange
in JS.
The third parameter will be the event value we want to send to the JS side.
Full example
public void onReceiveNativeEvent(final ThemedReactContext reactContext, final MaterialCalendarView materialCalendarView) {
materialCalendarView.setOnDateChangedListener(new OnDateSelectedListener() {
@Override
public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) {
WritableMap event = Arguments.createMap();
event.putString("date", date.getDate().toString());
event.putInt("day", date.getDay());
event.putInt("month", date.getMonth());
event.putInt("year", date.getYear());
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(materialCalendarView.getId(), "topChange", event);
}
});
}
First, we set our listener. Then, we read all the information we want from the event and use the receiveEvent
method to publish to the Javascript Side.
Now, we are going to replace our iface
description object for a wrapper component. This component enables us to read the raw event and set the custom behavior we want when our library user does not set the onChange
prop.
import React, { Component, PropTypes } from 'react';
import { NativeModules, requireNativeComponent, View, Text } from 'react-native';
class MaterialCalendarComponent extends Component {
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
}
_onChange(event) {
if(!this.props.onDateChange) {
return;
}
this.props.onDateChange(event.nativeEvent);
}
render() {
return <MaterialCalendarView {...this.props} onChange={this._onChange} />;
}
}
MaterialCalendarComponent.propTypes = {
day: PropTypes.number,
month: PropTypes.number,
year: PropTypes.number,
onDateChange: PropTypes.func,
...View.propTypes,
}
const MaterialCalendarView = requireNativeComponent(`MaterialCalendarView`, MaterialCalendarComponent, {
nativeOnly: {
onChange: true,
},
});
export default MaterialCalendarComponent;
First, we remove the iface
and replace it with a new react class wrapper. Since the implementation uses the topChange
that we previously set on the receiveEvent
method in Java, now we should use onChange
to get that value, as we can see in the render
method. Using a wrapper lets us set any custom name for the callbacks. In this example, we use onDateChange
to get the new date object.
The new, third parameter of requireNativeComponent
is something really interesting. This parameter we set to
{ nativeOnly: { onChange: true } }
A quote from the React Native documentation:
Note the use of
nativeOnly
above. Sometimes you'll have some special properties that you need to expose for the native component, but don't actually want them as part of the API for the associated React component. For example, Switch has a customonChangehandler
for the raw native event and exposes an onValueChange handler property that is invoked with just the boolean value rather than the raw event (similar toonChangeMessage
in the example above). Since you don't want these native only properties to be part of the API, you don't want to put them in propTypes, but if you don't, you'll get an error. The solution is simply to call them out via thenativeOnly
option.
Finally, we can now use our new native component in our react native app.
import MaterialCalendarView from './android-native-component/MaterialCalendarView';
export default class CalendarExample extends Component {
constructor(props) {
super(props);
this.state = { dateObject: {} };
}
render() {
return (
<View style={styles.container}> <Text style={styles.welcome}> Welcome to MaterialCalendarView </Text> <MaterialCalendarView style={styles.calendarView} onDateChange={(dateObject) => this.setState({ dateObject })} day={20} month={4} year={2017} /> <Text style={styles.instructions}> {JSON.stringify(this.state.dateObject)} </Text> </View>
);
}
}
Links