Создание Android-приложения с помощью React Native

Автор оригинала: Wern Ancheta
  • Перевод
  • Tutorial


Как следует из названия, в этой статье мы рассмотрим процесс создания средствами React Native приложения под Android. Оно будет простеньким, всего лишь с текстовым полем. Туда нужно будет ввести имя одного из покемонов, а приложение покажет подробную информацию о полученных данных и зачитает её с помощью функции преобразования текста в речь.

Мы не будем касаться установки Android SDK, React Native и создания проекта, или каких-то иных инструментов разработчиков. Будем исходить из того, что это ваш первый опыт создания приложения с помощью React Native, поэтому подробно рассмотрим каждый кусок кода.

Создание сервера


Для начала нам нужно создать серверный компонент нашего приложения. Писать мы его будем на РНР, а в роли базы данных выступит CouchDB. Инструкции по установке: РНР и CouchDB. После инсталляции и настройки нужно проверить, чтобы была запущена база данных. Для этого выполним curl localhost:5984 и получим в ответ что-то вроде:

{"couchdb":"Welcome","uuid":"xxxxxxxxxxxxxxxx","version":"1.6.0","vendor":{"name":"Ubuntu","version":"15.10"}}

Для получения доступа к Futon, системе управления CouchDB, откройте в браузере http://localhost:5984/_utils. Кликните Create Database, чтобы создать базу данных для хранения подробных данных, получаемых от Pokemon API. Назовите базу pokedex и кликните Create. Можно было бы обратиться к API напрямую из React Native, но тогда увеличивается сетевой трафик. А если хранить всё в БД, то можно получать данные по единственному запросу и в любое время.

Сохранение данных


Теперь перейдём непосредственно к созданию серверного компонента. Внутри вашей папки веб-сервера сделайте рабочую папку, внутри неё создайте файл composer.json и внесите в него код:

