» » Разработка простого приложения «шагомер» на ReactNative

 

Разработка простого приложения «шагомер» на ReactNative

Автор: admin от 13-05-2016, 11:41, посмотрело: 568

Разработка простого приложения «шагомер» на ReactNative
Сегодня в кругах программистов почти каждый знает о библиотеке 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;

Компонент реализующий столбец графика:


[code]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(

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



Источник: Хабрахабр

Категория: Программирование, Веб-разработка, Game Development, Android, iOS

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *