About this book

React Native helps web and mobile developers to build cross-platform apps that perform at the same level as any other natively developed app. The range of apps that can be built using this library is huge. From e-commerce to games, React Native is a good fit for any mobile project due to its flexibility and extendable nature.

This project-based book consists of four standalone projects. Each project will help you gain a sound understanding of the framework and build mobile apps with native user experience. Starting with a simple standalone car booking app, you will progressively move on to building advanced apps by adding connectivity with external APIs, using native features, such as the camera or microphone, in the mobile device, integrating with state management libraries such as Redux or MobX, or leveraging React Native’s performance by building a full-featured game.

This book is ideal for developers who want to build amazing cross-platform apps with React Native.

This book is embedded with useful assessments that will help you revise the concepts you have learned in this book.

Publication date:
March 2018
Publisher
Packt
Pages
182
ISBN
9781789136081

 

Chapter 1. Project 1 – Car Booking App

Considering the success of the React framework, Facebook recently introduced a new mobile development framework called React Native. With React Native's game-changing approach to hybrid mobile development, you can build native mobile applications that are much more powerful, interactive, and faster by using JavaScript.

In this lesson, we will set the focus on feature development rather than in building a user interface by delegating the styling of our applications to UI libraries such as native-base as well as spend more time in building custom UI components and screens.

The app we will build is a car booking app in which the user can select the location in which he/she wants to be picked up and the type of car she wants to book for the ride. Since we want to focus on the user interface, our app will only have two screens and a little state management is needed. Instead, we will dive deeper into aspects such as animations, component's layout, using custom fonts, or displaying external images.

The app will be available for iOS and Android devices, and since all the user interface will be custom made, 100% of the code will be reused between both platforms. We will only use two external libraries:

  • React-native-geocoder: This will translate coordinates into human-readable locations

  • React-native-maps: This will easily display the maps and the markers showing the locations for the bookable cars

Due to its nature, most of the car booking apps put their complexity in the backend code to connect drivers with riders effectively. We will skip this complexity and mock all that functionality in the app itself to focus on building beautiful and usable interfaces.

 

Overview


When building mobile apps, we need to make sure we reduce the interface complexity to the minimum, as it's often punishing to present the user intrusive manuals or tooltips once the app is open. It is a good practice to make our app self-explanatory, so the user can understand the usage just by going through the app screens. That's why using standard components such as drawer menus or standard lists is always a good idea, but is not always possible (as it happens in our current app) due to the kind of data we want to present to the user.

In our case, we put all the functionality in the main screen plus in a modal box. Let's take a look at what the app will look like on iOS devices:

The background on our main screen is the maps component itself where we will show all the available cars as markers in the map. On the maps, we will display three components:

  • The pickup location box displaying the selected pickup location

  • The location pin, which can be dragged around the maps to select a new location

  • The selector for the kind of car the user wants to book. We will display three options: ECONOMY, SPECIAL, and SUPERIOR

Since most of the components are custom built, this screen will look very similar in any Android device:

The main difference between the iOS and the Android version will be the map component. While iOS will use Apple maps by default, Android uses Google Maps. We will leave this setup as each platform has its own map component optimized, but it's good to know that we can switch the iOS version to use Google Maps just by configuring our component.

Once the user has selected a pickup location, we will display a modal box to confirm the booking and contact the nearest driver for pickup:

As it happened with the main screen, this screen uses custom components: we even decided to create our own animated activity indicator. Because of this, the Android version will look very similar:

Since our app won't be connected to any external API, it should be seen as a mere display of the visual capabilities of React Native, although it could be easily extended by adding a state management library and a matching API.

Let's take a look at the topics of this lesson:

  • Using maps in our app

  • Style sheets in React Native

  • Flexbox in React Native

  • Using external images in a React Native app

  • Adding custom fonts

  • Animations in React Native

  • Using modals

  • Working with shadows and opacity

 

Setting up the Folder Structure


Let's initialize a React Native project using React Native's CLI. The project will be named carBooking and will be available for iOS and Android devices:

react-native init --version="0.49.3" carBooking

In this app, there is only one screen so that the folder structure for the code should be very straightforward. Since we will be using external images and fonts, we will organize these resources in two separate folders: img and fonts, both under the root folder.

The images and fonts used to build this app can be downloaded freely from some image and font sock websites. The name of the font we will use is Blair ITC.

We also stored the following images inside the img folder:

  • car.png: A simple drawing of a car to represent the bookable cars on the map.

  • class.png: The silhouette of a car to show inside the class selection button

  • classBar.png: The bar in which the class selection button will be slid to change the class.

  • loading.png: Our custom spinner. It will be stored as a static image and animated through the code.