{
    "require": {
        "doctrine/couchdb-odm":  "@dev"
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

Это означает, что библиотека doctrine/couchdb-odm теперь относится к нашему проекту. Она поможет нам работать с CouchDB в РНР. Для её установки запустите composer install.

Теперь создайте внутри рабочей папки файл pokemon.php, а затем внесите в него код:

<?php
require 'vendor/autoload.php';
set_time_limit(0);

$client = \Doctrine\CouchDB\CouchDBClient::create(array('dbname' => 'pokedex'));

$pokedex = file_get_contents('http://pokeapi.co/api/v1/pokedex/1/');
$pokedex_data = json_decode($pokedex, true);


foreach($pokedex_data['pokemon'] as $row){
    //get details
    $pokemon = file_get_contents('http://pokeapi.co/' . $row['resource_uri']);
    $pokemon = json_decode($pokemon, true);

    //get description
    $pokemon_description = file_get_contents('http://pokeapi.co/' . $pokemon['descriptions'][0]['resource_uri']);

    $pokemon['description'] = json_decode($pokemon_description, true)['description'];

    //get sprites
    $pokemon_sprites = file_get_contents('http://pokeapi.co' . $pokemon['sprites'][0]['resource_uri']);
    $pokemon_sprites = json_decode($pokemon_sprites, true);

    $pokemon['small_photo'] = 'http://pokeapi.co' . $pokemon_sprites['image'];

    $client->postDocument($pokemon);

}

Рассмотрим приведённый выше код. В первую очередь добавляется файл автозагрузки. Он автоматически подгружает все библиотеки, установленные нами с помощью Composer. Далее параметру set_time_limit присваивается нулевое значение. Дело в том, что у РНР-скриптов по умолчанию продолжительность выполнения ограничена, и по окончании этого времени скрипт прерывается. Обнулив вышеуказанный параметр, мы отключаем временной лимит.

На момент написания этой статьи существовал 721 покемон, и для каждого из них нам нужно выполнить по три HTTP-запроса, чтобы получить подробные данные, описание и спрайты.

<?php
require 'vendor/autoload.php';
set_time_limit(0);

Инициализируем CouchDB-клиент и внесём название нашей базы данных:

<?php
$client = \Doctrine\CouchDB\CouchDBClient::create(array('dbname' => 'pokedex'));

С помощью функции file_get_contents получаем от API полный список покемонов. Данные будут возвращены в формате JSON, поэтому для дальнейшей работы нам придётся конвертировать их в массив:

<?php
$pokedex = file_get_contents('http://pokeapi.co/api/v1/pokedex/1/');
$pokedex_data = json_decode($pokedex, true);

Прогоняем цикл по всем результатам:

<?php
foreach($pokedex_data['pokemon'] as $row){
    ...
}

Внутри цикла для каждого покемона мы обращаемся к resource_uri и используем его для создания URL, возвращающего подробные данные:

<?php
//get details
$pokemon = file_get_contents('http://pokeapi.co/' . $row['resource_uri']);
$pokemon = json_decode($pokemon, true);

Данные, полученные в ответ на предыдущий запрос, мы используем для создания запроса на описание покемона и его спрайты:

<?php
//get description
$pokemon_description = file_get_contents('http://pokeapi.co/' . $pokemon['descriptions'][0]['resource_uri']);

$pokemon['description'] = json_decode($pokemon_description, true)['description'];

//get sprites
$pokemon_sprites = file_get_contents('http://pokeapi.co' . $pokemon['sprites'][0]['resource_uri']);
$pokemon_sprites = json_decode($pokemon_sprites, true);

$pokemon['small_photo'] = 'http://pokeapi.co' . $pokemon_sprites['image'];

Сохраним полученную информацию в CouchDB:

<?php
$client->postDocument($pokemon);

Для начала сохранения откроем в браузере файл pokemon.php. Это займёт какое-то время, а пока можно переходить к следующему шагу.

Извлечение данных


Для извлечения данных из CouchDB нам нужно создать вид (view). Зайдите в созданную нами базу данных, кликните View в выпадающем меню и выберите Temporary view. Под текстовым полем map function вставьте код:

function(doc) {
  emit(doc.name, null);
}

Теперь кликните на кнопке Run, чтобы проверить, как отобразятся наши данные:



Кликните на Save as, введите в поле Design document название базы pokemon, а в поле View nameby_name. Теперь вернитесь в рабочую папку, создайте файл get.php и внесите в него код:

<?php
require 'vendor/autoload.php';

$client = \Doctrine\CouchDB\CouchDBClient::create(array('dbname' => 'pokedex'));

$pokemon = $_GET['name'];

$query = $client->createViewQuery('pokemon', 'by_name');
$query->setKey($pokemon);
$query->setReduce(false);
$query->setIncludeDocs(true);
$result = $query->execute();

if(!empty($result[0])){

    $data = $result[0];
    echo json_encode($data);

}else{
    $result = array('no_result' => true);
    echo json_encode($result);
}

Давайте разберём его. Сначала мы получаем имя покемона, отправленное нам приложением:

<?php
$pokemon = $_GET['name'];

С помощью вызова метода createViewQuery создаём вид, задаём имена для Design document и View name, а затем определяем значения опций. Метод setKey используется для описания запроса; фильтрация получаемых от вида данных осуществляется с помощью setReduce, а setIncludeDocs добавляет конкретный документ для каждого возвращённого результата. Вы могли заметить, что на приведённом выше скриншоте документ отсутствует. Дело в том, что, когда вызывается setIncludeDocs и аргумент имеет значение true, по умолчанию используется тот документ, который был сохранён при предыдущем обращении к файлу pokemon.php.

<?php
$query = $client->createViewQuery('pokemon', 'by_name'); // имена design document и view name
$query->setKey($pokemon); // назначает ключом имя pokemon 
$query->setReduce(false); // отключает reduce
$query->setIncludeDocs(true); // добавляет конкретный документ для каждого результата
$result = $query->execute(); // выполняет запрос

Далее нужно проверить наличие результатов. Если они есть, то возвращаем JSON-версию, в противном случае возвращаем сообщение об отсутствии данных.

<?php
if(!empty($result[0])){

    $data = $result[0];
    echo json_encode($data);

}else{
    $result = array('no_result' => true);
    echo json_encode($result);
}

Если вы работаете на локальной машине, то можете открыть для сервера доступ в интернет с помощью Ngrok. Либо воспользуйтесь внутренним IP, позднее он ещё будет использоваться в нашем приложении.

Создание приложения


Установка зависимостей


Начнём с инициализации нового проекта в React Native:

react-native init Pokedex

Когда операция будет завершена, посредством npm установите зависимости:

cd Pokedex
npm install lodash react-native-android-speech react-native-gifted-spinner --save

Что делает этот код:

  • lodash — используется для перевода текста в прописное начертание и, а также для извлечения из массива конкретных данных.
  • react-native-android-speech — преобразует текстовое описание в речь.
  • react-native-gifted-spinner — отображает анимацию загрузки при выполнении сетевого запроса.

Просмотреть все установленные модули можно в папке node_modules/react-native/node_modules.

По завершении установки зайдите в репозиторий React Native Android Speech и настройте в соответствии с инструкциями.

Выполнение сетевых запросов


В корневой директории проекта React Native создайте папку src, внутри неё создайте файл api.js и внесите в него код:

module.exports = function(pokemon){

    var url = 'http://192.168.xxx.xxx/pokedex/get.php?name=' + pokemon;

    return fetch(url).then(function(response){
        return response.json();
    }).then(function(json){
        return json;
    });
}

С помощью метода fetch экспортируется функция извлечения данных с сервера. Таким способом в React Native осуществляются сетевые запросы. Этот метод получает URL для создания запроса, возвращает обещание (promise) использовать метод then и получает ответ с помощью callback-функции.

Полученные в ответе данные не получится использовать «как есть», сначала придётся передать их методу json, который доступен из объекта response. Оттуда уже можно получить данные в формате JSON, вызвав ещё один метод then. Они передаются в callback-функцию в качестве аргумента, который мы затем и возвращаем.

Обратите внимание, что возвращаемый результат метода fetch также является обещанием. Поэтому, когда мы позже вызовем этот модуль из нашего главного скрипта, до доступа к JSON-данным снова придётся использовать метод then.

Внимание: удостоверьтесь, что вы поменяли http://192.168.xxx.xxx/ на ранее упомянутый IP адрес или доменное имя. Помните, что pokedex — это папка внутри веб-индекса.

Главный файл приложения


Откройте файл index.android.js. В нём должен присутствовать некий код по умолчанию, удалите его весь.

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

Импортируйте React Native и дополнительные зависимости проекта:

'use strict';

var React = require('react-native');
var tts = require('react-native-android-speech')
var GiftedSpinner = require('react-native-gifted-spinner');
var _ = require('lodash');

Инициализируйте все необходимые компоненты и API:

var {
  AppRegistry,
  StyleSheet,
  Text,
  TextInput,
  View,
  Image,
  ListView
} = React;

В React Native доступны различые компоненты и API. Они по умолчанию не загружаются, поэтому вам придётся конкретно указать всё, что нужно. Можете рассматривать их как элементы пользовательского интерфейса. Допустим, в веб-странице есть текстовые поля, списки, таблицы, изображения, вкладки и т.д. В React Native все эти элементы интерфейса являются компонентами. Например, есть компоненты для отображения картинок, для отображения панелей прокрутки, для создания списков и т.д.

API в React служат для получения доступа к разным возможностям устройства, вроде камеры и пуш-уведомлений. Какими-то API вы будете пользоваться достаточно редко, а какие-то нужны в каждом проекте. В качестве примеров можно привести StyleSheet и AppRegistry.

Вот короткое описание компонентов и API, которые мы используем в нашем приложении:

  • AppRegistry — регистрирует кастомные компоненты. В React Native всё подряд является компонентами, причём компоненты могут состоять из ещё более мелких компонентов.
  • StyleSheet — используется для описания используемых в приложении стилей.
  • Text — отвечает за отображение текста.
  • TextInput — используется для создания текстовых полей.
  • View — основной компонент при создании пользовательского интерфейса. Обычно используется в качестве обёртки.
  • Image — отображает изображения.
  • ListView — выводит списки.

Теперь импортируйте ранее созданный файл src/api.js. Это позволит осуществлять сетевые запросы с помощью вызова метода api.

var api = require('./src/api.js');

Создайте новый кастомный компонент в React, содержащий всю логику интерфейса и приложения:

var Pokedex = React.createClass({
    ...
});

Инициализируйте состояние внутри класса. Под «состоянием» в React Native подразумевается способ хранения данных, которые будут доступны в рамках всего компонента.

  • query — вводимый пользователем текст.
  • hasResult — говорит о том, что есть результат поиска.
  • noResult — говорит о том, что результата поиска нет. Он выполняет задачу, противоположную hasResult, однако используется для того, чтобы решить, отображать ли сообщение об отсутствии результата. Мы же не хотим, чтобы пользователь сразу увидел такое откровение при первой загрузке приложения, когда никакой поиск ещё не осуществлялся.
  • result — хранит текущий результат, полученный от сервера.
  • isLoading — отображает индикатор загрузки.
  • dataSource — содержит источник данных для вида со списком. Сначала создаётся новый экземпляр ListView.DataSource, который получает объект, содержащий функцию rowHasChanged. При изменении источника данных эта функция даёт ListView команду перерендерить строку в списке. В нашем случае источником является массив, состоящий из объектов типов покемонов. Если вы посмотрите на метод search, то поймёте, как предоставляются эти данные.

getInitialState: function(){
  return {
    query: null,
    hasResult: false,
    noResult: false,
    result: null,
    isLoading: false,
    dataSource: new ListView.DataSource({
      rowHasChanged: (row1, row2) => row1 !== row2,
    })
  }
},

Теперь взгляните на метод render. Он отвечает за рендеринг пользовательского интерфейса.

render: function() {
    ...
},

Интерфейс возвращается внутри метода.

return (
  <View style={styles.container}>
    <View style={styles.search}>
      <TextInput
        style={styles.text_input}
        onChangeText={this.changeText}
        onSubmitEditing={this.search}
        placeholder="Type a pokemon name"
      />
    </View>

    {
      this.state.hasResult &&

      <View style={styles.result}>
        <View style={styles.main_details}>
          <Image source={{uri: this.state.result.small_photo}}
                 style={styles.image_dimensions} resizeMode={Image.resizeMode.contain} />
          <Text style={styles.main_text}>{this.state.result.name}</Text>

          <ListView contentContainerStyle={styles.types} dataSource={this.state.types} renderRow={this.renderType}></ListView>

          <View style={styles.description}>
            <Text style={styles.description_text}>{this.state.result.description}</Text>
          </View>
        </View>
      </View>

    }

    {
      this.state.noResult &&
      <View style={styles.no_result}>
        <Text style={styles.main_text}>Pokemon not found</Text>
        <Text style={styles.sub_text}>Please type the exact name</Text>
      </View>
    }

    {
      this.state.isLoading &&
      <View style={styles.loader}>
        <GiftedSpinner />
      </View>
    }
  </View>
);

Разберём этот код. Создаём основной контейнер:

<View style={styles.container}>
</View>

Обратите внимание: это является обязательным условием, поскольку должен существовать единственный корневой компонент, а все остальные компоненты получаются вложенными. Значением атрибута style является объект, описывающий стилизацию компонента. Ниже вы узнаете, как объявляется объект styles, а пока просто запомните, что при использовании объектов в качестве значений не надо использовать кавычки.

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

  • onChangeText — задаёт функцию, выполняемую при каждом изменении текста в текстовом поле.
  • onSubmitEditing — задаёт функцию, выполняемую при каждой отправке текста в текстовом поле.
  • placeholder — задаёт текст, отображаемый при отсутствии данных для ввода.

<View style={styles.search}>
  <TextInput
    style={styles.text_input}
    onChangeText={this.changeText}
    onSubmitEditing={this.search}
    placeholder="Type a pokemon name"
  />
</View>

Далее идёт компонент для отображения результатов поиска. Его синтаксис немного отличается от предыдущего, он заключён в фигурные скобки, а в начале идёт условие. Благодаря этому React рендерит компонент только в том случае, если в состоянии хранится результат. Внутри этого компонента есть ещё два: Image, который выводит изображение покемона, и Text с именем зверушки. Далее идёт компонент ListView, он содержит информацию о виде покемона. Дело в том, что некоторые покемоны могут относиться к нескольким видам, поэтому нам нужен этот компонент. И наконец, компонент View отвечает за вывод описания покемона.

{
  this.state.hasResult &&

  <View style={styles.result}>
    <View style={styles.main_details}>
      <Image source={{uri: this.state.result.small_photo}}
             style={styles.image_dimensions} resizeMode={Image.resizeMode.contain} />
      <Text style={styles.main_text}>{this.state.result.name}</Text>

      <ListView contentContainerStyle={styles.types} dataSource={this.state.types} renderRow={this.renderType}></ListView>

      <View style={styles.description}>
        <Text style={styles.description_text}>{this.state.result.description}</Text>
      </View>
    </View>
  </View>

}

Давайте подробнее рассмотрим каждый из компонентов. Image получает атрибут source, который позволяет нам указать источник изображения. Это может быть картинка из сети, из локальной файловой системы или из ресурсов приложения. В нашем случае используется картинка из сети, а данные хранятся в состоянии. resizeMode задаёт изменение размера изображения, если оно не вписывается в рамку. В качестве значения использовано contain. Это означает, что изображение вписывается в рамку без искажения пропорций.

<Image source={{uri: this.state.result.small_photo}}
             style={styles.image_dimensions} resizeMode={Image.resizeMode.contain} />

Компонент Text отображает текст. В React Native любой выводимый текст должен помещаться в компонент Text.

<Text style={styles.main_text}>{this.state.result.name}</Text>

Компонент ListView отображает список. Здесь есть один нюанс: для определения стиля вместо атрибута style используется contentContainerStyle.

dataSource позволяет определить источник данных для рендеринга списка, а renderRow задаёт функцию, выполняемую при рендеринге каждого элемента списка.

<ListView contentContainerStyle={styles.types} dataSource={this.state.types} renderRow={this.renderType}></ListView>

После выведения на экран результатов поиска идёт компонент, который отображается при отсутствии результатов.

{
  this.state.noResult &&
  <View style={styles.no_result}>
    <Text style={styles.main_text}>Pokemon not found</Text>
    <Text style={styles.sub_text}>Please type the exact name</Text>
  </View>
}

Ниже идёт индикатор загрузки, использующий для отображения анимации модуль Gifted Spinner. Индикатор показывается только в том случае, если у состояния свойство isLoading имеет значение true. Это делается сразу перед выполнением сетевого запроса, а когда ответ получен, значение меняется на false.

{
  this.state.isLoading &&
  <View style={styles.loader}>
    <GiftedSpinner />
  </View>
}

Затем добавляется метод для рендеринга каждый элемент списка. В объявлении компонента ListView в качестве значения атрибута renderRow использовано this.renderType. Это как раз тот самый метод.

renderType: function(type){

  return (
    <View style={[styles[type.name], styles.type]}>
      <Text style={styles.type_text}>{type.name}</Text>
    </View>
  );

},

Если вы посмотрите код ListView, то увидите, что в атрибуте renderRow мы нигде не биндили type, к которому обращаемся ниже. Дело в том, что renderRow автоматически передаёт данные скрытым образом.

Как следует из ответа, возвращаемого сервером, объект types содержит массив объектов, соответствующих разным типам покемонов:

[
    {
        "name":"electric",
        "resource_uri":"\/api\/v1\/type\/13\/"
    }
]

Мы обращаемся к этому объекту в методе renderType через аргумент type. Он используется для отображения типа покемона и управления стилем. Далее, в объявлении стиля понадобится добавить разные стили для каждого типа. Как вы могли заметить, для компонента View у нас используется два объявления стиля. Таким образом в React Native добавляется в массив каждое объявление стиля.

<View style={[styles[type.name], styles.type]}>
  <Text style={styles.type_text}>{type.name}</Text>
</View>

Теперь добавляется метод changeText, исполняемый при каждом изменении текста в текстовом поле. Поскольку этому методу передаются актуальные текстовые данные, он является хорошим местом для обновления свойства состояния, хранящего текущую введённую пользователем информацию.

changeText: function(text){
  this.setState({
    query: text
  });
},

Метод search исполняется при каждой отправке данных в текстовом поле. При вводе текста в Android появляется кнопка Done, нажатие которой запускает событие onSubmitEditing. Выше мы уже определяли this.search в качестве значения для атрибута onSubmitEditing. В результате выполняется следующий метод:

search: function(){
  var pokemon = _.capitalize(this.state.query);

  this.setState({
    isLoading: true
  });

  api(pokemon).then(
    (data) => {

      var speech = 'Pokemon was not found. Please type the exact name.';

      if(data.doc){
        var types = this.state.dataSource.cloneWithRows(data.doc.types);

        this.setState({
          hasResult: true,
          noResult: false,
          result: data.doc,
          types: types,
          isLoading: false
        });

        var type_names = _.map(data.doc.types, function(type){
           return type.name;
        });

        speech = data.doc.name + ". A " + type_names.join(' and ') + ' pokemon. ' + data.doc.description;

      }else{

        this.setState({
          hasResult: false,
          noResult: true,
          isLoading: false,
          result: null
        });

      }

      tts.speak({
        text: speech,
        forceStop : true ,
        language : 'en'
      });

    }
  );

}

Разберёмся с этим кодом. Предоставляемый Lodash метод capitalize вызывается для преобразования всех букв в прописные, за исключением первых букв. Затем состояние обновляется, а свойство isLoading получает значение true, благодаря чему под последним компонентом отображается индикатор загрузки.

var pokemon = _.capitalize(this.state.query);

this.setState({
  isLoading: true
});

С помощью модуля api выполняем сетевой запрос:

api(pokemon).then(
  (data) => {
    ...
  }
);

Обратите внимание: синтаксис callback-функции немного отличается от привычного нам.

api(pokemon).then(function(data){
  ...
});


Отличается также значение объекта this. Согласно новому синтаксису, этот объект ссылается на внешнюю область видимости, а не на callback-функцию. Это позволяет использовать this для обновления состояния изнутри callback-функции, без необходимости создания переменной для хранения текущей области видимости.

Внутри callback-функции задаётся текст по умолчанию, который будет преобразовываться в речь.

var speech = 'Pokemon was not found. Please type the exact name.';

Если в результатах присутствует объект doc, то массив типов извлекается и передаётся в качестве аргумента в метод cloneWithRows, который находится в состоянии, в ранее инициализированном компоненте dataSource. Таким образом возвращается объект, который можно использовать в качестве значения атрибута dataSource в ListView.

Для отображения результатов в интерфейсе обновляем состояние. После этого создаётся новый массив type_names, содержащий только типы, к которым принадлежит покемон. Делается это с помощью предоставляемого Lodash метода map. После этого можно преобразовывать в речь имя покемона, его тип и описание:

if(data.doc){
  //create the list view data source
  var types = this.state.dataSource.cloneWithRows(data.doc.types);

  //update the state
  this.setState({
    hasResult: true,
    noResult: false,
    result: data.doc,
    types: types,
    isLoading: false
  });

  //create an array containing the type names
  var type_names = _.map(data.doc.types, function(type){
     return type.name;
  });

  //construct the text to be used for the speech
  speech = data.doc.name + ". A " + type_names.join(' and ') + ' pokemon. ' + data.doc.description;
}

Иными словами, вы должны настроить все необходимые значения в состоянии:

  • hasResult нужно присвоить значение false, чтобы не отображался компонент, ответственный за вывод результата.
  • noResult нужно присвоить true, чтобы отображалось сообщение об отсутствии результата.
  • isLoading нужно присвоить false, чтобы индикаторы загрузки был скрыт.
  • result нужно присвоить null для очистки предыдущего результата.


...
else{

  this.setState({
    hasResult: false,
    noResult: true,
    isLoading: false,
    result: null
  });
}

Сразу под условиями задействуйте модуль преобразования в речь:

if(data.doc){
    ...
}else{
    ...
}

tts.speak({
  text: speech,
  forceStop : true ,
  language : 'en'
});

После закрывающих скобок класса Pokodex добавьте стили с помощью StyleSheet API:

var styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFF'
  },
  search: {
    flex: 1
  },
  result: {
    flex: 8
  },
  no_result: {
    flex: 8,
    alignItems: 'center'
  },
  loader: {
    flex: 1,
    alignItems: 'center'
  },
  main_details: {
    padding: 30,
    alignItems: 'center'
  },
  image_dimensions: {
    width: 100,
    height: 100
  },
  main_text: {
    fontSize: 25,
    fontWeight: 'bold',
    textAlign: 'center'
  },
  sub_text: {
    color: '#6e6e6e'
  },
  description: {
    marginTop: 20
  },
  text_input: {
    height: 40,
    borderColor: 'gray',
    borderWidth: 1
  },
  types: {
    flexDirection: 'row',
    marginTop: 20
  },
  type: {
    padding: 5,
    width: 100,
    alignItems: 'center'
  },
  type_text: {
    color: '#fff',
  },
  normal: {
    backgroundColor: '#8a8a59'
  },
  fire: {
    backgroundColor: '#f08030'
  },
  water: {
    backgroundColor: '#6890f0'
  },
  electric: {
    backgroundColor: '#f8d030'
  },
  grass: {
    backgroundColor: '#78c850'
  },
  ice: {
    backgroundColor: '#98d8d8'
  },
  fighting: {
    backgroundColor: '#c03028'
  },
  poison: {
    backgroundColor: '#a040a0'
  },
  ground: {
    backgroundColor: '#e0c068'
  },
  flying: {
    backgroundColor: '#a890f0'
  },
  psychic: {
    backgroundColor: '#f85888'
  },
  bug: {
    backgroundColor: '#a8b820'
  },
  rock: {
    backgroundColor: '#b8a038'
  },
  ghost: {
    backgroundColor: '#705898'
  },
  dragon: {
    backgroundColor: '#7038f8'
  },
  dark: {
    backgroundColor: '#705848'
  },
  steel: {
    backgroundColor: '#b8b8d0'
  },
  fairy: {
    backgroundColor: '#e898e8'
  }
});

