Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How to Develop a Simple Step Counter App on ReactNative

DZone's Guide to

How to Develop a Simple Step Counter App on ReactNative

Check out the author's experience developing a step counter application on ReactNative, all while showing you how to do it yourself!

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

Nowadays, basically, every developer knows about React, an open-source JavaScript library from Facebook.

Components are the main constituent elements of React. They are quite similar to browser DOM elements but created with JavaScript, not HTML. According to Facebook, the use of components allows developers to build the interface just once and then display it on all devices and platforms. It is clear how it’s done in the browser — components are transformed into DOM elements — but what about mobile apps? The answer is simple: React components are transformed into native components.

In the following article, I’d like to share my experience on how to develop a simple step counter application. I will showcase the code and its main features. The project is available on GitHub.

So, let’s dig in!

Requirements

We need OS X with Xcode for iOS development. With Android, several options are available, such as Linux, OS X, Windows. Android SDK is also required. To test the app, we will use an iPhone and an Android smartphone with Lollipop.

Project’s Structure

First off, let’s build our project’s structure. To manipulate app’s data, we will use Flux and Redux. We will need a router as well. I decided to use react-native-router-flux because it supports Redux off the shelf.

What do we need to know about Redux? It is a simple library that stores an application’s state. The state may be modified by addition of event handlers, including display rendering. These are several basic facts, but you can always find more info about it on the Web.

Let’s start building our step counter application by installing react-native with npm. It will help us manipulate the project.

npm install -g react-native-cli


Then, create the project:

react-native init AwesomeProject


And set up the dependencies:

npm install

So far, we have created two folders — iOS and Android — in the project’s root file. There you will find native files for each platform, respectively. Index.ios.js and android.js files are the app’s entry points.

Let’s install the libraries:

npm install —save react-native-router-flux redux redux-thunk react-redux lodash


And create directories’ structure:

app/
        actions/
        components/
        containers/
        constants/
        reducers/
        services/


Functions will be stored in the actions folder. They will describe what’s going on with the data in the store.

Components will include components of separate interface elements.

Containers will contain the root components from every page of the app.

Constants is self-explanatory.

Reducers will contain reducers which are specific functions that modify app’s state according to incoming data.

Create app.js in the app/containers folder. Redux will act as a root element of the app. All routers are set up as ordinary components. Initial notifies the router, which route should be activated when the app is initialized. Then, render the currently activated component to the route’s component property.

app/containers/app.js
<Provider store={store}>
      <Router hideNavBar={true}>
          <Route
            name="launch"
            component={Launch}
            initial={true}
            wrapRouter={true}
            title="Launch"/>
          <Route
            name="counter"
            component={CounterApp}
            title="Counter App"/>
        </Router>
 </Provider>


Create launch.js in the app/containers directory. Launch.js is an ordinary component that includes a simple button to access the counter’s page.

app/containers/launch.js
import { Actions } from ‘react-native-router-flux';
…
     <TouchableOpacity
            onPress={Actions.counter}>
            <Text>Counter</Text>
      </TouchableOpacity>


Actions is the object in which every route corresponds to a given method. Method names are collected from the route’s name.

Let’s describe possible step counter actions in the app/constants/actionTypes.js file:

export const INCREMENT = 'INCREMENT';
        export const DECREMENT = 'DECREMENT';


Create counterActions.js in the app/actions folder:

app/actions/counterActions.js
import * as types from '../constants/actionTypes';
export function increment() {
  return {
    type: types.INCREMENT
  };
}

export function decrement() {
  return {
    type: types.DECREMENT
  };
}


Functions increment and decrement describe the current action to the reducer. Based the received action, the reducer changes the app’s state.

initialState describes the initial state of the store. During the app’s initialization process, the counter will be set to 0.

app/reducers/counter.js
import * as types from '../constants/actionTypes';

const initialState = {
  count: 0
};

export default function counter(state = initialState, action = {}) {
  switch (action.type) {
    case types.INCREMENT:
      return {
        ...state,
        count: state.count + 1
      };
    case types.DECREMENT:
      return {
        ...state,
        count: state.count - 1
      };
    default:
      return state;
  }
}



In the counter.js file, you can find two buttons — to increment and decrement counter’s value. Also, it displays the counter’s current value.

app/components/counter.js
const { counter, increment, decrement } = this.props;
…
<Text>{counter}</Text>
<TouchableOpacity onPress={increment} style={styles.button}>
          <Text>up</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={decrement} style={styles.button}>
          <Text>down</Text>
        </TouchableOpacity>


Event handlers and counter’s values are rendered from container’s component. Let’s have a closer look at it below.

app/containers/counterApp.js

import React, { Component } from 'react-native';
import {bindActionCreators} from 'redux';
import Counter from '../components/counter';
import * as counterActions from '../actions/counterActions';
import { connect } from 'react-redux';
class CounterApp extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    const { state, actions } = this.props;
    return (
      <Counter
        counter={state.count}
        {...actions} />
    );
  }
}
/* Make the component to modify the store’s state with the action. props.state displays the current state of the counter */
export default connect(state => ({
    state: state.counter
  }),
/* Add actions to the component. Get access to actions to manipulate the counter props.actions.increment() and props.actions.decrement() */
  (dispatch) => ({
    actions: bindActionCreators(counterActions, dispatch)
  })
)(CounterApp);



As a result, we have built a simple application with main components. It can be used as a base app for any other app on ReactNative.

The Diagram

To display the step counter results, it makes sense to create a simple bar chart: Y – to display the number of steps; X – to display the time.

Off-the-shelf, ReactNative doesn’t support canvas. To utilize canvas, we will also have to rely on webview. So, only two options are available to us: 1) write a native component for each platform; 2) use a standard set of components. The first option is time-consuming, yet the result is generally better in terms of scalability and flexibility. We will choose the second option, though.

To display the data, we will render them to component as an array of objects:

[
{
    label, // data displayed on X 
    value, // value
    color // bar color
}
]


Create three files:

app/components/chart.js
app/components/chart-item.js
app/components/chart-label.js


Below you will find the larger part of the code I wrote to create the diagram:

app/components/chart.js

import ChartItem from './chart-item';
import ChartLabel from './chart-label';

class Chart extends Component {
  constructor(props) {
    super(props);
    let data = props.data || [];

    this.state = {
      data: data,
      maxValue: this.countMaxValue(data)
    }
  }
/* function to calculate the max value of the transmitted data .*/
  countMaxValue(data) {
    return data.reduce((prev, curn) => (curn.value >= prev) ? curn.value : prev, 0);
  }
  componentWillReceiveProps(newProps) {
    let data = newProps.data || [];
    this.setState({
      data: data,
      maxValue: this.countMaxValue(data)
    });
  }
/* function to render the array of bar components */
  renderBars() {
    return this.state.data.map((value, index) => (
        <ChartItem
          value={value.value}
          color={value.color}
          key={index}
          barInterval={this.props.barInterval}
          maxValue={this.state.maxValue}/>
    ));
  }
/* function to render the array of components for bar labels */
  renderLabels() {
    return this.state.data.map((value, index) => (
        <ChartLabel
          label={value.label}
          barInterval={this.props.barInterval}
          key={index}
          labelFontSize={this.props.labelFontSize}
          labelColor={this.props.labelFontColor}/>
    ));
  }
  render() {
    let labelStyles = {
      fontSize: this.props.labelFontSize,
      color: this.props.labelFontColor
    };

    return(
      <View style={[styles.container, {backgroundColor: this.props.backgroundColor}]}>
        <View style={styles.labelContainer}>
          <Text style={labelStyles}>
            {this.state.maxValue}
          </Text>
        </View>
        <View style={styles.itemsContainer}>
          <View style={[styles.polygonContainer, {borderColor: this.props.borderColor}]}>
            {this.renderBars()}
          </View>
          <View style={styles.itemsLabelContainer}>
            {this.renderLabels()}
          </View>
        </View>
      </View>
    );
  }
}
/* validate the transmitted data */
Chart.propTypes = {
  data: PropTypes.arrayOf(React.PropTypes.shape({
    value: PropTypes.number,
    label: PropTypes.string,
    color: PropTypes.string
  })), // array of displayed data
  barInterval: PropTypes.number, // interval between the bars
  labelFontSize: PropTypes.number, // label’s font size
  labelFontColor: PropTypes.string, // label’s font color
  borderColor: PropTypes.string, // axis color
  backgroundColor: PropTypes.string // diagram’s background color
}

export default Chart;

Now, let’s dig deeper into the bar chart’s component:

app/components/chart-item.js

export default class ChartItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
/* use animation for bars, set initial values */
      animatedTop: new Animated.Value(1000),
/* current to max value ratio */
      value: props.value / props.maxValue
    }
  }

  componentWillReceiveProps(nextProps) {
    this.setState({
      value: nextProps.value / nextProps.maxValue,
      animatedTop: new Animated.Value(1000)
    });
  }

  render() {
    const { color, barInterval } = this.props;
/* animation is fired up when rendering */
    Animated.timing(this.state.animatedTop, {toValue: 0, timing: 2000}).start();

    return(
      <View style={[styles.item, {marginHorizontal: barInterval}]}>
        <Animated.View style={[styles.animatedElement, {top: this.state.animatedTop}]}>
          <View style={{flex: 1 - this.state.value}} />
          <View style={{flex: this.state.value, backgroundColor: color}}/>
        </Animated.View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  item: {
    flex: 1,
    overflow: 'hidden',
    width: 1,
    alignItems: 'center'
  },
  animatedElement: {
    flex: 1,
    left: 0,
    width: 50
  }
});

Label’s code is below:

app/components/chart-label.js
export default ChartLabel = (props) => {
  const { label, barInterval, labelFontSize, labelColor } = props;

  return(
    <View style={[{marginHorizontal: barInterval}, styles.label]}>
      <View style={styles.labelWrapper}>
        <Text style={[styles.labelText, {fontSize: labelFontSize, color: labelColor}]}>
          {label}
        </Text>
      </View>
    </View>
  );
}


As a result, we have created a simple histogram, using a standard set of components.

The Step Counter

ReactNative is a quite new project. It allows us to use a standard set of instruments to create simple applications that collect and display specific data from the server. Yet, when it comes to generating data on the device itself, we have to build unique modules on ‘native’ languages.

Our goal is to create a simple step counter application. If you don’t know objective-c, Java and API of given devices, it will be an extremely complex task. It is doable, though if you are really committed to the task and have enough time for development.

Fortunately, such projects as Apache Cordova and Adobe PhoneGap are easily accessible. There are not new to the market, and their communities have developed a bunch of different modules. These modules are easily portable to React. The logic part doesn’t change. All you need is to rewrite a bridge.

When it comes to iOS, you can rely on API HealthKit to collect data about in-app actions. Apple provides detailed guidelines, so it won’t be a problem to work with it. For instance, these guidelines include instructions as to how to solve simple problems, etc. It is quite the other story with Android. All we have is a set of sensors. Android guidelines are clear that API 19 enables the step counter feature, though. Android is a widespread OS that is installed on millions devices worldwide, yet brands tend to install only a standard set on sensors such as pedometers, step counters, light sensors, proximity sensors, etc. So, we will have to write the code for Android 4.4 devices, devices with step counter on board, and for older devices also. It will allow us to efficiently gather and analyze data.

Let the implementation begin.

Note: the code below is hardly perfect because I didn’t have much time. Also, I have never worked with these programming languages before.

iOS

Create two data files:

ios/BHealthKit.h
#ifndef BHealthKit_h
#define BHealthKit_h

#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"

@import HealthKit;

@interface BHealthKit : NSObject <RCTBridgeModule>

@property (nonatomic) HKHealthStore* healthKitStore;

@end

#endif /* BHealthKit_h */

ios/BHealthKit.m

#import "BHealthKit.h"
#import "RCTConvert.h"

@implementation BHealthKit

RCT_EXPORT_MODULE();

- (NSDictionary *)constantsToExport
{

  NSMutableDictionary *hkConstants = [NSMutableDictionary new];

  NSMutableDictionary *hkQuantityTypes = [NSMutableDictionary new];

  [hkQuantityTypes setValue:HKQuantityTypeIdentifierStepCount forKey:@"StepCount"];

  [hkConstants setObject:hkQuantityTypes forKey:@"Type"];

  return hkConstants;
}

