使用React Native创建乘车预订应用程序

在本教程中,我们将使用React Native和Pusher创建一个乘车预订应用程序。 我们将创建的应用程序将类似于流行的乘车预订应用程序,例如Uber,Lyft或Grab。

React Native将用于为驾驶员和乘客创建一个Android应用程序。 Pusher将用于两者之间的实时通信。

您将创造什么

像其他任何出租车预订应用程序一样,这里将有一个驾驶员应用程序和一个乘客应用程序。 乘客应用程序将用于预订行程,而驾驶员应用程序仅接收来自乘客应用程序的任何请求。 为了保持一致性,我们仅将应用程序称为“ grabClone”。

应用流程

我们将要创建的克隆几乎与那里的任何乘车预订应用程序具有相同的流程:乘客预订乘车→应用程序寻找驾驶员→驾驶员接受请求→驾驶员接载乘客→驾驶员开车前往目的地→乘客付钱给司机。

在这里,我只想向您展示该过程在应用程序内部的外观。 这样,您将清楚地知道要创建什么。

1.该应用程序确定用户的位置并将其显示在地图上(注意:此时需要启用GPS)。

2.在旅客应用程序中,用户单击“预订旅程”。

3.将打开一个模式,允许乘客选择他们要去的地方。

4.应用程序要求乘客确认目的地。

5.确认后,应用程序会向驾驶员应用程序发送请求以接载乘客。 在应用程序等待驾驶员接受请求时,将显示加载动画。

6.驱动程序收到请求。 驾驶员可以从此处接受或拒绝该请求。

7.驾驶员接受请求后,其详细信息将显示在乘客应用程序中。

8.乘客应用程序在地图上显示驾驶员的当前位置。

9.驾驶员离乘客所在地50米以内时,他们将看到警报,提示驾驶员在附近。

10.驾驶员离乘客位置20米以内时,驾驶员应用程序会向乘客应用程序发送一条消息,告知驾驶员几乎就在附近。

11.接载乘客后,驾驶员开车前往目的地。

12.一旦驾驶员离目的地20米之内,驾驶员应用程序就会向乘客应用程序发送一条消息,告知他们距离目的地很近。

至此,行程结束,乘客可以预订另一个行程。 驾驶员也可以自由接受任何传入的乘车请求。

先决条件

1.推送帐户

注册一个Pusher帐户使用您现有的 帐户 登录 。 创建帐户后,创建一个新应用程序→为前端技术选择“ React”→为后端技术选择“ Node.js”。

接下来,点击“应用设置”标签,然后选中“启用客户端事件”。 这使我们能够使驾驶员和乘客应用程序直接相互通信。

最后,单击“应用程序密钥”并复制凭据。 如果您担心定价, 则Pusher沙盒计划非常慷慨,因此您可以在测试应用程序时免费使用它。

2. Android Studio

您实际上并不需要Android Studio,但它附带了我们需要的Android SDK。 Google也不再为此提供单独的下载。

3. React Native

我为此建议的方法是以本机方式构建项目。 在React Native网站上时,单击“使用本机代码构建项目”选项卡,然后按照其中的说明进行操作。 Expo客户端非常适合快速制作应用原型,但实际上并不能为我们提供快速的方法来测试此应用所需的地理定位功能。

4. Genymotion

我们使用Genymotion测试驱动程序。 我们使用它而不是默认的Android模拟器,因为它带有GPS仿真工具,该工具可让我们搜索特定位置并将其用作仿真设备的位置。 它使用Google地图作为界面,您也可以移动标记。 这使我们可以模拟行驶中的车辆。

安装Genymotion后,您需要登录到您的帐户才能添加设备。 对我来说,我已经安装了Google Nexus 5x进行测试。

5. Android设备

这将用于测试乘客应用程序。 确保检查手机的Android版本。 如果最低为4.2,则需要通过Android SDK Manager安装其他软件包。 这是因为,默认情况下,React Native针对API版本23或更高版本。 这意味着您手机的Android版本至少必须为6.0版,否则该应用将无法运行。 如果您已经安装了Android Studio,则可以通过打开Android Studio→单击“配置”→选择“ SDK管理器”来访问SDK Manager。 然后在“ SDK平台”下,检查要支持的Android版本。

在那里,单击“ SDK工具”,并确保您还安装了与我的工具相同的工具:

6.额外的计算机(可选)

这是可选的。 我之所以将其包含在此处是因为React Native一次只能在单个设备或仿真器上运行该应用程序。 因此,您需要做一些额外的工作才能运行这两个应用程序,这将在以后看到。

创建身份验证服务器

现在是时候弄脏我们的手了。 首先,让我们在身份验证服务器上工作。 这是必需的,因为我们将从应用程序发送客户端事件 ,客户端事件要求Pusher通道为私有通道,而私有通道的访问权限受到限制。 这是身份验证服务器的来源。它用作Pusher知道尝试连接的用户是否确实是该应用程序的注册用户的一种方式。

首先安装依赖项:

npm install --save express body-parser pusher

接下来,创建一个server.js文件并添加以下代码:

var express = require ( 'express' );
var bodyParser = require ( 'body-parser' );
var Pusher = require ( 'pusher' );

var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended : false }));

var pusher = new Pusher({ // connect to pusher
  appId: process.env.APP_ID, 
  key : process.env.APP_KEY, 
  secret :  process.env.APP_SECRET,
  cluster : process.env.APP_CLUSTER, 
});

app.get( '/' , function ( req, res ) { // for testing if the server is running
  res.send( 'all is well...' );
});

// for authenticating users
app.get( "/pusher/auth" , function ( req, res )  {
  var query = req.query;
  var socketId = query.socket_id;
  var channel = query.channel_name;
  var callback = query.callback;

  var auth = JSON .stringify(pusher.authenticate(socketId, channel));
  var cb = callback.replace( /\"/g , "" ) + "(" + auth + ");" ;

  res.set({
    "Content-Type" : "application/javascript"
  });

  res.send(cb);
});

app.post( '/pusher/auth' , function ( req, res )  {
  var socketId = req.body.socket_id;
  var channel = req.body.channel_name;
  var auth = pusher.authenticate(socketId, channel);
  res.send(auth);
});

var port = process.env.PORT || 5000 ;
app.listen(port);

由于上面的代码已在文档中的“ 验证用户”中进行了解释,因此我不再赘述。

为简单起见,我实际上没有添加代码来检查用户是否确实存在于数据库中。 您可以在/pusher/auth端点中通过检查用户名是否存在来做到这一点。 这是一个例子:

var users = [ 'luz' , 'vi' , 'minda' ];
var username = req.body.username;

if (users.indexOf(username) !== -1){
  var socketId = req.body.socket_id;
  var channel = req.body.channel_name;
  var auth = pusher.authenticate(socketId, channel);
  res.send(auth);
}

// otherwise: return error

稍后再连接到客户端的Pusher时,不要忘记传递username

完成后,尝试运行服务器:

node server.js

在浏览器上访问http://localhost:5000以查看其是否有效。

部署认证服务器

由于Pusher必须连接到身份验证服务器,因此需要可以从Internet进行访问。

您可以使用now.sh部署身份验证服务器。 您可以使用以下命令进行安装:

npm install now

一旦安装完毕,现在就可以浏览到您拥有的文件夹server.js文件并执行now 。 系统会要求您输入电子邮件并验证您的帐户。

验证您的帐户后,执行以下操作将Pusher应用程序设置作为环境变量添加到now.sh帐户,以便您可以在服务器内部使用它:

now secret add pusher_app_id YOUR_PUSHER_APP_ID
now secret add pusher_app_key YOUR_PUSHER_APP_KEY
now secret add pusher_app_secret YOUR_PUSHER_APP_SECRET
now secret add pusher_app_cluster YOUR_PUSHER_APP_CLUSTER

接下来,在提供您添加的机密值的同时部署服务器:

now -e APP_ID=@pusher_app_id -e APP_KEY=@pusher_app_key -e APP_SECRET=@pusher_app_secret APP_CLUSTER=@pusher_app_cluster

这样,您可以从服务器内部访问Pusher应用程序设置,如下所示:

process.env.APP_ID

now.sh返回的部署URL是您稍后将用于将应用程序连接到身份验证服务器的URL。

创建驱动程序

现在,您可以开始创建驱动程序了。

首先,创建一个新的React Native应用程序:

react-native init grabDriver

安装依赖项

完成此操作后,请在grabDriver目录中导航并安装我们需要的库。 这包括用于与Pusher配合使用的pusher-js ,用于显示地图的React Native Maps和用于将地理位置反向地理编码为实际位置名称的React Native Geocoding

npm install --save pusher-js react-native-maps react-native-geocoding

安装所有库后,React Native Maps需要一些额外的步骤才能运行。 首先是链接项目资源:

react-native link react-native-maps

接下来,您需要创建一个Google项目,从Google开发者控制台获取API密钥,并启用Google Maps Android APIGoogle Maps Geocoding API 。 之后,在您的项目目录中打开android\app\src\main\AndroidManifest.xml文件。 在<application>标记下,添加包含服务器API密钥的<meta-data>

< application >
    < meta-data
      android:name = "com.google.android.geo.API_KEY"
      android:value = "YOUR GOOGLE SERVER API KEY" />
</ application >

在那里,请在默认权限下方添加以下内容。 这使我们可以检查网络状态,并从设备请求地理定位数据。

< uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
< uses-permission android:name = "android.permission.ACCESS_FINE_LOCATION" />

还要确保其目标与通过Genymotion安装的设备使用相同的API版本。 就像我之前说过的,如果它的版本23或更高版本,您实际上不需要执行任何操作,但是如果它的版本低于23,则必须准确运行该应用程序。

< uses-sdk
        android:minSdkVersion = "16"
        android:targetSdkVersion = "23" />

最后,由于我们将主要使用Genymotion来测试驱动程序,因此您需要按照此处说明进行操作 。 万一链接断开,这是您需要做的:

  1. 访问opengapps.org
  2. 选择x86作为平台。
  3. 选择与您的虚拟设备相对应的Android版本。
  4. 选择nano作为变体。
  5. 下载压缩文件。
  6. 将zip安装程序拖放到新的Genymotion虚拟设备中(仅限2.7.2和更高版本)。
  7. 按照弹出说明进行操作。

我们需要这样做,因为React Native Maps库主要使用Google Maps。 我们需要添加Google Play服务才能使其正常运行。 与大多数已经安装了此功能的Android手机不同,Genymotion由于知识产权原因默认不具有该功能。 因此,我们需要手动安装它。

如果您在发行后的一段时间内正在阅读本文档 ,请务必查看“ 安装”文档以确保您没有遗漏任何内容。

编码驱动程序

现在,您可以开始对应用程序进行编码了。 首先打开index.android.js文件,并将默认代码替换为以下代码:

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

这样做是导入App组件,它是应用程序的主要组件。 然后将其注册为默认组件,以便将其呈现在屏幕中。

接下来,创建App.js文件,并从React Native包中导入我们需要的东西:

import React, { Component } from 'react' ;
import {
  StyleSheet,
  Text,
  View,
  Alert
} from 'react-native' ;

还要导入我们之前安装的第三方库:

import Pusher from 'pusher-js/react-native' ;
import MapView from 'react-native-maps' ;

import Geocoder from 'react-native-geocoding' ;
Geocoder.setApiKey( 'YOUR GOOGLE SERVER API KEY' );

最后,导入helpers文件:

import { regionFrom, getLatLonDiffInMeters } from './helpers' ;

helpers.js文件包含以下内容:

export function regionFrom ( lat, lon, accuracy )  {
  const oneDegreeOfLongitudeInMeters = 111.32 * 1000 ;
  const circumference = ( 40075 / 360 ) * 1000 ;

  const latDelta = accuracy * ( 1 / ( Math .cos(lat) * circumference));
  const lonDelta = (accuracy / oneDegreeOfLongitudeInMeters);

  return {
    latitude : lat,
    longitude : lon,
    latitudeDelta : Math .max( 0 , latDelta),
    longitudeDelta : Math .max( 0 , lonDelta)
  };
} 

export function getLatLonDiffInMeters ( lat1, lon1, lat2, lon2 )  {
  var R = 6371 ; // Radius of the earth in km
  var dLat = deg2rad(lat2-lat1);  // deg2rad below
  var dLon = deg2rad(lon2-lon1); 
  var a = 
    Math .sin(dLat/ 2 ) * Math .sin(dLat/ 2 ) +
    Math .cos(deg2rad(lat1)) * Math .cos(deg2rad(lat2)) * 
    Math .sin(dLon/ 2 ) * Math .sin(dLon/ 2 )
    ; 
  var c = 2 * Math .atan2( Math .sqrt(a), Math .sqrt( 1 -a)); 
  var d = R * c; // Distance in km
  return d * 1000 ;
}

function deg2rad ( deg )  {
  return deg * ( Math .PI/ 180 )
}

这些函数用于获取React Native Maps库显示地图所需的纬度和经度增量值。 另一个函数( getLatLonDiffInMeters )用于确定两个坐标之间的距离(以米为getLatLonDiffInMeters )。 稍后,这将使我们能够告知用户他们是否已经彼此靠近或何时接近目的地。

接下来,创建主应用程序组件并声明默认状态:

export default class grabDriver extends Component  {

  state = {
    passenger : null , // for storing the passenger info
    region: null , // for storing the current location of the driver
    accuracy: null , // for storing the accuracy of the location
    nearby_alert: false , // whether the nearby alert has already been issued
    has_passenger: false , // whether the driver has a passenger (once they agree to a request, this becomes true)
    has_ridden: false // whether the passenger has already ridden the vehicle
  }
}
// next: add constructor code

在构造函数内部,初始化将在整个应用程序中使用的变量:

constructor () {
  super ();

  this .available_drivers_channel = null ; // this is where passengers will send a request to any available driver
  this .ride_channel = null ; // the channel used for communicating the current location
  // for a specific ride. Channel name is the username of the passenger
 
  this .pusher = null ; // the pusher client
}

// next: add code for connecting to pusher

在安装组件之前,请连接到您之前创建的身份验证服务器。 确保替换压入键, authEndpointcluster

componentWillMount() {
  this .pusher = new Pusher( 'YOUR PUSHER KEY' , {
    authEndpoint : 'YOUR PUSHER AUTH SERVER ENDPOINT' ,
    cluster : 'YOUR PUSHER CLUSTER' ,
    encrypted : true
  });
  
  // next: add code for listening to passenger requests
}

现在,您已经连接到身份验证服务器,现在可以开始侦听来自旅客应用程序的请求。 第一步是订阅私人频道 。 该通道是所有乘客和驾驶员订阅的地方。 在这种情况下,驾驶员可以使用它来收听乘车请求。 它必须是专用通道,因为出于安全原因, 客户端事件只能在专用通道和状态通道上触发。 您知道这是一个专用频道,因为有private-前缀。

this .available_drivers_channel = this .pusher.subscribe( 'private-available-drivers' ); // subscribe to "available-drivers" channel

接下来,侦听client-driver-request事件。 您知道由于client-前缀,这是一个客户端事件。 客户端事件不需要服务器干预即可工作,消息直接从客户端发送到客户端。 这就是为什么我们需要一个身份验证服务器来确保所有尝试连接的用户都是该应用程序的真实用户的原因。

回到代码,我们通过在订阅的通道上调用bind方法并传入事件名称作为第一个参数来侦听客户端事件。 第二个参数是一旦其他客户端(使用乘客应用程序请求乘车的任何人)触发此事件后要执行的功能。 在下面的代码中,我们显示一条警告消息,询问驾驶员是否要接受乘客。 请注意,该应用程序假设在任何一次只能有一名乘客。

// listen to the "driver-request" event
this .available_drivers_channel.bind( 'client-driver-request' , (passenger_data) => {
  
  if (! this .state.has_passenger){ // if the driver has currently no passenger
    // alert the driver that they have a request
    Alert.alert(
      "You got a passenger!" , // alert title
      "Pickup: " + passenger_data.pickup.name + "\nDrop off: " + passenger_data.dropoff.name, // alert body
      [
        {
          text : "Later bro" , // text for rejecting the request
          onPress: () => {
            console .log( 'Cancel Pressed' );
          },
          style : 'cancel'
        },
        {
          text : 'Gotcha!' , // text for accepting the request
          onPress: () => {
            // next: add code for when driver accepts the request
          }  
        },
      ],
      { cancelable : false } // no cancel button
    );

  }

});

一旦驾驶员同意接载乘客,我们就会订阅其私人频道。 该通道仅保留用于驾驶员和乘客之间的通信,这就是为什么我们在通道名称中使用唯一的乘客用户名的原因。

this .ride_channel = this .pusher.subscribe( 'private-ride-' + passenger_data.username);

available-drivers通道不同,在执行其他任何操作之前,我们需要监听订阅实际成功的时间( pusher:subscription_succeeded )。 这是因为我们将立即触发将客户事件发送给乘客的事件。 此事件( client-driver-response )是一个握手事件,用于让乘客知道向其发送请求的驾驶员仍然可用。 如果当时乘客仍未乘车,则乘客应用会触发同一事件,让驾驶员知道他们仍然可以接车。 此时,我们更新状态,以使UI相应更改。

this .ride_channel.bind( 'pusher:subscription_succeeded' , () => {
   // send a handshake event to the passenger
  this .ride_channel.trigger( 'client-driver-response' , {
    response : 'yes' // yes, I'm available
  });
  
  // listen for the acknowledgement from the passenger
  this .ride_channel.bind( 'client-driver-response' , (driver_response) => {
    
    if (driver_response.response == 'yes' ){ // passenger says yes

      //passenger has no ride yet
      this .setState({
        has_passenger : true ,
        passenger : {
          username : passenger_data.username,
          pickup : passenger_data.pickup,
          dropoff : passenger_data.dropoff
        }
      });
      
      // next: reverse-geocode the driver location to the actual name of the place
      
    } else {
      // alert that passenger already has a ride
      Alert.alert(
        "Too late bro!" ,
        "Another driver beat you to it." ,
        [
          {
            text : 'Ok'
          },
        ],
        { cancelable : false }
      );
    }

  });

});

接下来,我们使用地理编码库来确定驾驶员当前所在的地方的名称。 在后台,它使用Google Geocoding API,通常返回街道名称。 收到响应后,我们将触发found-driver事件,以告知乘客该应用已找到他们的驾驶员。 其中包含驱动程序信息,例如名称和当前位置。

Geocoder.getFromLatLng( this .state.region.latitude, this .state.region.longitude).then(
  ( json ) => {
    var address_component = json.results[ 0 ].address_components[ 0 ];
    
    // inform passenger that it has found a driver
    this .ride_channel.trigger( 'client-found-driver' , { 
      driver : {
        name : 'John Smith'
      },
      location : { 
        name : address_component.long_name,
        latitude : this .state.region.latitude,
        longitude : this .state.region.longitude,
        accuracy : this .state.accuracy
      }
    });

  },
  (error) => {
    console .log( 'err geocoding: ' , error);
  }
);  
// next: add componentDidMount code

组件安装完成后,我们将使用React Native的Geolocation API来监视位置更新。 每当位置更改时,传递给watchPosition函数的函数都会执行。

componentDidMount() {
  this .watchId = navigator.geolocation.watchPosition(
    ( position ) => {
     
      var region = regionFrom(
        position.coords.latitude, 
        position.coords.longitude, 
        position.coords.accuracy
      );
      // update the UI
      this .setState({
        region : region,
        accuracy : position.coords.accuracy
      });
      
      if ( this .state.has_passenger && this .state.passenger){
        // next: add code for sending driver's current location to passenger
      }
    },
    (error) => this .setState({ error : error.message }),
    { 
      enableHighAccuracy : true , // allows you to get the most accurate location
      timeout: 20000 , // (milliseconds) in which the app has to wait for location before it throws an error
      maximumAge: 1000 , // (milliseconds) if a previous location exists in the cache, how old for it to be considered acceptable 
      distanceFilter: 10 // (meters) how many meters the user has to move before a location update is triggered
    },
  );
}

接下来,将驾驶员的当前位置发送给乘客。 这将更新乘客应用程序上的UI,以显示驾驶员的当前位置。 稍后,当我们继续为乘客应用程序编码时,您将看到乘客应用程序如何绑定到该事件。

this .ride_channel.trigger( 'client-driver-location' , { 
  latitude : position.coords.latitude,
  longitude : position.coords.longitude,
  accuracy : position.coords.accuracy
});

接下来,我们要告知乘客和驾驶员他们已经彼此靠近了。 为此,我们使用helpers.js文件中的getLatLonDiffInMeters函数来确定乘客和驾驶员之间的米数。 由于驾驶员在接受请求时已经收到乘客的位置,因此只需获取驾驶员的当前位置并将其传递给getLanLonDiffInMeters函数即可获取米差。 从那里,我们只需根据仪表的数量通知驾驶员或乘客。 稍后,您将看到乘客应用程序中如何接收这些事件。

var diff_in_meter_pickup = getLatLonDiffInMeters(
  position.coords.latitude, position.coords.longitude, 
  this .state.passenger.pickup.latitude, this .state.passenger.pickup.longitude);

if (diff_in_meter_pickup <= 20 ){
  
  if (! this .state.has_ridden){
    // inform the passenger that the driver is very near
    this .ride_channel.trigger( 'client-driver-message' , {
      type : 'near_pickup' ,
      title : 'Just a heads up' ,
      msg : 'Your driver is near, let your presence be known!'
    });

    /*
    we're going to go ahead and assume that the passenger has rode 
    the vehicle at this point
    */
    this .setState({
      has_ridden : true
    });
  }

} else if (diff_in_meter_pickup <= 50 ){
  
  if (! this .state.nearby_alert){
    this .setState({
      nearby_alert : true
    });
    /* 
    since the location updates every 10 meters, this alert will be triggered 
    at least five times unless we do this
    */
    Alert.alert(
      "Slow down" ,
      "Your passenger is just around the corner" ,
      [
        {
          text : 'Gotcha!'
        },
      ],
      { cancelable : false }
    );

  }

}

// next: add code for sending messages when near the destination

在这一点上,我们假设驾驶员已经接载了乘客,并且他们现在正在前往目的地。 因此,这次我们得到了当前位置和下车点之间的距离。 一旦他们到达下车点20米,驾驶员应用程序就会向乘客发送一条消息,告知他们他们离目的地非常近。 完成此操作后,我们假设乘客将在几秒钟内下车。 因此,我们取消了正在监听的事件的绑定,并取消了该乘客的私人频道的订阅。 这有效地切断了驾驶员和乘客应用程序之间的连接。 唯一保持打开状态的连接是available-drivers通道。

var diff_in_meter_dropoff = getLatLonDiffInMeters(
  position.coords.latitude, position.coords.longitude, 
  this .state.passenger.dropoff.latitude, this .state.passenger.dropoff.longitude);

if (diff_in_meter_dropoff <= 20 ){
  this .ride_channel.trigger( 'client-driver-message' , {
    type : 'near_dropoff' ,
    title : "Brace yourself" ,
    msg : "You're very close to your destination. Please prepare your payment."
  });

  // unbind from passenger event
  this .ride_channel.unbind( 'client-driver-response' );
  // unsubscribe from passenger channel 
  this .pusher.unsubscribe( 'private-ride-' + this .state.passenger.username);

  this .setState({
    passenger : null ,
    has_passenger : false ,
    has_ridden : false
  });

}

// next: add code for rendering the UI

驾驶员应用程序的用户界面仅显示驾驶员和乘客的地图以及标记。

render() {
  return (
    < View style = {styles.container} >
      {
        this.state.region && 
         <MapView
          style={styles.map}
          region={this.state.region}
        >
            <MapView.Marker
              coordinate={{
              latitude: this.state.region.latitude, 
              longitude: this.state.region.longitude}}
              title={"You're here"}
            />
            {
              this.state.passenger && !this.state.has_ridden && 
              <MapView.Marker
                coordinate={{
                latitude: this.state.passenger.pickup.latitude, 
                longitude: this.state.passenger.pickup.longitude}}
                title={"Your passenger is here"}
                pinColor={"#4CDB00"}
              />
            }
        </MapView>
      }
    </View>
  );
}
// next: add code when component unmounts

在卸载组件之前,我们通过调用clearWatch方法来停止位置监视clearWatch

componentWillUnmount() {
  navigator.geolocation.clearWatch( this .watchId);
} 

最后,添加样式:

const styles = StyleSheet.create({
  container : {
    ...StyleSheet.absoluteFillObject,
    justifyContent : 'flex-end' ,
    alignItems : 'center' ,
  },
  map : {
    ...StyleSheet.absoluteFillObject,
  },
});

创建旅客应用

乘客应用程序将与驱动程序应用程序非常相似,因此我将不再详细介绍相似的部件。 继续并创建一个新的应用程序:

react-native init grabClone

安装依赖项

您还需要安装相同的库以及更多库:

npm install --save pusher-js react-native-geocoding github:geordasche/react-native-google-place-picker react-native-loading-spinner-overlay react-native-maps

其他两个库是Google Place PickerLoading Spinner Overlay 。 尽管由于与React Native Maps的兼容性问题(原始存储库中尚未解决),我们使用了Google Place Picker的一个分支

由于我们已经安装了相同的库,因此您可以返回到我们进行了一些其他配置才能使库正常工作的部分。 完成这些操作后,请回到这里。

接下来,Google Place Picker还需要一些其他配置才能工作。 首先,打开android/app/src/main/java/com/grabClone/MainApplication.java文件,并在最后一次导入下方添加以下内容:

import com.reactlibrary.RNGooglePlacePickerPackage;

getPackages()函数下添加刚导入的库。 当您在那里时,还请确保同时列出了MapsPackage()

protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
      new MainReactPackage(),
      new MapsPackage(),
      new RNGooglePlacePickerPackage() // <- add this
  );
}