Разберём этот кусок кода. У нас есть основной контейнер, в котором flex присвоено значение 1, поскольку для создания макета используется Flexbox. Единица означает, что задействуется весь экран. Так сделано потому, что мы применили стиль к корневому компоненту. А поскольку у нас нет других компонентов того же уровня, то используется весь экран.

container: {
  flex: 1,
  backgroundColor: '#FFF'
},

У нас есть стили для поиска, вывода результата, отсутствия результата и загрузки:

search: {
  flex: 1
},
result: {
  flex: 8
},
no_result: {
  flex: 8,
  alignItems: 'center'
},
loader: {
  flex: 1,
  alignItems: 'center'
},

Все они являются дочерними, и потому совместно используют доступную площадь. Раз корневой компонент растянулся на весь экран, то и его дочерние компоненты будут делить между собой всю площадь экрана. Удобнее всего это воспринимать в виде дробей. У компонентов поиска и загрузки flex: 1, поэтому им достанется меньше всего места. Поскольку одновременно могут отображаться 10 секций, то каждый из них получит по 1 секции, а компоненты наличия и отсутствия результатов занимают 8 секций.

<View style={styles.search}>
    ...
</View>

<View style={styles.result}>
    ...
</View>

<View style={styles.no_result}>
    ...
</View>

<View style={styles.loader}>
    ...
</View>

В качестве контейнера для изображения, имени, типа и описания покемона выступает main_details. Выровняйте все по центру, но не забудьте про поля, чтобы текст не лип к краям.

main_details: {
  padding: 30,
  alignItems: 'center'
},

Далее идёт объявление стилей – стандартный CSS.

image_dimensions: {
  width: 100,
  height: 100
},
main_text: {
  fontSize: 25,
  fontWeight: 'bold',
  textAlign: 'center'
},
sub_text: {
  color: '#6e6e6e'
},
description: {
  marginTop: 20
},

Теперь очередь стилей для списка типов. В компоненте ListView аргумент flexDirection имеет значение row. Это означает, что отведённое для списка пространство поделено на секции в соответствии со строками, что обеспечивает эффект поточного встраивания (inline effect). То есть элементы списка рендерятся один за другим. Ширина у них одинаковая вне зависимости от длины текста, поскольку для каждого элемента задаётся параметр width. Благодаря отступу шириной в 5 пикселей текст не прилипает к краям контейнера. Для выравнивания текста по центру используется alignItems.

types: {
  flexDirection: 'row',
  marginTop: 20
},
type: {
  padding: 5,
  width: 100,
  alignItems: 'center',
},
type_text: {
  color: '#fff',
},

Далее настраиваем стили для типа покемона. К примеру, если это электрический покемон, то фон контейнера будет жёлтого цвета.

normal: {
  backgroundColor: '#8a8a59'
},
fire: {
  backgroundColor: '#f08030'
},
water: {
  backgroundColor: '#6890f0'
},
electric: {
  backgroundColor: '#f8d030'
},
grass: {
  backgroundColor: '#78c850'
},
ice: {
  backgroundColor: '#98d8d8'
},
fighting: {
  backgroundColor: '#c03028'
},
poison: {
  backgroundColor: '#a040a0'
},
ground: {
  backgroundColor: '#e0c068'
},
flying: {
  backgroundColor: '#a890f0'
},
psychic: {
  backgroundColor: '#f85888'
},
bug: {
  backgroundColor: '#a8b820'
},
rock: {
  backgroundColor: '#b8a038'
},
ghost: {
  backgroundColor: '#705898'
},
dragon: {
  backgroundColor: '#7038f8'
},
dark: {
  backgroundColor: '#705848'
},
steel: {
  backgroundColor: '#b8b8d0'
},
fairy: {
  backgroundColor: '#e898e8'
}

Зарегистрируйте компонент в AppRegistry. Тогда React Native будет рендерить компонент Pokedex при открытии приложения.