/* method to ask for permission to get access to data from HealthKit */
RCT_EXPORT_METHOD(askForPermissionToReadTypes:(NSArray *)types callback:(RCTResponseSenderBlock)callback){

  if(!self.healthKitStore){
    self.healthKitStore = [[HKHealthStore alloc] init];
  }

  NSMutableSet* typesToRequest = [NSMutableSet new];

  for (NSString* type in types) {
    [typesToRequest addObject:[HKQuantityType quantityTypeForIdentifier:type]];

  }

  [self.healthKitStore requestAuthorizationToShareTypes:nil readTypes:typesToRequest completion:^(BOOL success, NSError *error) {
    /* if everything is fine, we call up a  callback with argument null that triggers the error */
    if(success){
      callback(@[[NSNull null]]);
      return;
    }
/* otherwise, send the error message to callback */
    callback(@[[error localizedDescription]]);   
  }];
}
/* method to receive the step count for a given time period. We send the initial time as the first argument, final time as the second one and callback as the third.
*/
RCT_EXPORT_METHOD(getStepsData:(NSDate *)startDate endDate:(NSDate *)endDate cb:(RCTResponseSenderBlock)callback){

  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];

  NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate];

  [dateFormatter setLocale:enUSPOSIXLocale];
  [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];

  HKSampleQuery *stepsQuery = [[HKSampleQuery alloc]
                               initWithSampleType:[HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]
                               predicate:predicate
                               limit:2000 sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {

    if(error){
      /* if there is an error, send its description to callback */
      callback(@[[error localizedDescription]]);

      return;
    }

    NSMutableArray *data = [NSMutableArray new];

    for (HKQuantitySample* sample in results) {

      double count = [sample.quantity doubleValueForUnit:[HKUnit countUnit]];
      NSNumber *val = [NSNumber numberWithDouble:count];

      NSMutableDictionary* s = [NSMutableDictionary new];

      [s setValue:val forKey:@"value"];
      [s setValue:sample.sampleType.description forKey:@"data_type"];

      [s setValue:[dateFormatter stringFromDate:sample.startDate] forKey:@"start_date"];
      [s setValue:[dateFormatter stringFromDate:sample.endDate] forKey:@"end_date"];

      [data addObject:s];
    }
   /* if everything is OK, call up a callback; null will be the first argument as there are ni mistakes, the array of data will come after it. */
    callback(@[[NSNull null], data ]);
  }];

  [self.healthKitStore executeQuery:stepsQuery];

};

@end

Then, you need to add these files to the project. Fire up Xcode, click the right mouse button on the catalogue -> Add Files to “project name”. Start up HealthKit in Capabilities tab. Then, go to General > Linked Frameworks and Libraries, press “+” and add HealthKit.framework.



This is it. The native part is ready. Now, we need to import data from the JS part of the project. For that end, let’s create app/services/health.ios.js:

app/services/health.ios.js

/* Add and start up our module. BHealthKit contains two methods that we created in BHealthKit.m
*/
const {
    BHealthKit
} = React.NativeModules;

let auth;
// function to request authorization rights
function requestAuth() {
    return new Promise((resolve, reject) => {
        BHealthKit.askForPermissionToReadTypes([BHealthKit.Type.StepCount], (err) => {
            if (err) {
                reject(err);
            } else {
                resolve(true);
            }
        });
    });
}
// function to request data
function requestData() {
    let date = new Date().getTime();
    let before = new Date();
    before.setDate(before.getDate() - 5);
    /* as native module requests are rendered asynchronously, add and return a promise */
    return new Promise((resolve, reject) => {
        BHealthKit.getStepsData(before.getTime(), date, (err, data) => {
            if (err) {
                reject(err);
            } else {
                let result = {};
/* Rended the data to display it as we need */
                for (let val in data) {
                    const date = new Date(data[val].start_date);
                    const day = date.getDate();
                    if (!result[day]) {
                        result[day] = {};
                    }
                    result[day]['steps'] = (result[day] && result[day]['steps'] > 0) ?
                        result[day]['steps'] + data[val].value :
                        data[val].value;
                    result[day]['date'] = date;
                }
                resolve(Object.values(result));
            }
        });
    });
}
export default () => {
    if (auth) {
        return requestData();
    } else {
        return requestAuth().then(() => {
            auth = true;
            return requestData();
        });
    }
}

Android



The Android code is very bulky, so I will just describe what I have done.