Finally, let's take a look at our package.json file:

{
    "name": "carBooking",
    "version": "0.0.1",
    "private": true,
    "scripts": {
        "start": "node node_modules/react-native/local-cli/cli.js start",
        "test": "jest"
    },
    "dependencies": {
        "react": "16.0.0-beta.5",
        "react-native": "0.49.3",
"react-native-geocoder": " 0.4.8",
        "react-native-maps": " 0.15.2"
    },
    "devDependencies": {
        "babel-jest": "20.0.3",
        "babel-preset-react-native": "1.9.2",
        "jest": "20.0.4",
        "react-test-renderer": "16.0.0-alpha.6"
    },
    "jest": {
        "preset": "react-native"
    },
 "rnpm": {
        "assets": ["./fonts"]
    }
}

We only use two npm modules:

  • react-native-geocoder: This translates coordinates into human-readable locations

  • react-native-maps: This easily displays the maps and the markers showing the locations for the bookable cars

In order to allow the app to use custom fonts, we need to make sure they are accessible from the native side. For that, we need to add a new key to package.json named rnpm. This key will store an array of assets in which we will define our fonts folder. During build time, React Native will copy the fonts to a location from where they will be available natively and therefore usable within our code. This is only required by fonts and some special resources, but not by images.

Files and Folders Created by React Native's CLI

Let's take the chance of having a simple folder structure in this app to show what other files and folders are created by React Native's CLI when initializing a project through react-native init <projectName>.

__tests__/

React Native's CLI includes Jest as a developer dependency and, to get testing started, it includes a folder named __tests__, in which all tests can be stored. By default, React Native's CLI adds one test file: index.js, representing the initial set of tests. Developers can add later tests for any components in the app. React Native also adds a test script in our package.json, so we can run npm run test from the very first moment.

Jest is ready to be used with every project initialized through the CLI and it's definitely the easiest option when it comes to testing React components, although it is also possible to use other libraries such as Jasmine or Mocha.

android/ and ios/

These two folders hold the built app for both platforms natively. This means that we can find our .xcodeproj and java files in here. Every time we need to make changes to the native code of our app, we will need to modify some files in these two directories.

The most common reasons to find and modify files in these folders are:

  • Modify permissions (push notifications, access to location services, access to compass, and many more) by changing Info.plist (iOS) or AndroidManifest.xml (Android)

  • Change the build settings for any platform

  • Add API keys for native libraries

  • Add or modify native libraries to be used from our React Native code

node_modules/

This folder should be familiar to most of the JavaScript developers who worked with npm as it is where npm stores all the modules marked as a dependency in our project. It is not common to have the necessity to modify anything inside this folder, as everything should be handled through npm's CLI and our package.json file.

Files in the Root Folder

React Native's CLI creates a number of files in the root directory of our project; let's take a look at the most important ones:

  • .babelrc: Babel is the default library in React Native to compile our JavaScript files containing JSX and ES6 (for example, syntax into plain JavaScript capable to be understood by most of the JavaScript engines). Here, we can modify the configuration for this compiler so we can, for example, use the @ syntax for decorators as it was done in the first versions of React.

  • .buckconfig: Buck is the build system used by Facebook. This file is used to configure the building process when using Buck.

  • .watchmanconfig: Watchman is a service that watches the files in our project to trigger a rebuild anytime something changes in them. In this file, we can add some configuration options such as directories, which should be ignored.

  • app.json: This file is used by the react-native eject command to configure the native apps. It stores the name that identifies the app in each platform and also the name that will be displayed on the home screen of the device when the app is installed.

  • yarn.lock: The package.json file describes the intended versions desired by the original author, while yarn.lock describes the last-known-good configuration for a given application.

react-native link

Some apps depend on libraries with native capabilities which, before React Native CLI, required developers to copy native library files into the native projects. This was a cumbersome and repetitive project until react-native link came to the rescue. In this lesson we will use it to copy library files from react-native-maps and to link custom fonts from our /fonts folder to the compiled app.

By running react-native link in our project's root folder we will trigger the linking steps which will result in those native capabilities and resources to be accessible from our React Native code.

 

Running the App in the Simulator


Having the dependencies in the package.json file and all the initial files in place, we can run the following command (in the root folder of our project) to finish the installation:

npm install

Then, all the dependencies should be installed in our project. Once npm finishes installing all dependencies, we can start our app in the iOS simulator:

react-native run-ios

Or in the Android emulator using the following command:

react-native run-android

When React Native detects the app is running in a simulator, it enables a developer toolset available through a hidden menu, which can be accessed through the shortcuts command + D on iOS or command + M on Android (on Windows Ctrl should be used instead of command). This is how the developer menu looks like in iOS:

And this is how it looks like in the Android simulator:

The Developer Menu

In the process of building an app in React Native, the developer will have debugging needs. React Native fulfills these needs with the ability to remotely debug our apps in Chrome developer's tools or external applications such as React Native Debugger. Errors, logs, and even React components can be debugged easily as in a normal web environment.

On top of that, React Native provides a way to automatically reload our app each time a change is done saving the developers the task of manually reloading the app (which can be achieved by pressing command + R or Ctrl + R). There are two options when we set our app for automatic reloading:

  • Live reload detects any changes we make in the app's code and resets the app to its initial state after reloading.

  • Hot reload also detects changes and reloads the app, but keeps the current state of the app. This is really useful when we are implementing user flows to save the developer to repeat each step in the flow (for example, logging in or registering test users)

  • Finally, we can start the performance monitor to detect possible performance issues when performing complex operations such as animations or mathematical calculations.

 

Creating our App's Entry Point


Let's start our app's code by creating the entry point for our app: index.js. We import src/main.js in this file to use a common root component for our code base. Moreover, we will register the app with the name carBooking:

/*** index.js ***/

import { AppRegistry } from 'react-native';
import App from './src/main';
AppRegistry.registerComponent('carBooking', () => App);

Let's start building our src/main.js by adding a map component:

/*** src/main.js ** */

import React from 'react';
import { View, StyleSheet } from 'react-native';
import MapView from 'react-native-maps';

export default class Main extends React.Component {
  constructor(props) {
    super(props);
    this.initialRegion = {
      latitude: 37.78825,
      longitude: -122.4324,
      latitudeDelta: 0.00922,
      longitudeDelta: 0.00421,
    };
  }

  render() {
    return (
      <View style={{ flex: 1 }}>
        <MapView
          style={styles.fullScreenMap}
          initialRegion={this.initialRegion}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
fullScreenMap: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  },
});

Instead of using libraries for styling, we will create our own styles using StyleSheet, a React Native API, which serves as an abstraction similar to CSS style sheets. With StyleSheet, we can create a style sheet from an object (through the create method), which can be used in our components by referring to each style by its ID.

This way, we can reuse the style code and make the code more readable as we will be using meaningful names to refer to each style (for example, <Text style={styles.title}>Title 1</Text>).

At this point, we will only create a style referred by the key fullScreenMap and make it as an absolute position by covering the fullscreen size by adding top, bottom, left, and right coordinates to zero. On top of this, we need to add some styling to our container view to ensure it fills the whole screen: {flex: 1}. Setting flex to 1, we want our view to fill all the space its parent occupies. Since this is the main view, {flex: 1} will take over the whole screen.

For our map component, we will use react-native-maps, an open module created by Airbnb using native maps capabilities for Google and Apple maps. react-native-maps is a very flexible module, really well maintained, and fully featured so that it has become the de facto maps module for React Native. As we will see later in this lesson, react-native-maps requires the developer to run react-native link in order for it to work.

Apart from the style, the <MapView/> component will take initialRegion as a property to centre the map in a specific set of coordinates, which should be the current location of the user. For consistency reasons, we will locate the center of the map in San Francisco where we will also place some bookable cars:

/** * src/main.js ** */

import React from 'react';
import { View, Animated, Image, StyleSheet } from 'react-native';
import MapView from 'react-native-maps';

export default class Main extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
carLocations: [
        {
          rotation: 78,
          latitude: 37.78725,
          longitude: -122.4318,
        },
        {
          rotation: -10,
          latitude: 37.79015,
          longitude: -122.4318,
        },
        {
          rotation: 262,
          latitude: 37.78525,
          longitude: -122.4348,
        },
      ],
    };
    this.initialRegion = {
      latitude: 37.78825,
      longitude: -122.4324,
      latitudeDelta: 0.00922,
      longitudeDelta: 0.00421,
    };
  }

  render() {
    return (
      <View style={{ flex: 1 }}>
        <MapView
          style={styles.fullScreenMap}
          initialRegion={this.initialRegion}
        >
          {this.state.carLocations.map((carLocation, i) => (
            <MapView.Marker key={i} coordinate={carLocation}>
              <Animated.Image
                style={{
                  transform: [{ rotate: `${carLocation.rotation}deg` }],
                }}
                source={require('../img/car.png')}
              />
            </MapView.Marker>
          ))}
        </MapView>
      </View>
    );
  }
}

...