AppRegistry.registerComponent('Pokedex', () => Pokedex);

Запуск приложения


Сначала удостоверьтесь, что работает ваш РНР-бэкенд, а затем с помощью команды react-native run-android запустите компиляцию и выполнение приложения на вашем устройстве или в эмуляторе. В этот момент вы получите ошибку:



Дело в том, что сначала нужно запустить сервер React, который на лету конвертирует компоненты. Приложение будет автоматически перекомпилировано при каждом изменении исходных файлов (например, index.android.js). Для запуска сервера React в терминале выполните команду react-native start. Вы увидите нечто подобное:

[7:38:33 AM] <START> Building Dependency Graph
[7:38:33 AM] <START> Crawling File System
[7:38:33 AM] <START> Loading bundles layout
[7:38:33 AM] <END>   Loading bundles layout (1ms)

React packager ready.

[7:38:46 AM] <END>   Crawling File System (13516ms)
[7:38:46 AM] <START> Building in-memory fs for JavaScript
[7:38:52 AM] <END>   Building in-memory fs for JavaScript (6200ms)
[7:38:52 AM] <START> Building in-memory fs for Assets
[7:38:59 AM] <END>   Building in-memory fs for Assets (6048ms)
[7:38:59 AM] <START> Building Haste Map
[7:39:03 AM] <START> Building (deprecated) Asset Map
[7:39:05 AM] <END>   Building (deprecated) Asset Map (2191ms)
[7:39:08 AM] <END>   Building Haste Map (9348ms)
[7:39:08 AM] <END>   Building Dependency Graph (35135ms)

