image
Сегодня в кругах программистов почти каждый знает о библиотеке 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(Андрей Хахарев)

Поделиться с друзьями
-->

Комментарии (14)


  1. lizarge
    13.05.2016 12:10
    +6

    Я понимаю что это приложение лишь демонстрация и туториал.

    Но даже на примере такого простого приложения видно что использование React не привело к особому ускорению или улучшению процесса разработки, нативный интерфейс был бы роднее, быстрее и более кастомизируем. Сделать такой просто интерфейс в XCode заняло бы минут 20-60 минут, но он бы имел ряд преимуществ. Но даже здесь видно что основной код все равно пришлось писать нативно или адаптировать другие модули.

    Можно конечно сказать что кросс-интерфейс решения будут более эффективны на больших проектах, однако это не так, ибо с увеличением сложности возросло бы и количество хаков и тормозов, тем более на огромных проектах обычно хватает времени (денег) на нативную разработку.

    Интересно услышать мнение сообщества для какой ниши применение таких инструментов оправдано (из моего опыта есть только миграция большого cordova проекта в нативный код, еще до конца первого цикла разрабоки по выше сказаным причинам).


    1. ximet
      13.05.2016 13:31
      -1

      Этот подход логичен только для прототипирования приложений, не более того. Меня скорее интересует, как работать с ReactNative не дебажив его в эмуляторе для начала? Например Cordova позволяет сначала дебажить в обычном хроме, после уже билдить на мобилы. Тем самым разработка происходит немного быстрее. Что касается React-Native, когда начинал разбираться, я кроме как подхода билдь и дебаж — не нашел. Плюс ко всему, разочаровывает, что нет стабильности. От версии к версии могут произойти слишком большие изменения, и прошлый код уже не будет работоспособен, потому что некоторые методы удалены из новой. И приходится провозится достаточное время, чтобы всё заработало снова.


      1. webschik
        13.05.2016 15:06

        Конечно же там есть отладка. Синхронизация с Chrome DevTools, как с устройства, так и с эмулятора.


        1. ximet
          13.05.2016 15:17

          Единтсвенный минус, что оно постоянно отваливается.


          1. webschik
            13.05.2016 15:26
            +1

            Хм, пилил проект для себя на RN 0.21 и Nexus5, не помню особых проблем с отладкой. Да, пару раз отвалился дебаггер, но все ведь прекрасно понимают, что RN в активной разработке и авторы пытаются вводить немало улучшений с каждым релизом. Нужно дать время на стабилизацию проекта :)


    1. indestructable
      13.05.2016 15:14
      +1

      Когда хватает денег (и специалистов) на нативную разработку, то нужно выбирать нативную разработку. С этим сложно спорить.

      Сравнивать с Cordova тоже не очень правильно, она работает в браузере, а нейтив реакт работает с родными компонентами платформы.

      Для чего выбирать? Например, для использования архитектуры реакта. Какие UI-паттерны на iOS и Android? MVP в лучшем случае (я не уверен), императивно описываем все переходы состояний, в том числе работу с данными. Реактивный подход намного удобнее.


      1. tagantroy
        16.05.2016 10:55

        На днях смотрел на реализацию flux для андроид, плакал и смеялся одновременно, хотя для человека, который пишет фронтенд он показался прекрасным. Проблема в том, что не всегда паттерны из фронтенда хорошо ложатся на мобильную разработку.

        «MVP в лучшем случае (я не уверен)» — MVP/MVVM/VIPER может вполне подойти для решения различных проблем. FRP в мире мобилок очень активно развивается и не юзает его только ленивый. Проблема в том, что многие не хотят это все изучать и пишут под android использую концепции из 2010 года


    1. extempl
      15.05.2016 17:39

      Насколько я понимаю, здесь как раз (в том числе и для демонстрации) приведён тот самый кейс, когда под необходимый функционал нужно писать (или адаптировать из кордовы) плагин. Но это далеко не всегда необходимо. Если отбросить этот момент, то вполне подойдёт для простых по смыслу приложений, которые занимаются отображением, пересылкой данных, их обработкой и т. д.
      Кроме того, даже в данном случае, реализовав плагин под шагомер такое приложение должно быть проще поддерживать. Для добавления новой страницы или нового параметра в график достаточно работать только с одним кодом, а не с двумя нативными.


  1. dimka11
    13.05.2016 16:44

    KitKat и ниже, не поддерживается?


    1. bobych_spb
      16.05.2016 10:55

      дефолтовый пример заработал на 4.3. Как поведут себя более сложные приложения будем посмотреть.


    1. boozzd
      16.05.2016 10:55

      ReactNative поддерживает >Jelly Bean(Api 16). В примере если SDK level меньше 19 или отсутствует датчик шагов, используется акселерометр. Ниже KitKat не тестировал, но предполагаю, что работать будет.


  1. maxDanylenko
    16.05.2016 10:55

    Это UI приложение, хотелось бы видеть скрины, хоть это не главное, но все таки)


    1. chuikoffru
      18.05.2016 12:13

      Скрины есть на странице гитхаба — https://github.com/Singree/react-native-example-app#screenshots

      Сам случайно заметил :)


      1. maxDanylenko
        18.05.2016 12:19

        Спасибо, я не заметил((