Статьи

Розробка простого додатка «крокомір» на 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?

Новости

Как создать фото из видео
Кризис заставляет искать дополнительные источники дохода. Одним из таких источников может стать торговля на валютном рынке Форекс. Но чтобы не потерять свои деньги необходимо работать с надежным брокером.

Как оформить группу в вконтакте видео
Дано хотел свой магазин в вк, но не знал с чего начать его делать. Так как хотелось не банальный магазин с кучей ссылок и фото, а красиво оформленный. С меню, с аватаркой. После просмотра видео создал

Как оформить диск малыш от рождения до года из фото и видео
Оформить диск "Малыш от рождения до года" из фото и видео можно совершенно разными способами! Кто-то для достижения данной цели идет на шоу-таланты, кто-то пользуется услугами профессионалов, а кто-то