Статьи

Розробка простого додатка «крокомір» на ReactNative

  1. вимоги
  2. Створення структури проекту
  3. діаграма
  4. крокомір
  5. iOS
  6. Android
  7. Преимущества:

Сьогодні в колах програмістів майже кожен знає про бібліотеку Facebook - React
Сьогодні в колах програмістів майже кожен знає про бібліотеку Facebook - React.

В основі React лежать компоненти. Вони схожі з DOM елементами браузера, тільки написані не на HTML, а за допомогою JavaScript. Використання компонентів, за словами Facebook, дозволяє один раз написати інтерфейс і відображати його на всіх пристроях. У браузері все зрозуміло (дані компоненти перетворюються в DOM елементи), а що ж з мобільними додатками? Тут теж передбачувано: React компоненти перетворюються в нативні компоненти.


У даній статті я хочу розповісти, як розробити простий додаток-крокомір. Буде показана частина коду, яка відображає основні моменти. Весь проект доступний за засланні на GitHub .


Тож почнемо.


вимоги

Для розробки під iOS вам буде необхідна OS X з Xcode. З Android все простіше: можна вибирати з Linux, OS X, Windows. Також доведеться встановити Android SDK. Для бойового тестування будуть необхідні iPhone і будь-який Android смартфон з Lollipop на борту.


Створення структури проекту

Для початку створимо структуру проекту. Для маніпуляції з даними в додатку будемо використовувати ідею flux, а саме Redux як його реалізацію. Також потрібен буде роутер. Як роутера я вибрав react-native-router-flux, так як він з коробки підтримує Redux.


Пару слів про Redux. Redux - це проста бібліотека, яка зберігає стан додатка. На зміну стану можна навішати обробники події, включаючи рендеринг відображення. Ознайомитися з redux рекомендую по відеоуроку.


Приступимо до реалізації. Встановимо react-native-cli за допомогою npm, за допомогою якого будемо виконувати в подальшому всі маніпуляції з проектом.


npm install -g react-native-cli

Далі створюємо проект:


react-native init AwesomeProject

Встановлюємо залежності:


npm install

В результаті в корені проекту створилися папки ios і android, в яких знаходяться "нативні" файли під кожну з платформ відповідно. Файли index.ios.js і index.android.js є точками входу додатки.


Встановимо необхідні бібліотеки:


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

Створюємо структуру директорій:


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

В папці actions знаходитимуться функції, що описують, що відбувається з даними в store.
components, виходячи з назви, буде містити компоненти окремих елементів інтерфейсу.
containers містить кореневі компоненти кожної з сторінок додатку.
constants - назва говорить сама за себе.
У reducers будуть знаходитися так звані "редюсери". Це функції, які змінюють стан додаток залежно від отриманих даних.


В папці app / containers створимо app.js. Як кореневого елемента додатки виступає обгортка redux. Все Рауса прописуються у вигляді звичайних компонентів. Властивість initial говорить роутера, який роут повинен відпрацювати при ініціалізації програми. У властивість component роута передаємо компонент, який буде показаний при переході на нього.


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>

В директорії app / containers створюємо launch.js. launch.js - звичайний компонент c кнопкою для переходу на сторінку лічильника.


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

Actions - об'єкт, в якому кожному Рауса відповідає метод. Імена таких методів беруться з властивості name роута.
У файлі app / constants / actionTypes.js опишемо можливі події лічильника:


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

В папці app / actions створюємо файл counterActions.js з вмістом:


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

Функції increment і decrement описують те, що відбувається дія редюсеру. Залежно від дії, редюсер змінює стан програми. initialState - описує початковий стан сховища. При ініціалізації програми лічильник буде встановлено на 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; }}

У файлі counter.js розташовуються дві кнопки для зменшення і збільшення значення лічильника, а також відображається поточне значення.


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>

Обробники подій і саме значення лічильника передаються з компонента контейнера. Розглянемо його нижче.


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);

У підсумку ми отримали просте додаток, яке включає в себе необхідні компоненти. Цей додаток можна взяти за основу будь-якої програми, розробленого за допомогою ReactNative.


діаграма

Так як ми розробляємо програму-крокомір, відповідно нам потрібно відобразити результати вимірювань. Найкращим способом, як мені здається, є діаграма. Таким чином, розробимо просту столбчатую діаграму (bar chart): вісь Y показує кількість кроків, а X - час.


ReactNative з коробки не підтримує canvas і, до того ж, для використання canvas необхідно використовувати webview. Таким чином, залишається два варіанти: писати нативний компонент під кожну з платформ або використовувати стандартний набір компонент. Перший варіант найбільш трудомісткий, але, в результаті, отримаємо продуктивне і гнучке рішення. Зупинимося на другому варіанті.


Для відображення даних будемо передавати їх компоненту у вигляді масиву об'єктів:


[{Label, // відображається дані на осі X value, // значення color // колір стовпчика}]

Створюємо три файли:


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

Нижче код основного компонента діаграми:


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;

Компонент реалізує стовпець графіка:


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}} );

Код компонента підписи даних:


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>); }

У підсумку ми отримали просту гистограмму, реалізовану за допомогою стандартного набору компонентів.


крокомір

ReactNative - досить молодий проект, який має тільки основний набір інструментів для створення простого додатка, яке бере з мережі дані і відображає їх. Але, коли стоїть завдання створення даних на самому пристрої, доведеться попрацювати з написанням модулів на рідних для платформ мовами.


