Keep Moving Forward | X-Team Magazine

Java UI Component on React Native

Written by William Cabrera | Apr 6, 2017 4:00:00 AM

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.

1. Create the ViewManager

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;
    }
}

2. Create the Package Module

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()
        );
    }
}

3. Add the Package Module to the MainApplication Class

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);
  }
}

4. Implement the JS Module

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.

5. Add Custom Props to the Native View

Java Side

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);
    }

Javascript Side

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);

6. Add a Callback From Java to Javascript

Java Side

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.

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 custom onChangehandler 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 to onChangeMessage 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 the nativeOnly option.

7. Enjoy the Component

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

Repository

Official Documentation