接下来,打开android/settings.gradle文件,并将其添加到include ':app'指令的上方:

include ':react-native-google-place-picker'
project( ':react-native-google-place-picker' ).projectDir = new File(rootProject.projectDir,         '../node_modules/react-native-google-place-picker/android' )

在此期间,还请确保还添加了React Native Maps的资源:

include ':react-native-maps'
project( ':react-native-maps' ).projectDir = new File(rootProject.projectDir, '../node_modules/react-native-maps/lib/android' )

接下来,打开android/app/build.gradle文件,并在dependencies下添加以下内容:

dependencies {
  compile project ( ':react-native-google-place-picker' ) // <- add this
}

最后,确保已编译React Native Maps:

compile project ( ':react-native-maps' )

编码旅客应用

打开index.android.js文件并添加以下内容:

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

就像驱动程序应用程序一样,它也使用App.js作为主要组件。 继续并导入库。 它还使用相同的helpers.js文件,因此您也可以从驱动程序应用程序中复制它。

import React, { Component } from 'react' ;
import { StyleSheet, Text, View, Button, Alert } from 'react-native' ;

import Pusher from 'pusher-js/react-native' ;
import RNGooglePlacePicker from 'react-native-google-place-picker' ;
import Geocoder from 'react-native-geocoding' ;
import MapView from 'react-native-maps' ;
import Spinner from 'react-native-loading-spinner-overlay' ;

import { regionFrom, getLatLonDiffInMeters } from './helpers' ; 

Geocoder.setApiKey( 'YOUR GOOGLE SERVER API KEY' );

创建组件并声明默认状态:

export default class App extends Component  {
  state = {
    location : null , // current location of the passenger
    error: null , // for storing errors
    has_ride: false , // whether the passenger already has a driver which accepted their request
    destination: null , // for storing the destination / dropoff info
    driver: null , // the driver info
    origin: null , // for storing the location where the passenger booked a ride
    is_searching: false , // if the app is currently searching for a driver
    has_ridden: false // if the passenger has already been picked up by the driver
  };
  
  // next: add constructor code
}

为简单起见,我们在构造函数中声明乘客的用户名。 我们还初始化Pusher通道:

constructor () {
  super ();
  this .username = 'wernancheta' ; // the unique username of the passenger
  this .available_drivers_channel = null ; // the pusher channel where all drivers and passengers are subscribed to
  this .user_ride_channel = null ; // the pusher channel exclusive to the passenger and driver in a given ride
  this .bookRide = this .bookRide.bind( this ); // bind the function for booking a ride
}
// next: add bookRide() function

当用户点击“ Book Ride”按钮时, bookRide()函数被执行。 这将打开一个位置选择器,允许用户选择其目的地。 选定位置后,该应用就会向所有驾驶员发送出行请求。 如您先前在驱动程序应用程序中所看到的,这会触发一个警报以显示在驱动程序应用程序中,该警报询问驱动程序是否要接受请求。 此时,加载程序将继续旋转,直到驱动程序接受请求为止。