По завершении Building Dependency Graph откройте новое окно с терминалом и выполните команду adb shell input keyevent 82. Это откроет на устройстве меню разработчика. Выберите пункт Dev settings, в секции Debugging введите внутренний IP-адрес вашего компьютера и порт, на котором выполняется сервер React.



Затем вернитесь на главный экран приложения, снова откройте меню разработчика и выберите пункт Reload JS. Приложение будет перезапущено, теперь всё должно работать.

Распространённые проблемы


Запуск сервера React


Если вы получили ошибку при запуске, то причиной наверняка является Watchman.

Error building DependencyGraph:
Error: Watcher took too long to load
Try running `watchman version` from your terminal
https://facebook.github.io/watchman/docs/troubleshooting.html
at [object Object]._onTimeout (index.js:103:16)
at Timer.listOnTimeout (timers.js:89:15)

Для её исправления выполните следующие команды:

sudo sysctl fs.inotify.max_user_instances=99999
sudo sysctl fs.inotify.max_user_watches=99999
sudo sysctl fs.inotify.max_queued_events=99999

watchman shutdown-server

Если не сработало, попробуйте так:

echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_user_instances
echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_user_watches
echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_queued_events

watchman shutdown-server

Эти команды задают количество просмотров разных корневых папок, вложенных в них папок, а также количество событий в очереди. Последняя строка прерывает выполнение сервера Watchman, чтобы применились внесённые изменения.