На даному етапі нам належить написати свій педометр. Не знаючи objective-c і java, а також api пристроїв, зробити це складно, але можна, - все упирається в час. Благо існують такі проекти, як Apache Cordova і Adobe PhoneGap. Вони вже досить давно присутні на ринку, і співтовариство написало багато модулів під них. Ці модулі легко перенести під react. Вся логіка залишається незмінною, потрібно тільки переписати інтерфейс (bridge).


В iOS для отримання даних активності є чудове api - HealthKit. Apple має хорошу документацію, в якій навіть присутні реалізації звичайних простих завдань. З Android інша ситуація. Все, що є у нас, - набір датчиків. Причому в документації написано, що, починаючи з api 19, є можливість отримувати дані датчика кроків. На Android працює величезна кількість пристроїв, і сумлінні китайські виробники і не тільки (включаючи досить імениті бренди) встановлюють лише основний набір датчиків: акселерометр, датчик освітленості і датчик наближення. Таким чином, доведеться окремо писати код для пристроїв з Android 4.4+ і з датчиком кроків (а також для більш старих пристроїв). Це дозволить поліпшити точність вимірювань.


Приступимо до реалізації.


Відразу обмовлюся. Перепрошую за якість коду. Я вперше зіткнувся з даними мовами програмування і довелося розбиратися на інтуїтивному рівні, так як часу було обмаль.


iOS

Створюємо два файли c вмістом:


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

Далі ці файли потрібно додати в проект. Відкриваємо Xcode, правою кнопкою по кореневого каталогу -> Add Files to "project name". У розділі Capabilities включаємо HealthKit. Далі в розділі General -> Linked Frameworks and Libraries тиснемо "+" і додаємо HealthKit.framework.


З нативной частиною закінчили. далі переходимо безпосередньо до отримання даних в js частини проекту.
Створюємо файл 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

Код вийшов об'ємний, тому я опишу принцип роботи.


Android SDK не надає сховище, звертаючись до якого можна отримати дані за певний проміжок часу, а лише можливість отримання даних в реальному часі. Для цього використовуються сервіси, які постійно працюють в фоні і виконують потрібні завдання. З одного боку, це дуже гнучко, але припустимо, що на пристрої встановлено двадцять шагомеров і кожен додаток буде мати свій сервіс, який виконує ту ж задачу, що і решта 19.


Реалізуємо два сервісу: для пристроїв з датчиком кроків і без. Це файли android / app / src / main / java / com / awesomeproject / pedometer / StepCounterService.java і android / app / src / main / java / com / awesomeproject / pedometer / StepCounterOldService.java.


У файлі android / app / src / main / java / com / awesomeproject / pedometer / StepCounterBootReceiver.java при запуску пристрою описуємо, який з сервісів буде запускатися в залежності від пристрою.


У файлах android / app / src / main / java / com / awesomeproject / RNPedometerModule.java і RNPedometerPackage.java реалізовуем зв'язок додатки з react.


Отримуємо дозвіл на використання датчиків, додавши рядки в 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>

Підключаємо модуль до додатка і при запуску програми запускаємо сервіси.


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)); }}

Отримання даних javascript частини. Створюємо файл 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);});}); }

У підсумку ми отримали два файли health.ios.js і health.android.js, які отримують статистику активності користувача з нативних модулів платформ. Далі в будь-якому місці програми виразом:


import Health from '<path> health';

React Native підключає потрібний файл, виходячи з префікса файлів. Тепер ми можемо використовувати цю функцію, не замислюючись, на IOS або Android виконується додаток.


В результаті, ми написали простеньке додаток-крокомір і розглянули основні моменти, які вам треба буде пройти при розробці власного додатка.


В кінці хочеться виділити переваги і недоліки ReactNative.


Преимущества:

  1. розробник, Який має досвід розробки на JavaScript, легко может Написати додаток;
  2. розробляючі один додаток, ви відразу отрімуєте можлівість Виконувати его на Android и IOS;
  3. ReactNative має Досить великий набір реалізованіх компонент, Які найчастіше покріють всі Ваші вимоги;
  4. активна спільнота, Пожалуйста Швидко темпами пише Різні модулі.

недоліки:

  1. не завжди гладко один і той же код працює на обох платформах (найчастіше проблеми з відображенням);
  2. при специфічної завданню часто немає реалізованих модулів і доведеться їх писати самому;
  3. продуктивність. У порівнянні з PhoneGap і Cordova, react дуже швидкий, але все ж нативное додаток буде швидше.

Коли доцільно вибрати ReactNative?

Якщо потрібно розробити простий додаток для отримання даних з сервера і їх відображення, то вибір очевидний. Якщо ж перед вами стоїть завдання реалізації крутого дизайну, критично важлива продуктивність, або ж стоїть завдання, яку складно вирішити за допомогою готових компонент, то тут варто задуматися. Так як більшу частину доведеться писати рідною мовою платформ, будувати піраміду з цього безумовно не найкращий варіант.


Дякуємо за увагу.


Статтю підготували: greebn9k (Сергій Грібняк), boozzd (Дмитро Шаповаленко), silmarilion (Андрій Хахарев)

У браузері все зрозуміло (дані компоненти перетворюються в DOM елементи), а що ж з мобільними додатками?
Reduce ((prev, curn) => ( curn.value> = prev)?
GetDate (); if (! result [day]) {result [day] = {};} result [day] [ 'steps'] = (result [day] && result [day] [ 'steps']> 0)?
Коли доцільно вибрати ReactNative?

Новости