bookRide() {

  RNGooglePlacePicker.show( ( response ) => {
    if (response.didCancel){
      console .log( 'User cancelled GooglePlacePicker' );
    } else if (response.error){
      console .log( 'GooglePlacePicker Error: ' , response.error);
    } else {
      this .setState({
        is_searching : true , // show the loader
        destination: response // update the destination, this is used in the UI to display the name of the place
      });
      
      // the pickup location / origin
      let pickup_data = {
        name : this .state.origin.name,
        latitude : this .state.location.latitude,
        longitude : this .state.location.longitude
      };
      
      // the dropoff / destination
      let dropoff_data = {
        name : response.name,
        latitude : response.latitude,
        longitude : response.longitude
      };
      
      // send a ride request to all drivers
      this .available_drivers_channel.trigger( 'client-driver-request' , {
        username : this .username,
        pickup : pickup_data,
        dropoff : dropoff_data
      });

    }
  });
}
// next: add _setCurrentLocation() function

_setCurrentLocation()函数获取乘客的当前位置。 请注意,这里我们使用的是getCurrentPosition() ,而不是之前在驱动程序应用程序中使用的watchPosition() 。 两者之间的唯一区别是getCurrentPosition()仅获取一次位置。

_setCurrentLocation() {

  navigator.geolocation.getCurrentPosition(
    ( position ) => {
      var region = regionFrom(
        position.coords.latitude, 
        position.coords.longitude, 
        position.coords.accuracy
      );
      
      // get the name of the place by supplying the coordinates      
      Geocoder.getFromLatLng(position.coords.latitude, position.coords.longitude).then(
        ( json ) => {
          var address_component = json.results[ 0 ].address_components[ 0 ];
          
          this .setState({
            origin : { // the passenger's current location
              name: address_component.long_name, // the name of the place
              latitude: position.coords.latitude,
              longitude : position.coords.longitude
            },
            location : region, // location to be used for the Map
            destination: null , 
            has_ride : false , 
            has_ridden : false ,
            driver : null    
          });

        },
        (error) => {
          console .log( 'err geocoding: ' , error);
        }
      );

    },
    (error) => this .setState({ error : error.message }),
    { enableHighAccuracy : false , timeout : 10000 , maximumAge : 3000 },
  );

}