Проблемы с приложением


После запуска сервера React вы можете столкнуться с тем, что приложение всё равно не запускается. Это происходит по двум причинам:

  1. Android-устройство и компьютер находятся в разных сетях.
  2. Введённый на устройстве IP-адрес не совпадает с IP-адресом компьютера.

Так что просто проверьте эти два пункта, и всё будет в порядке.

Отладка и живая перезагрузка


Когда приложение запустится, вы можете воспользоваться такими приятными вещами, как отладка в Chrome и живая перезагрузка. Для этого нужно открыть меню разработчика и поставить галочки в пунктах Debug in Chrome и Enable Live Reload. Теперь вы можете просматривать console.log и отслеживать ошибки из консоли Chrome. А живая перезагрузка бывает полезна, когда нужно перезапустить приложение для внесения изменений в исходные файлы.

Что дальше?


Куда двигаться дальше после того, как вы создали своё первое приложение с помощью React Native app?


В заключение


В этой статье мы рассмотрели процесс создания простого приложения с помощью React Native. Были разобраны основные задачи: создание компонента, работа со стилями, выполнение сетевых запросов и использование сторонних библиотек. Если вам не удаётся запустить своё приложение, то можете скачать с GitHub весь исходный код, рассмотренный в статье.
NIX
Компания

Комментарии 4

    +4
    Статья о создании приложения на ReactNative, а треть описывает создание бэкенда на PHP) Можно было использовать какое-нибудь открытое api для примера.
      0
      Как в ReactNative для Android сделано сохранения состояния в savedInstanceState Bundle, когда активити уничтожается при нехватке ресурсов?
        0
        Подскажите, как у react Native с производительностью?
          0
          Судя по сравнениям производительности с Ionic и т.п. приложениям — то ReactNative выигрывает очень сильно. Например, тут сравнивают https://medium.com/react-id/ionic-framework-hybrid-app-vs-react-native-4facdd93f690

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

        Самое читаемое