📜 ⬆️ ⬇️

Developing a simple pedometer application on ReactNative

image
Today, in programming circles, almost everyone knows about the Facebook library - React.


React is based on components. They are similar to the DOM elements of the browser, only written not in HTML, but using JavaScript. Using components, according to Facebook, allows you to write an interface once and display it on all devices. Everything is clear in the browser (these components are converted to DOM elements), but what about mobile applications? Here, too, is predictable: React components are converted to native components.


In this article I want to tell how to develop a simple application pedometer. A piece of code showing the main points will be displayed. The whole project is available at the link to GitHub .


So, let's begin.


Requirements

For iOS development, you will need OS X with Xcode. With Android, everything is simpler: you can choose from Linux, OS X, Windows. You will also have to install the Android SDK. An iPhone and any Android smartphone with a Lollipop on board will be required for combat testing.


Creating a project structure

To begin, create a project structure. To manipulate data in the application, we will use the idea of ​​flux, namely Redux, as its implementation. You will also need a router. As a router, I chose react-native-router-flux, since it supports Redux out of the box.


A few words about Redux. Redux is a simple library that stores application state. You can hang event handlers on the state change, including display rendering. I would like to get acquainted with redux using video tutorials.


We proceed to the implementation. Install react-native-cli with npm, with which we will perform further manipulations with the project.


npm install -g react-native-cli 

Next, create a project:


 react-native init AwesomeProject 

Install dependencies:


 npm install 

As a result, ios and android folders were created at the root of the project, in which there are “native” files for each of the platforms, respectively. The index.ios.js and index.android.js files are application entry points.


Install the necessary libraries:


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

Create a directory structure:


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

The actions folder will contain functions describing what happens to the data in the store.
components, as the name implies, will contain components of individual interface elements.
The containers contains the root components of each of the pages of the application.
constants - the name speaks for itself.
In reducers there will be so-called “reductors”. These are functions that change the state of an application depending on the data received.


Create the app.js folder in the app / containers folder. The redux wrapper acts as the root element of the application. All routes are written as ordinary components. The initial property tells the router which route should work when the application is initialized. In the component property of the route we pass the component that will be shown when switching to it.


 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> 

In the app / containers directory, create launch.js. launch.js is a normal component with a button to go to the counter page.


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

Actions - an object in which each route corresponds to a method. The names of such methods are taken from the name property of the route.
In the file app / constants / actionTypes.js, we describe the possible counter events:


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

In the app / actions folder, create a file counterActions.js with the contents:


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

The increment and decrement functions describe the action taking place to the reducer. Depending on the action, the reducer changes the state of the application. initialState - describes the initial state of the repository. When the application is initialized, 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 there are two buttons to decrease and increase the counter value, and the current value is displayed.


 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 the value of the counter itself are transferred from the container component. Consider 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} /> ); } } /*      .   props.state     */ export default connect(state => ({ state: state.counter }), /*    .      props.actions.increment()  props.actions.decrement() */ (dispatch) => ({ actions: bindActionCreators(counterActions, dispatch) }) )(CounterApp); 

As a result, we got a simple application that includes the necessary components. This application can be taken as a basis for any application developed with ReactNative.


Diagram

Since we are developing a pedometer application, accordingly we need to display the measurement results. The best way, I think, is a chart. Thus, we will develop a simple bar chart (bar chart): the Y axis shows the number of steps, and X shows the time.


ReactNative out of the box does not support canvas and, moreover, to use canvas, you must use a webview. Thus, there are two options: write a native component for each of the platforms or use a standard set of components. The first option is the most time-consuming, but, as a result, we get a productive and flexible solution. Let's stop on the second variant.


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


 [ { label, //     X value, //  color //   } ] 

Create three files:


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

Below is the main component code of 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) } } /*        .*/ 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) }); } /*       */ 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}/> )); } /*        */ 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> ); } } /*     */ Chart.propTypes = { data: PropTypes.arrayOf(React.PropTypes.shape({ value: PropTypes.number, label: PropTypes.string, color: PropTypes.string })), //    barInterval: PropTypes.number, //    labelFontSize: PropTypes.number, //      labelFontColor: PropTypes.string, //      borderColor: PropTypes.string, //   backgroundColor: PropTypes.string //    } export default Chart; 

Component implements the graph column:


 app/components/chart-item.js export default class ChartItem extends Component { constructor(props) { super(props); this.state = { /*    ,     */ animatedTop: new Animated.Value(1000), /*       */ 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; /*        */ 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 } }); 

Data Signature Component Code:


 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 obtained a simple histogram implemented using a standard set of components.


Pedometer

ReactNative is a fairly young project that has only the basic set of tools for creating a simple application that takes data from the network and displays it. But when there is a task of generating data on the device itself, you will have to work with writing modules in native languages ​​for the platforms.


At this stage, we have to write your pedometer. Not knowing objective-c and java, as well as api devices, it is difficult to make it, but it is possible, everything rests on time. Fortunately, there are projects such as Apache Cordova and Adobe PhoneGap. They have been on the market for quite some time, and the community has written many modules for them. These modules are easy to port under react. All logic remains unchanged, you only need to rewrite the interface (bridge).


In iOS, there is a great api for getting activity data - HealthKit. Apple has good documentation that even implements ordinary simple tasks. With Android, the situation is different. All that we have is a set of sensors. Moreover, the documentation says that, starting with api 19, it is possible to obtain data from the step sensor. A huge number of devices work on Android, and bona fide Chinese manufacturers and not only (including quite well-known brands) install only the main set of sensors: accelerometer, light sensor and proximity sensor. Thus, it is necessary to separately write the code for devices with Android 4.4+ and with a step sensor (as well as for older devices). This will improve the accuracy of measurements.


We proceed to the implementation.


Immediately make a reservation. I apologize for the quality of the code. I first encountered these programming languages ​​and had to understand at an intuitive level, since time was running out.


iOS

Create two files with contents:


 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; } /*         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) { /*   ,    callback   null,    */ if(success){ callback(@[[NSNull null]]); return; } /*    callback   */ callback(@[[error localizedDescription]]); }]; } /*        .     ,  –   ,   – callback */ 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){ /*      ,     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]; } /*   ,  callback,    null,    ,   –  . */ callback(@[[NSNull null], data ]); }]; [self.healthKitStore executeQuery:stepsQuery]; }; @end 

