React:Cross-Platform Application Development with React Native
上QQ阅读APP看书,第一时间看更新

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.