Developing full-stack, cross-platform mobile apps: Build a run-tracking app using React Native – IBM Developer

Join the Digital Developer Conference: AIOps & Integration to propel your AI-powered automation skills Register for free

Developing full-stack, cross-platform mobile apps using React Native

In this tutorial, you’ll use React Native to build a cross-platform app that tracks a user’s running data, including run time, duration, distance, and location. You’ll access the device GPS and use it to show the user’s location on Google Maps.

What you’ll need to build your app

  • Android Studio, if you want to build and test your app for Android devices
  • Xcode, if you want to build and test your app for iOS devices
  • A phpMyAdmin local database (download the latest version from here: https://www.phpmyadmin.net/)
  • Google Developer account to create an API key
  • Ruby on Rails
  • Node.js (latest version)

Steps

We recommend that you type the code in each of the steps below to reinforce the learning process. Alternatively, you can follow along with the tutorial code in the GitHub repo.

Step 1: Create a React Native app

The first step is to create a React Native app. React Native enables you to start a project without installing or configuring any tools to build native code. You can create the app for Android or iOS.

Create a new React Native app in Android

  1. Assuming that you have Node.js installed, you can use npm to install the create-react-native-app:

    npm install -g expo-cli

  2. Run the following commands to create a run-tracking app:

    expo init RunTracking cd RunTracking

  3. Turn on the debugging option of your phone and connect your phone using a USB cable. To create a build of your app, run the command on the terminal:

    npm run android

Create a new React Native app in iOS

  1. Make sure you are still in the main project directory [RunTracking] inside your terminal.
  2. To switch to the iOS Directory, run the cd ios command.
  3. To install Cocoapod Dependencies, run the pod install command.
  4. Open XCode and install a simulator or a physical device, depending on which you want to use to run your app:

    Xcode Menu > •Open Developer Tool > •Simulator >• Hardware > •Device > •Manage Devices > •Simulators > •Left Column > •Click on + to create your device > •Add Simulator Name (in this case it will be iPhone X)…Device type to be iPhone Xs and OS Version iOS 13.3 > •Create
    
  5. Now go back to your main directory [RunTracking] and run the following command:

    npm run ios --simulator=”iPhone X”

Your app will now be running.

Step 2: Create the splash screen

The splash screen is the first visible screen that the user sees when the application is launched.

Splash screen

Here, the splash screen displays an image, which could be the application logo. Some data for the next screen is also fetched. In the app.json file:

{
  "expo": {
    "name": "mydistance",
    "slug": "mydistance",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "web": {
      "favicon": "./assets/favicon.png"
    }
  }
}

Step 3: Create the login screen

Let’s look at how the login page is built. If the user is not first registered, they must register on the app as shown here:

Login screen

These functions track changes to the state of email and password when a user inputs some information.

import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { StyleSheet, Button, View } from 'react-native';
import styled from 'styled-components';
import { userLogin } from '../Actions/authAction';

const SignIn = ({ navigation }) => {
    const dispatch = useDispatch();
    const { message } = useSelector(state => state.auth)
    const [email, setEmail] = React.useState('');
    const [password, setPassword] = React.useState('');
    const signIn = () => {
        dispatch(userLogin({
            email: email,
            password: password
        }))
    }
    return (
        <RegistrationView style={styles.screenContainer}>
            {undefined !== message && <ErrorMessage>{message}</ErrorMessage>}
            <Box>
                <Description>Email</Description>
                <TextInput onChangeText={text => setEmail(text)} />
            </Box>
            <Box>
                <Description>Password</Description>
                <TextInput onChangeText={text => setPassword(text)} secureTextEntry={true} />
            </Box>
            {/* <Button onPress={signIn}
                title="Sign In"
            /> */}

            <ButtonContainer onPress={signIn}>
                <ButtonText>Sign In</ButtonText>
            </ButtonContainer>
            <View>
                <Description>Don't have an account?</Description>
                <Button onPress={() => navigation.navigate('SignUp')} title="Sign Up" />
            </View>
        </RegistrationView>
    );
}
export default SignIn

const styles = StyleSheet.create({
    screenContainer: {
        flex: 1
    }
});

const TextInput = styled.TextInput`
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-radius: 8px;
    height: 40px;
    padding: 8px 15px;
`;
const Description = styled.Text`
   font-size: 16px;
   margin: 8px 0;
`;
const ErrorMessage = styled.Text`
   font-size: 16px;
   margin: 8px 0;
   color: red;
`;
const RegistrationView = styled.View`
   display: flex;
   align-items: center;
   height: 100%;
`;
const Box = styled.View`
   display: flex;
   justify-content: flex-start;
   width: 100%;
   padding: 8px 15px;
`;
const ButtonContainer = styled.TouchableOpacity`
    margin-top: 15px;
    width: 93%;
    height: 40px;
    padding: 8px 15px;
    border-radius: 8px;
    background-color: black;
    display: flex;
    align-items: center;
`;

const ButtonText = styled.Text`
    font-size: 16px;
    color: white;
    text-align: center;
`;

Step 4: Create the sign-up screen

For users who have not already registered in the app, they can use the sign-up screen:

Sign-up screen

Here is the code to create the sign up screen:

import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { StyleSheet, Button, View } from 'react-native';
import styled from 'styled-components';
import { createUser } from '../Actions/authAction';
// import AsyncStorage from '@react-native-async-storage/async-storage'
const SignUp = ({ navigation }) => {
    const dispatch = useDispatch();
    const [name, setname] = React.useState('');
    const [email, setEmail] = React.useState('');
    const [password, setPassword] = React.useState('');
    const signUp = () => {
        dispatch(createUser({
            name: name,
            email: email,
            password: password
        }))
        navigation.navigate('SignIn')
    }
    // useEffect(() => {
    //     AsyncStorage.clear().then((response) => {
    //         console.log("CLEAR: ",response);
    //     }).catch((error) =>console.log("error: ",error));
    // }, [])
    return (
        <RegistrationView style={styles.screenContainer}>
            <Box>
                <Description>Full Name</Description>
                <TextInput onChangeText={text => setname(text)} />
            </Box>
            <Box>
                <Description>Email</Description>
                <TextInput onChangeText={text => setEmail(text)} />
            </Box>
            <Box>
                <Description>Password</Description>
                <TextInput onChangeText={text => setPassword(text)} secureTextEntry={true} />
            </Box>
            <ButtonContainer onPress={signUp}>
                <ButtonText>Sign Up</ButtonText>
            </ButtonContainer>
            <View>
                <Description>Don't have an account?</Description>
                <Button onPress={() => navigation.navigate('SignIn')} title="Sign In" />
            </View>
        </RegistrationView>
    );
}
export default SignUp

const styles = StyleSheet.create({
    screenContainer: {
        flex: 1
    }
});

const TextInput = styled.TextInput`
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-radius: 8px;
    height: 40px;
    padding: 8px 15px;
`;
const Description = styled.Text`
   font-size: 16px;
   margin: 8px 0;
`;
const RegistrationView = styled.View`
   display: flex;
   align-items: center;
   height: 100%;
`;
const Box = styled.View`
   display: flex;
   justify-content: flex-start;
   width: 100%;
   padding: 8px 15px;
`;
const ButtonContainer = styled.TouchableOpacity`
    margin-top: 15px;
    width: 93%;
    height: 40px;
    padding: 8px 15px;
    border-radius: 8px;
    background-color: black;
    display: flex;
    align-items: center;
`;

const ButtonText = styled.Text`
    font-size: 16px;
    color: white;
    text-align: center;
`;

Step 5: Enable location tracking

In the next screen, you can give permission for the app to track your location using the phone settings:

import React from 'react';
import { Alert } from 'react-native';
import { Permissions } from 'expo-permissions';

const requestLocationPermission = async () => {
  const { status } = await Permissions.askAsync(Permissions.LOCATION);
  if (status !== 'granted') {
    Alert.alert(
      'Alert',
      'Permission to access location was denied',
      [{ text: 'OK', onPress: () => false }],
      { cancelable: false },
    );
  } else {
    return true;
  }
};

class CheckLocation extends React.Component {
  static async hasLocationPermission() {
    const gotPermission = await requestLocationPermission();
    if (gotPermission) {
      return true;
    }
    return false;
  }
}

export default CheckLocation;

Step 6: Creating the dashboard screen

Following are several methods used for the dashboard screen, where users can see the stop watch and the distance traveled by the user:

Dashboard screen showing distance traveled

The following method gets the user’s current location and sends it to the map using state management:

const getCurrentLocation = () => {
    Location.getCurrentPositionAsync({
      enableHighAccuracy: true,
    }).then(position => {
      const { latitude, longitude } = position.coords;
      setOrigin({ latitude: latitude, longitude: longitude })
      settingDestination();
    });
  };

The next method continuously retrieves the user’s position. It also stores an object to show the routes and the user’s current location:

const watchLocation = async () => {
  await Location.watchPositionAsync(
    {
      enableHighAccuracy: true,
      distanceInterval: 0,
    },
    position => {
      const { latitude, longitude } = position.coords;

      const newCoordinate = { latitude, longitude };
      if (marker) {
        coordinate.timing(newCoordinate).start();
      }
      setLatitude(latitude)
      setLongitude(longitude)
      setRouteCoordinates(routeCoordinates.concat([newCoordinate]))
    }
  )
}

Dashboard showing time running

Step 7: Creating the database back end

Now let’s take a look at the Ruby on Rails back end that we created for our app, starting with the authenticate method.

Controllers content

The authenticate method is used to authenticate the current user; it checks the header to determine if the user is logged in.

def authenticate
access_token = request.headers[:HTTP_ACCESS_TOKEN] ||   request.filtered_parameters["headers"]["ACCESS_TOKEN"]
    if access_token.present?
      token = Token.where(token: access_token).last
      if token.present? && token.user
        sign_in token.user
      else
        render json: {error: 'Unauthorized'}, status: 401
      end
    else
      render json: {error: 'Unauthorized'}, status: 401
    end
end

The signup method is used to sign up a new user. Request parameters are stored in the database, setting the user’s session and response.

def signup
    @user = User.new(user_params)
    if @user.save
      loop do
        @token = Devise.friendly_token
        break unless Token.where(token: @token).first
      end
      @user.tokens.create token: @token
      sign_in :user, @user
    else
      render json: { error: @user.errors.full_messages.to_sentence }, status: 400
    end
  end

The login method works in a similar fashion. The difference is that the login method verifies the email and password of the request parameters and then it checks the user’s email and password using cryptography. This information is stored in the database at registration time.

def login
    current_user = User.find_by email: params[:data][:user][:email]
    if current_user.present? and current_user.valid_password? params[:data][:user][:password]
      loop do
        @token = Devise.friendly_token
        break unless Token.where(token: @token).first
      end
      current_user.tokens.create token: @token
      sign_in :user, current_user
    else
      render json: { error: 'Incorrect email or password' }, status: 401
      return
    end
  end

Track distance

This method is used store data for the current user in our database. Two parameters are stored: distance and time, corresponding to the distance run by the user and the time taken to cover that distance. This method is called when the user’s stop-timer data is sent through the front end; it’s stored in the database and revers to the front end to stop the clock.

def track_distance
    track_distance = current_user.track_distances.new(distance: params[:data][:distance], current_time: params[:data]  [:current_time])
    if track_distance.save
       render json: { message: "added distance" }, status: 200
     else
       render json: { error: 'something went wrong' }, status: 400
     end
  end

The logout method logs out from both the front end and the back end. We have created multiple tokens on sign-up and login, which will be destroyed on logout. For the front end, we remove the key from asynchronous storage.

  def logout
    current_user.tokens.destroy_all
    render json: {message: 'logout successfully'},status: 200
  end

The user parameters use a private method.

 private
  def user_params
    params.require(:data).require(:user).permit(:id, :email, :first_name, :last_name, :password)
  end

Model content

This model defines the relationship with other models, such as tokens and track distance. In this case, we use the generate_authentication_token method, which generates random unique tokens.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :tokens
  has_many :track_distances

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless Token.where(token: token).first
    end
  end

  def ensure_authentication_token
    self.tokens.create token: generate_authentication_token
  end
end

The token model belongs to the user model, which means that the user has many tokens depending upon the user.

class Token < ApplicationRecord
    belongs_to :user
end

The TrackDistance model belongs to the user model, meaning that the user has many track results in this table.

class TrackDistance < ApplicationRecord
    belongs_to :user
end

Gemfile

The devise gem is used for prebuilding functionality for authenticating users. We have modified this module according to our app.

Migrations

With Ruby on Rails, we use the DeviseCreateUsers classes to migrate a user table according to the structured devise, and also so we can add extra fields, like first name.

class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      t.string :first_name
      t.string :last_name

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.inet     :current_sign_in_ip
      # t.inet     :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

Tokens migration

Here is the CreateTokens class:

class CreateTokens < ActiveRecord::Migration[5.1]
  def change
    create_table :tokens do |t|
      t.string :token
      t.integer :user_id

      t.timestamps
    end

 end
end

Track Distance

Here is the CreateTrackDistances class:

class CreateTrackDistances < ActiveRecord::Migration[5.1]
  def change
    create_table :track_distances do |t|
      t.decimal :distance
      t.integer :user_id
      t.string :current_time

      t.timestamps
    end
  end
end

Summary

You should now have a good idea of how to create full-stack, cross-platform mobile app for Android and iOS using React Native. Remember, you can download and experiment with the source code we used to build our example RunTracker app from my GitHub repo.