Next, these files need to be added to the project. Open Xcode, right click on the root directory -> Add Files to “project name”. In the Capabilities section, turn on the HealthKit. Further in the section General -> Linked Frameworks and Libraries click “+” and add HealthKit.framework.


With the native part finished. then go directly to the data in the js part of the project.
Create the file app / services / health.ios.js:


 app/services/health.ios.js /*    . BHealthKit   ,     BHealthKit.m */ const { BHealthKit } = React.NativeModules; let auth; //     function requestAuth() { return new Promise((resolve, reject) => { BHealthKit.askForPermissionToReadTypes([BHealthKit.Type.StepCount], (err) => { if (err) { reject(err); } else { resolve(true); } }); }); } //   . function requestData() { let date = new Date().getTime(); let before = new Date(); before.setDate(before.getDate() - 5); /*         ,    .*/ return new Promise((resolve, reject) => { BHealthKit.getStepsData(before.getTime(), date, (err, data) => { if (err) { reject(err); } else { let result = {}; /*           */ 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 code turned out to be voluminous, so I will describe the principle of operation.


Android SDK does not provide storage, referring to which you can get data for a certain period of time, but only the possibility of obtaining data in real time. To do this, use services that are constantly running in the background and perform the necessary tasks. On the one hand, this is very flexible, but suppose that twenty pedometers are installed on the device and each application will have its own service that performs the same task as the other 19.


We implement two services: for devices with a step sensor and without. These are android / app / src / main / java / com / awesomeproject / pedometer / StepCounterService.java and android / app / src / main / java / com / awesomeproject / pedometer / StepCounterOldService.java files.


In the android / app / src / main / java / com / awesomeproject / pedometer / StepCounterBootReceiver.java file, when the device starts, we describe which service will start depending on the device.


In the android / app / src / main / java / com / awesomeproject / RNPedometerModule.java and RNPedometerPackage.java files, we implement the connection of the application with react.


We get permission to use sensors by adding lines to 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" />      ,    ,       . <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> 

We connect the module to the application and run the services when the application starts.


 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 (!can) { startService(new Intent(this, StepCounterService.class)); } else { /*     */ startService(new Intent(this, StepCounterOldService.class)); } } 

Getting data javascript part. Create app / services / health.android.js
const Pedometer = React.NativeModules. PedometerAndroid;


 export default () => { /*      ,     . */ return new Promise((resolve, reject) => { Pedometer.getHistory((result) => { try { result = JSON.parse(result); //      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 received two files, health.ios.js and health.android.js, which receive statistics on user activity from native platform modules. Further, anywhere in the application expression:


 import Health from '<path>health'; 

React Native connects the desired file, based on the prefix of the files. Now we can use this function, without thinking, the application is running on IOS or Android.


As a result, we wrote a simple pedometer application and reviewed the main points that you have to go through when developing your own application.


In the end I want to highlight the advantages and disadvantages of ReactNative.


Benefits:

  1. a developer with JavaScript development experience can easily write an application;
  2. developing one application, you immediately get the opportunity to run it on Android and IOS;
  3. ReactNative has a fairly large set of implemented components that will often cover all your requirements;
  4. active community, which is rapidly writing various modules.

Disadvantages:

  1. the same code does not always run smoothly on both platforms (often display problems);
  2. with a specific task, there are often no implemented modules and you have to write them yourself;
  3. performance. In comparison with PhoneGap and Cordova, react is very fast, but still the native application will be faster.

When is it advisable to choose ReactNative?

If you need to develop a simple application to retrieve data from the server and display it, then the choice is obvious. If you are faced with the task of implementing a cool design, performance is critical, or there is a task that is difficult to solve with ready-made components, then you should think about it. Since most will have to write in the native languages ​​of the platforms, building a pyramid from this is definitely not the best option.


Thanks for attention.


The article was prepared by: greebn9k (Sergey Gribnyak), boozzd (Dmitry Shapovalenko), silmarilion (Andrey Khakharev)


')

Source: https://habr.com/ru/post/283494/


All Articles