// next: add componentDidMount() function

在安装组件时,我们要设置乘客的当前位置,连接到auth服务器并订阅两个通道:可用的驱动程序和乘客的专用通道,用于仅与发送乘车请求的驾驶员进行通信。

componentDidMount() {

  this ._setCurrentLocation(); // set current location of the passenger
  // connect to the auth server
  var pusher = new Pusher( 'YOUR PUSHER API KEY' , {
    authEndpoint : 'YOUR AUTH SERVER ENDPOINT' ,
    cluster : 'YOUR PUSHER CLUSTER' ,
    encrypted : true
  });
  
  // subscribe to the available drivers channel
  this .available_drivers_channel = pusher.subscribe( 'private-available-drivers' );
  
  // subscribe to the passenger's private channel
  this .user_ride_channel = pusher.subscribe( 'private-ride-' + this .username);
  
  // next: add code for listening to handshake responses
  
}

接下来,添加用于侦听驱动程序握手响应的代码。 当驾驶员接受乘车请求时,这是从驾驶员应用程序发送的。 这使我们可以确保乘客仍在寻找乘车路线。 如果乘客回答“是”,则这是驾驶员发送其信息的唯一时间。

this .user_ride_channel.bind( 'client-driver-response' , (data) => {
  let passenger_response = 'no' ;
  if (! this .state.has_ride){ // passenger is still looking for a ride
    passenger_response = 'yes' ;
  }

  // passenger responds to driver's response
  this .user_ride_channel.trigger( 'client-driver-response' , {
    response : passenger_response
  });
});

// next: add listener for when a driver is found

驱动程序通过触发client-found-driver事件发送其信息。 如您先前在驱动程序应用程序中所看到的,它包含驱动程序的名称及其当前位置。

this .user_ride_channel.bind( 'client-found-driver' , (data) => {
  // the driver's location info  
  let region = regionFrom(
    data.location.latitude,
    data.location.longitude,
    data.location.accuracy 
  );

  this .setState({
    has_ride : true , // passenger has already a ride
    is_searching: false , // stop the loading UI from spinning
    location: region, // display the driver's location in the map
    driver: { // the driver location details
      latitude: data.location.latitude,
      longitude : data.location.longitude,
      accuracy : data.location.accuracy
    }
  });
  
  // alert the passenger that a driver was found
  Alert.alert(
    "Orayt!" ,
    "We found you a driver. \nName: " + data.driver.name + "\nCurrent location: " + data.location.name,
    [
      {
        text : 'Sweet!'
      },
    ],
    { cancelable : false }
  );      
});
// next: add code for listening to driver's current location

此时,乘客现在可以聆听驾驶员的位置变化。 每次触发此事件,我们只需更新UI:

this .user_ride_channel.bind( 'client-driver-location' , (data) => {
  let region = regionFrom(
    data.latitude,
    data.longitude,
    data.accuracy
  );
  
  // update the Map to display the current location of the driver
  this .setState({
    location : region, // the driver's location
    driver: {
      latitude : data.latitude,
      longitude : data.longitude
    }
  });

});

接下来是在特定实例上触发的事件。 它的主要目的是向驾驶员发送有关驾驶员位置( near_pickup )的更新,以及当他们已经在下车位置( near_dropoff )附近时发送更新。

