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 theimg/
folder in the root of our project- Then the image will be displayed by creating an
<Image/>
component using thesource
property:<Image source={require('../img/car.png')} />
Notice how the
source
property doesn't accept a string, but arequire('../img/car.png')
. This is a special case in React Native and may change in future versions.