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!
Join the DZone community and get the full member experience.
Join For Free
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 Bitbucket.
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 the 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 the 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 the 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 the 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 the 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 of 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”. Startup HealthKit in the 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 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 step counters on board and without them. These are the following files:
- android/app/src/main/java/com/awesomeproject/pedometer/StepCounterService.java
- 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!
Opinions expressed by DZone contributors are their own.
Comments