We have added an array of carLocations to be shown on the map as markers. Inside our render function, we will iterate over this array and place the corresponding <MapView.Marker/> in the provided coordinates. Inside each marker, we will add the image of the car rotating it by a specific number of degrees, so they match the streets directions. Rotating images must be done with the Animated API, which will be better explained later in this lesson.

Let's add a new property in our state to store a human-readable position for the location in which the map is centered:

/** * src/main.js ** */

import GeoCoder from 'react-native-geocoder';

export default class Main extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      position: null,

      ...

    };

    ...

  }

_onRegionChange(region) {
    this.setState({ position: null });
    const self = this;
    if (this.timeoutId) clearTimeout(this.timeoutId);
    this.timeoutId = setTimeout(async () => {
      try {
        const res = await GeoCoder.geocodePosition({
          lat: region.latitude,
          lng: region.longitude,
        });
        self.setState({ position: res[0] });
      } catch (err) {
        console.log(err);
      }
    }, 2000);
  }
componentDidMount() {
    this._onRegionChange.call(this, this.initialRegion);
  }

  render() {
    <View style={{ flex: 1 }}>
      <MapView
        style={styles.fullScreenMap}
        initialRegion={this.initialRegion}
onRegionChange={this._onRegionChange.bind(this)}
      >

      ...

      </MapView>
    </View>;
  }
}

...

To fill this state variable, we also created a function _onRegionChange, which uses the react-native-geocoder module. This module uses Google Maps reverse geocoding services to translate some coordinates into a human-readable location. Because it's a Google Service, we might need to add an API key in order to authenticate our app with the service. All the instructions to get this module fully installed can be found at its repository URL https://github.com/airbnb/react-native maps/blob/master/docs/installation.md.

We want this state variable to be available from the first mount of the main component, so we will call _onRegionChange in componentDidMount so that the name of the initial location is also stored in the state. Moreover, we will add the onRegionChange property on our <MapView/> to ensure the name of the location is recalculated every time the map is moved to show a different region, so we always have the name of the location in the center of the map in our position state variable.

As a final step on this screen, we will add all the subviews and another function to confirm the booking request:

/** * src/main.js ** */

...

import LocationPin from './components/LocationPin';
import LocationSearch from './components/LocationSearch';
import ClassSelection from './components/ClassSelection';
import ConfirmationModal from './components/ConfirmationModal';

export default class Main extends React.Component {
  ...

_onBookingRequest() {
    this.setState({
      confirmationModalVisible: true,
    });
  }

  render() {
    return (
      <View style={{ flex: 1 }}>
        ...

<LocationSearch
          value={
            this.state.position &&
            (this.state.position.feature ||
              this.state.position.formattedAddress)
          }
        />
        <LocationPin onPress={this._onBookingRequest.bind(this)} />
        <ClassSelection />
        <ConfirmationModal
          visible={this.state.confirmationModalVisible}
          onClose={() => {
            this.setState({ confirmationModalVisible: false });
          }}
        />
      </View>
    );
  }
}

...

We added four subviews:

  • LocationSearch: The component in which we will show the user the location that is centered on the map so she can know the name of the location she is exactly requesting the pickup.

  • LocationPin: A pinpointing to the center of the map, so the user can see on the map where she will request the pickup. It will also display a button to confirm the pickup.

  • ClassSelection: A bar where the user can select the type of car for the pickup (economy, special, or superior).

  • ConfirmationModal: The modal displaying the confirmation of the request.

The _onBookingRequest method will be responsible for bringing the confirmation modal up when a booking is requested.

Adding Images to Our App

React Native deals with images in a similar way as websites do: images should be placed in a folder inside the projects folder structure, and then they can be referenced from the <Image /> (or <Animated.Image />) by the source property. Let's see an example from our app:

  • car.png: This is placed inside the img/ folder in the root of our project

  • Then the image will be displayed by creating an <Image/> component using the source property:

           <Image source={require('../img/car.png')} />

    Notice how the source property doesn't accept a string, but a require('../img/car.png'). This is a special case in React Native and may change in future versions.

 

LocationSearch


This should be a simple textbox displaying the human-readable name of the location in which the map is centered. Let's take a look at the code:

/*** src/components/LocationSearch.js ** */

import React from 'react';
import {
  View,
  Text,
  TextInput,
  ActivityIndicator,
  StyleSheet,
} from 'react-native';

export default class LocationSearch extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>PICKUP LOCATION</Text>
        {this.props.value && (
          <TextInput style={styles.location} value={this.props.value} />
        )}
        {!this.props.value && <ActivityIndicator style={styles.spinner} />}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    margin: 20,
    marginTop: 40,
    height: 60,
    padding: 10,
    borderColor: '#ccc',
    borderWidth: 1,
  },
  title: {
    alignSelf: 'center',
    fontSize: 12,
    color: 'green',
    fontWeight: 'bold',
  },
  location: {
    height: 40,
    textAlign: 'center',
    fontSize: 13,
  },
  spinner: {
    margin: 10,
  },
});

It receives only one property: value (the name of the location to be displayed). If it's not set, it will display a spinner to show activity.

Because there are many different styles to be applied in this component, it's beneficial to use the StyleSheet API to organize the styles in a key/value object and refer it from our render method. This separation between logic and style helps in readability of the code and also enables code reuse as the styles can be cascaded down to child components.

Aligning Elements

React Native uses Flexbox for setting up the layout of the elements in an app. This is mostly straightforward, but sometimes it can be confusing when it comes to aligning elements as there are four properties that can be used for this purpose:

  • justifyContent: It defines the alignment of the child elements through the main axis

  • alignItems: It defines the alignment of the child elements through the cross-axis

  • alignContent: It aligns a flex container's lines within when there is extra space in the cross-axis

  • alignSelf: It allows the default alignment (or the one specified by alignItems) to be overridden for individual flex items

The first three properties should be assigned to the container element, while the fourth one will be applied to a child element in case we want to override the default alignment.

In our case, we only want one element (the title) to be center aligned so we can use alignSelf: 'center'. Later in this lesson, we will see other uses for the different align properties.

 

LocationPin


In this section, we will focus on building the pinpointing to the center of the map to visually confirm the pickup location. This pin also contains a button, which can be used to trigger a pickup request:

/** * src/components/LocationPin.js ** */

import React from 'react';
import {
  View,
  Text,
Dimensions,
  TouchableOpacity,
  StyleSheet,
} from 'react-native';

const { height, width } = Dimensions.get('window');

export default class LocationPin extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <View style={styles.banner}>
          <Text style={styles.bannerText}>SET PICKUP LOCATION</Text>
<TouchableOpacity
            style={styles.bannerButton}
            onPress={this.props.onPress}
          >
            <Text style={styles.bannerButtonText}>{'>'}</Text>
          </TouchableOpacity>
        </View>
        <View style={styles.bannerPole} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
top: height / 2 - 60,
    left: width / 2 - 120,
  },
  banner: {
flexDirection: 'row',
    alignSelf: 'center',
    justifyContent: 'center',
    borderRadius: 20,
    backgroundColor: '#333',
    padding: 10,
    paddingBottom: 10,
shadowColor: '#000000',
    shadowOffset: {
      width: 0,
      height: 3,
    },
    shadowRadius: 5,
    shadowOpacity: 1.0,
  },
  bannerText: {
    alignSelf: 'center',
    color: 'white',
    marginRight: 10,
    marginLeft: 10,
    fontSize: 18,
  },
  bannerButton: {
    borderWidth: 1,
    borderColor: '#ccc',
    width: 26,
    height: 26,
    borderRadius: 13,
  },
  bannerButtonText: {
    color: 'white',
    textAlign: 'center',
backgroundColor: 'transparent',
    fontSize: 18,
  },
  bannerPole: {
    backgroundColor: '#333',
    width: 3,
    height: 30,
    alignSelf: 'center',
  },
});

This component is again very light in terms of functionality, but has a lot of custom style. Let's dive into some of the style details.

flexDirection

By default, React Native and Flexbox stack elements vertically:

For the banner in our pin, we want to stack every element horizontally after each other as follows:

This can be achieved by adding the following styles to the containing element flexDirection: 'row'. The other valid options for flexDirection are:

  • row-reverse

  • column (default)

  • column-reverse

Dimensions

One of the first lines of code in this component extracts the height and the width from the device into two variables:

const {height, width} = Dimensions.get('window');

Obtaining the height and width of the device enables us developers to absolute position some elements being confident they will show properly aligned. For example, we want the banner of our pin to be aligned in the center of the screen, so it points to the center of the map. We can do this by adding {top: (height/2), left: (width/2)} to the banner style in our style sheet. Of book, that would align the upper-left corner, so we need to subtract half the size of the banner to each property to ensure it gets centered in the middle of the element. This trick can be used whenever we need to align an element that is not relative to any other in the components tree although it is recommended to use relative positioning when possible.

Shadows

Let's set focus on our banner's style, specifically on the shadows properties:

banner: {
  ...
  shadowColor: '#000000',
  shadowOffset: {
    width: 0,
    height: 3
  },
  shadowRadius: 5,
  shadowOpacity: 1.0
}