this .user_ride_channel.bind( 'client-driver-message' , (data) => {
  if (data.type == 'near_pickup' ){ // the driver is very near the pickup location
    // remove passenger marker since we assume that the passenger has rode the vehicle at this point
    this .setState({
      has_ridden : true 
    });
  }

  if (data.type == 'near_dropoff' ){ // they're near the dropoff location
    this ._setCurrentLocation(); // assume that the ride is over, so reset the UI to the current location of the passenger
  }
  
  // display the message sent from the driver app
  Alert.alert(
    data.title,
    data.msg,
    [
      {
        text : 'Aye sir!'
      },
    ],
    { cancelable : false }
  );        
});

// next: render the UI

用户界面由加载微调框(仅在应用程序搜索驾驶员时可见),标题,用于预订乘车的按钮,乘客位置( origin )及其目的地以及最初显示当前位置的地图组成用户,然后在预订行程后显示驾驶员的当前位置。

render() {

  return (
    < View style = {styles.container} >
       <Spinner 
          visible={this.state.is_searching} 
          textContent={"Looking for drivers..."} 
          textStyle={{color: '#FFF'}} />
      <View style={styles.header}>
        <Text style={styles.header_text}>GrabClone</Text>
      </View>
      {
        !this.state.has_ride && 
        <View style={styles.form_container}>
          <Button
            onPress={this.bookRide}
            title="Book a Ride"
            color="#103D50"
          />
        </View>
      }
      
      <View style={styles.map_container}>  
      {
        this.state.origin && this.state.destination &&
        <View style={styles.origin_destination}>
          <Text style={styles.label}>Origin: </Text>
          <Text style={styles.text}>{this.state.origin.name}</Text>
         
          <Text style={styles.label}>Destination: </Text>
          <Text style={styles.text}>{this.state.destination.name}</Text>
        </View>  
      }
      {
        this.state.location &&
        <MapView
          style={styles.map}
          region={this.state.location}
        >
          {
            this.state.origin && !this.state.has_ridden &&
            <MapView.Marker
              coordinate={{
              latitude: this.state.origin.latitude, 
              longitude: this.state.origin.longitude}}
              title={"You're here"}
            />
          }
  
          {
            this.state.driver &&
            <MapView.Marker
              coordinate={{
              latitude: this.state.driver.latitude, 
              longitude: this.state.driver.longitude}}
              title={"Your driver is here"}
              pinColor={"#4CDB00"}
            />
          }
        </MapView>
      }
      </View>
    </View>
  );
}

最后,添加样式:

const styles = StyleSheet.create({
  container : {
    ...StyleSheet.absoluteFillObject,
    justifyContent : 'flex-end'
  },
  form_container : {
    flex : 1 ,
    justifyContent : 'center' ,
    padding : 20
  },
  header : {
    padding : 20 ,
    backgroundColor : '#333' ,
  },
  header_text : {
    color : '#FFF' ,
    fontSize : 20 ,
    fontWeight : 'bold'
  },  
  origin_destination : {
    alignItems : 'center' ,
    padding : 10
  },
  label : {
    fontSize : 18
  },
  text : {
    fontSize : 18 ,
    fontWeight : 'bold' ,
  },
  map_container : {
    flex : 9
  },
  map : {
   flex : 1
  },
});

现在您可以运行该应用程序了。 正如我在先决条件部分中提到的那样,您将可选地需要两台计算机,其中一台用于运行每个应用程序。 这将允许您为两者启用日志记录( console.log )。 但是,如果只有一台机器,则必须按特定顺序运行它们:先是乘客应用程序,然后是驾驶员应用程序。

继续,将您的Android设备连接到计算机,然后运行以下命令:

react-native run-android

这将在您的设备上编译,安装和运行该应用程序。 一旦运行,请终止观察程序并断开设备与计算机的连接。

接下来,打开Genymotion并启动您先前安装的设备。 这次,运行驱动程序。 应用运行后,您将看到一个空白屏幕。 这是正常现象,因为该应用需要一个位置才能进行渲染。 您可以通过单击模拟器UI右上方的“ GPS”来实现,然后启用GPS。

如果需要,也可以单击地图按钮并选择一个特定位置:

选择位置后,应用程序中的地图用户界面应显示您选择的相同位置。

接下来,您现在可以按照前面“ 应用程序流程”部分中的步骤进行操作。 请注意,您可以通过点击Genymotion Map UI来模拟正在行驶的车辆。 如果乘客已经预订了行程并且驾驶员已经接受了请求,则应该开始更新乘客的应用程序和驾驶员应用程序的驾驶员当前位置。

如果您使用的是两台机器,则只需在两台机器上运行react-native run-android 。 一个应该连接到您的设备,另一个应该打开Genymotion仿真器。

结论

而已! 在本教程中,您学习了如何利用Pusher创建乘车预订应用程序。 如您所见,您构建的应用程序非常简单。 我们仅坚持构建乘车预订应用程序中最重要的部分。 如果您愿意,可以向应用程序添加更多功能,也可以在自己的项目中使用它。 您可以在其GitHub repo上找到此应用程序中使用的源代码。

本教程最初发布在 Pusher Tutorial Hub中

From: https://hackernoon.com/creating-a-ride-booking-app-with-react-native-chnk31xn

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值