Android SDK doesn’t provide a storage to receive data in a given period of time. It only allows us to receive data in real time. Thus, we have to rely on services that are always on and collect the necessary data. On the one hand, this approach is very flexible. But it doesn't make any sense if, for instance, you have installed twenty step counters and each one will collect the same data as the other ones.

Let’s create two services: for devices with ster counters on board and without them. These are the following files:

  1. android/app/src/main/java/com/awesomeproject/pedometer/StepCounterService.java
  2. android/app/src/main/java/com/awesomeproject/pedometer/StepCounterOldService.java

Describe which service will start up when a given device is activated in android/app/src/main/java/com/awesomeproject/pedometer/StepCounterBootReceiver.java file.

Bind the app and React in android/app/src/main/java/com/awesomeproject/RNPedometerModule.java and RNPedometerPackage.java files.

Let’s get access to use the sensors by adding the following code in android/app/src/main/AndroidManifest.xml

<uses-feature
        android:name="android.hardware.sensor.stepcounter"
        android:required="true"/>
<uses-feature
        android:name="android.hardware.sensor.stepdetector"
        android:required=“true"/>
 <uses-feature
        android:name="android.hardware.sensor.accelerometer"
        android:required="true" />


Notify the app about our services and set the receiver which will activate the services when the smartphone is on.

<application>
…
<service android:name=".pedometer.StepCounterService"/>
      <service android:name=".pedometer.StepCounterOldService" />
      <receiver android:name=".pedometer.StepCounterBootReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
      </receiver>
</application>


Add on the module to our application. When the app is on, the services will be automatically activated.

android/app/src/main/java/com/awesomeproject/MainActivity.java

…
protected List<ReactPackage> getPackages() {
        return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new RNPedometerPackage(this)
        );
    }
…
@Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        Boolean can = StepCounterOldService.deviceHasStepCounter(this.getPackageManager());
/* if the device has a step counter sensor on board, activate a service that uses it */
        if (!can) {
            startService(new Intent(this, StepCounterService.class));
        } else {
/* otherwise, start up a service that uses the step counter*/
            startService(new Intent(this, StepCounterOldService.class));
        }
    }



Receive data from the JS part. Create app/services/health.android.js

const Pedometer = React.NativeModules.PedometerAndroid; file
export default () => {
/* create promise for request because the data is rendered asynchronously. */
  return new Promise((resolve, reject) => {
    Pedometer.getHistory((result) => {
      try {
        result = JSON.parse(result);
// Render the data to get necessary view
        result = Object.keys(result).map((key) => {
          let date = new Date(key);
          date.setHours(0);
          return {
            steps: result[key].steps,
            date: date
          }
        });
        resolve(result);
      } catch(err) {
        reject(err);
      };
    }, (err) => {
      reject(err);
    });
  });
}



As a result, we have created two files, health.ios.js and health.android.js, that collect data about user’s activity from native modules. We can use the following string of code in any part of our app:

import Health from ‘<path>health’;


ReactNative will automatically activate the necessary file based on its prefix. We can use this function on both iOS and Android platforms.

To Sum It Up:

We have developed a simple step counter application and looked through some keystones of its development. To finish the article, I’d like to point out some advantages and disadvantages of ReactNative.

Advantages

  • You can easily develop a completely new app if you are good at JavaScript;
  • You can use the same app on both iOS and Android but you create the code once;
  • You can utilize the power of React’s multiple ready-to-use components to meet most of your needs;
  • You can become a member of an active community that develops lots of modules with an amazing speed.

Disadvantages

  • Sometimes, the code can be rendered differently on different platforms. It causes errors and performance issues;
  • If you have a specific goal, it will be difficult to reach it using just standard modules. Most likely, you will need to build them yourself;
  • The operation speed may be higher. React is really impressive if compared with PhoneGap and Cordova but the native app will be much faster all the same.

When to Use ReactNative

If you need to develop a basic app that collects data from the server to render, the decision is quite obvious. But if you are going to create an app with a great design, impressive scalability and performance, ReactNative is hardly your option. The same is true if you know that the problem can’t be solved by using standard modules. If that is true, you will have to write the greater part of the code yourself, so it doesn’t make any sense to mount different modules and pieces of code on each other.

Thanks for your time! If you have any questions, ask them in the comments section below or through contacts on the site clever-solution.com.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
javascript ,react native ,react ,apps

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}