In order to add a shadow to a component, we need to add four properties:

  • shadowColor: This adds the hexadecimal or RGBA value of the color we want for our component

  • shadowOffset: This shows how far we want our shadow to be casted

  • shadowRadius: This shows the value of the radius in the corner of our shadow

  • shadowOpacity: This shows how dark we want our shadow to be

That's it for our LocationPin component.

 

ClassSelection


In this component, we will explore the Animated API in React Native to get started with animations. Moreover, we will use custom fonts to improve the user experience and increase the feeling of customization in our app:

/*** src/components/ClassSelection.js ** */

import React from 'react';
import {
  View,
  Image,
  Dimensions,
  Text,
  TouchableOpacity,
Animated,
  StyleSheet,
} from 'react-native';

const { height, width } = Dimensions.get('window');

export default class ClassSelection extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
classButtonPosition: new Animated.Value(15 + width * 0.1),
    };
  }

  _onClassChange(className) {
    if (className === 'superior') {
Animated.timing(this.state.classButtonPosition, {
        toValue: width * 0.77,
        duration: 500,
      }).start();
    }

    if (className === 'special') {
Animated.timing(this.state.classButtonPosition, {
        toValue: width * 0.5 - 20,
        duration: 500,
      }).start();
    }

    if (className === 'economy') {
Animated.timing(this.state.classButtonPosition, {
        toValue: 15 + width * 0.1,
        duration: 500,
      }).start();
    }
  }

  render() {
    return (
      <View style={styles.container}>
        <Image
          style={styles.classBar}
          source={require('../../img/classBar.png')}
        />
<Animated.View
          style={[styles.classButton, { left: this.state.classButtonPosition }]}
        >
          <Image
            style={styles.classButtonImage}
            source={require('../../img/class.png')}
          />
        </Animated.View>
        <TouchableOpacity
          style={[
            styles.classButtonContainer,
            {
              width: width / 3 - 10,
              left: width * 0.11,
            },
          ]}
          onPress={this._onClassChange.bind(this, 'economy')}
        >
          <Text style={styles.classLabel}>economy</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={[
            styles.classButtonContainer,
            { width: width / 3, left: width / 3 },
          ]}
          onPress={this._onClassChange.bind(this, 'special')}
        >
          <Text style={[styles.classLabel, { textAlign: 'center' }]}>
            Special
          </Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={[
            styles.classButtonContainer,
            { width: width / 3, right: width * 0.11 },
          ]}
          onPress={this._onClassChange.bind(this, 'superior')}
        >
          <Text style={[styles.classLabel, { textAlign: 'right' }]}>
            Superior
          </Text>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    height: 80,
    backgroundColor: 'white',
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    paddingBottom: 10,
  },
  classBar: {
width: width * 0.7,
    left: width * 0.15,
    resizeMode: 'contain',
    height: 30,
    top: 35,
  },
  classButton: {
    top: 30,
    justifyContent: 'center',
    borderRadius: 20,
    borderColor: '#ccc',
    borderWidth: 1,
    position: 'absolute',
    backgroundColor: 'white',
    height: 40,
    width: 40,
  },
  classButtonImage: {
    alignSelf: 'center',
    resizeMode: 'contain',
    width: 30,
  },
  classButtonContainer: {
    backgroundColor: 'transparent',
    position: 'absolute',
    height: 70,
    top: 10,
  },
  classLabel: {
    paddingTop: 5,
    fontSize: 12,
  },
});

This simple component is made out of five sub components:

  • classBar: This is an image showing the bar and the stop points for each class

  • classButton: This is the round button, which will be moved to the selected class once the user presses a specific class

  • classButtonContainer: This is the touchable component detecting what class the user wants to select

  • classLabel: These are titles for each class to be displayed on top of the bar

Let's start by taking a look at the styles as we can find a new property for image components: resizeMode, which determines how to resize the image when the frame doesn't match the raw image dimensions. From the five possible values (cover, contain, stretch, repeat, and center), we chose contain as we want to scale the image uniformly (maintain the image's aspect ratio) so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. We are using these properties both in classBar and classButtonImage being the two images we will need to resize in this view.

Adding Custom Fonts

React Native includes a long list of cross-platform fonts available by default. The list of fonts can be checked on https://github.com/react-native-training/react-native-fonts.

Nevertheless, adding custom fonts is a common need when developing apps, especially when designers are involved, so we will use our car booking app as a playground to test this functionality.

Adding custom fonts to our app is a three steps task:

  1. Add the font file (.ttf) into a folder inside our project. We used fonts/ for this app.

  2. Add the following lines to our package.json:

          "rnpm": {
              "assets": ["./fonts"]
          }
  3. Run the following command in a terminal:

         react-native link
    

That's it, React Native's CLI will handle the insertion of the fonts folder and its files inside the iOS and Android project at once. Our fonts will be available by their font name (which may not be the same as the filename). In our case, we have fontFamily: 'Blair ITC' in our style sheet.

We can now modify our classLabel style in the ClassSelection component to include the new font:

...

classLabel: {
    fontFamily: 'Blair ITC',
    paddingTop: 5,
    fontSize: 12,
},

...

Animations

React Native's Animated API is designed to make it very easy to concisely express a wide variety of interesting animation and interaction patterns in a very performant way. Animated focuses on declarative relationships between inputs and outputs, with configurable transforms in between, and simple start/stop methods to control time-based animation execution.

What we want to do in our app is to move the classButton to a specific location whenever the user presses the class she wants to book. Let's take a closer look at how we are using this API in our app:

/** * src/components/ClassSelection ***/

...

export default class ClassSelection extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      classButtonPosition: new Animated.Value(15 + width * 0.1),
    };
  }

  _onClassChange(className) {
    if (className === 'superior') {
      Animated.timing(this.state.classButtonPosition, {
        toValue: width * 0.77,
        duration: 500,
      }).start();
    }

    ...

  }

  render() {
    return (
      ...

      <Animated.View style={{ left: this.state.classButtonPosition }}>
        <Image
          style={styles.classButtonImage}
          source={require('../../img/class.png')}
        />
      </Animated.View>

      ...

      <TouchableOpacity
        onPress={this._onClassChange.bind(this, 'superior')}
      >
        <Text>Superior</Text>
      </TouchableOpacity>

      ...
    );
  }
}

...

For this movement to happen correctly, we need to wrap the classButtonImage in Animated.View and provide an initial Animated.Value to it as a left coordinate. We will use this.state.classButtonPosition for this matter so that we can change it when the user selects a specific class.

We are ready to start our animation. It will be triggered by the _onClassChange method, as it is the one invoked when the user presses classButtonContainer (<TouchableOpacity/>). This method is calling the Animated.timing function passing two parameters:

  • The animated value to drive (this.state.classButtonPosition)

  • An object containing the end value and the duration of the animation

Invoking Animated.timing will result in an object containing the start() method, which we call right away to start the animation. React Native will then know that the left coordinate of the Animated.View needs to be slowly changed according to the provided parameters.

As this may feel a bit overcomplicated for a simple move animation, it allows a wide range of customization as chaining animations or modifying the easing functions. We will see a rotation animation later in this lesson.

 

ConfirmationModal


Our last component is a modal view, which will be opened once the user has pressed on the SET PICKUP LOCATION button on the location pin. We will display the modal and a custom activity indicator, which will use a complex animation setup to continuously rotate in its position:

/** * src/components/ConfirmationModal.js ***/

import React from 'react';
import {
Modal,
  View,
  Text,
  Animated,
  Easing,
  TouchableOpacity,
  StyleSheet,
} from 'react-native';

export default class ConfirmationModal extends React.Component {
  componentWillMount() {
    this._animatedValue = new Animated.Value(0);
  }

cycleAnimation() {
    Animated.sequence([
      Animated.timing(this._animatedValue, {
        toValue: 100,
        duration: 1000,
        easing: Easing.linear,
      }),
      Animated.timing(this._animatedValue, {
        toValue: 0,
        duration: 0,
      }),
    ]).start(() => {
      this.cycleAnimation();
    });
  }

componentDidMount() {
    this.cycleAnimation();
  }

  render() {
const interpolatedRotateAnimation = this._animatedValue.interpolate({
      inputRange: [0, 100],
      outputRange: ['0deg', '360deg'],
    });

    return (
<Modal
        animationType={'fade'}
        visible={this.props.visible}
        transparent={true}
      >
        <View style={styles.overlay}>
          <View style={styles.container}>
            <Text style={styles.title}>Contacting nearest car...</Text>
<Animated.Image
              style={[
                styles.spinner,
                { transform: [{ rotate: interpolatedRotateAnimation }] },
              ]}
              source={require('../../img/loading.png')}
            />
            <TouchableOpacity
              style={styles.closeButton}
              onPress={this.props.onClose}
            >
              <Text style={styles.closeButtonText}>X</Text>
            </TouchableOpacity>
          </View>
        </View>
      </Modal>
    );
  }
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: '#0006',
    justifyContent: 'center',
  },
  container: {
    backgroundColor: 'white',
    alignSelf: 'center',
    padding: 20,
    borderColor: '#ccc',
    borderWidth: 1,
  },
  title: {
    textAlign: 'right',
    fontFamily: 'Blair ITC',
    paddingTop: 5,
    fontSize: 12,
  },
  spinner: {
    resizeMode: 'contain',
    height: 50,
    width: 50,
    margin: 50,
    alignSelf: 'center',
  },
  closeButton: {
    backgroundColor: '#333',
    width: 40,
    height: 40,
    borderRadius: 20,
    justifyContent: 'center',
    alignSelf: 'center',
  },
  closeButtonText: {
    color: 'white',
    alignSelf: 'center',
    fontSize: 20,
  },
});

For this component, we are using the <Modal /> component available in React Native to take advantage of its fade animation and visibility capabilities. The property this.props.visible will drive the visibility of this component as it is the parent who is aware of the pickup request from the user.

Let's focus again on animations as we want to do a more complex setup for the spinner showing activity. We want to display an endless rotating animation, so we need to systematically call our start() animation method. In order to achieve this, we created a cycleAnimation() method, which is called on the component mount (to get the animation started) and from the Animated.timing returned object as it is passed as a callback to be invoked every time the animation ends.

We are also using Animated.sequence to concatenate two animations:

  • Moving from 0 degrees to 360 (in one second using a linear easing)

  • Moving from 360 degrees to 0 (in 0 seconds)

This is required to repeat the first animation over at the end of each cycle.

Finally, we defined a variable named interpolatedRotateAnimation to store the interpolation from 0 degrees to 360, so it can be passed to the transform/rotate style defining what are going to be the available rotation values when animating our Animated.Image.

As an experiment, we can try and change loading.png with an alternative image and see how it gets animated. This can be easily achieved by replacing the source property in our <Animated.Image /> component:

...            

            <Animated.Image
              style={[
                styles.spinner,
                { transform: [{ rotate: interpolatedRotateAnimation }] },
              ]}
source={require('../../img/spinner.png')}
            />

...
 

Summary


Using UI libraries such as native-base or react-native-elements saves a lot of time and maintenance hassle when we need to build apps, but the results end up having a standard flavor, which is not always desirable in terms of user experience. That's why learning how to manipulate the style of our apps is always a good idea, especially on teams where the design is provided by UX specialists or app designers.

In this lesson, we took a deep look into the folders and files created by React Native's CLI when initializing a project. Moreover, we familiarized ourselves with the developer menu and its debugging functionalities. When building our app we set the focus on the layouts and component styling, but also on how to add and manipulate animations to make our interface more appealing to the user. We took a look at Flexbox layout system and how to stack and center elements in our components. API's such as dimensions were used to retrieve the device width and height to perform positioning tricks on some components. You learned how to add fonts and images into our app and how to show them to improve the user experience.

Now that we know how to build more custom interfaces, let's build in the next lesson an image sharing app in which design plays a key role.

 

Assessments


  1. Why does the react-native-geocoder module uses Google Maps reverse geocoding services?

    1. To store a human-readable position for the location in which the map is centred

    2. To translate some coordinates into a human-readable location

    3. To add an API key in order to authenticate our app with the service

    4. To ensure the name of the location is recalculated every time the map is moved to show a different region

  2. Which of the following properties is used for aligning elements?

    1. justifyContent

    2. alignLeft

    3. alignRight

    4. alignJustify

  3. By default, React Native and Flexbox stack elements ________.

    1. Diagonally

    2. Reverse

    3. Vertically

    4. Horizontally

  4. Which of the following lines of code extracts the height and the width from a device into two variables?

    1. const {height, width} = Dimensions.get('height, width');

    2. constant {height, width} = Dimensions.get('window');

    3. const {height, width} = get('window');

    4. const {height, width} = Dimensions.get('window');

  5. Which are the four properties in order to add a shadow to a component?

About the Author

  • Emilio Rodriguez Martinez

    Emilio Rodriguez Martinez is a senior software engineer who has been working on highly demanding JavaScript projects since 2010. He transitioned from web development positions into mobile development, first with hybrid technologies such as Cordova and then with native JavaScript solutions such as Titanium. In 2015, he focused on the development and maintenance of several apps built in React Native, some of which were featured in Apple's App Store as the top apps of the week. Nowadays, Emilio is part of the Red Hat mobile team, which leverages Red Hat's open source mobile platform. He serves as an advocate for mobile developers using RHMAP. He is also an active contributor to React Native's codebase and Stack Overflow, where he provides advice on React and React Native questions.

    Browse publications by this author

Latest Reviews

(4 reviews total)
wow learned a lot in the path of expart
Preço muito bom e qualidade do material também.
No siento que sea correcto usar herramientas que no son explicadas a fondo, si no las van a explicar entonces no las usen...
React: Cross-Platform Application Development with React Native
Unlock this book and the full library for FREE
Start free trial