Статьи

Проще реагировать на развитие с помощью Expo

Expo — это набор инструментов, облегчающих кодирование приложений React Native. В этом уроке я покажу вам, как быстро создавать приложения React Native с помощью Expo.

С помощью Expo разработчики могут создавать приложения React Native без каких-либо проблем, связанных с установкой и настройкой программных зависимостей, таких как Android Studio, Xcode или других инструментов, необходимых для разработки и запуска приложения React Native.

В этом уроке я собираюсь показать вам, как создать простую игру памяти с помощью Expo. По пути вы также узнаете следующее:

  • Как использовать инструменты, предоставляемые Expo. Это включает в себя CLI, SDK и клиентское приложение Expo.
  • Как создать приложение React Native с помощью Expo.

Expo — это платформа для быстрой разработки приложений React Native. Это как Laravel или Symphony для разработчиков PHP или Ruby on Rails для разработчиков Ruby. Expo предоставляет слой поверх React Native API, чтобы упростить их использование и управление. Он также предоставляет инструменты, облегчающие загрузку и тестирование приложений React Native. Наконец, он предоставляет компоненты и службы пользовательского интерфейса, которые обычно доступны только при установке стороннего компонента React Native. Все они доступны через Expo SDK.

Прежде чем продолжить, важно знать о некоторых ограничениях Expo:

  1. Приложения Expo не поддерживают фоновое выполнение кода. Это означает, что вы не можете, например, запустить код, который прослушивает изменения местоположения, когда приложение закрыто.
  2. Приложения Expos ограничены собственными API, которые поддерживает Expo SDK. Это означает, что если ваше приложение имеет очень специфический сценарий использования, например, связь с периферийным устройством Bluetooth, единственная возможность реализовать такие функции — использовать простой React Native или написать собственный код с использованием библиотеки с именем ExpoKit.
  3. Экспо запирает вас в их набор инструментов . Это означает, что вы не можете просто установить и использовать большинство замечательных инструментов, доступных для разработки React Native, таких как инструменты командной строки, скаффолдеры и инфраструктуры пользовательского интерфейса. Но хорошо то, что Expo SDK совместим с простыми приложениями React Native, поэтому у вас не возникнет никаких проблем при извлечении приложения из Expo.
  4. Автономные двоичные файлы приложений Expo можно создавать только онлайн. Expo предоставляет инструмент командной строки под названием Exp . Это позволяет разработчикам инициировать процесс сборки на серверах Expo. После этого будет предоставлен URL-адрес для загрузки файла .apk или .ipa .

Даже с учетом этих ограничений важно помнить, что Expo — это полнофункциональная структура с большой поддержкой часто используемых API-интерфейсов Android или iOS. Это означает, что он обеспечил вас большинством функций, которые обычно нужны приложениям. Поэтому часто нет необходимости смотреть за пределы Expo, чтобы реализовать нативную функциональность.

Приложение, которое мы собираемся создать, — игра на память. Возможно, вы знакомы с этим типом игры — пользователь должен найти подходящие пары, переворачивая карты по две за раз. Вот как выглядит экран по умолчанию:

Memory game default app screen

И вот как это выглядит, когда все пары были открыты:

Game completed

После того, как они решили игру, пользователь может нажать на кнопку сброса , чтобы сбросить элементы в их первоначальное состояние. Это позволяет им начать игру заново.

В отличие от простого React Native, где вам необходимо установить и настроить Android Studio или Xcode и другие зависимости, в Expo есть только несколько шагов, чтобы начать разработку приложений:

  1. Загрузите и установите Node.js. Expo зависит от платформы Node.js за инструменты командной строки и управление зависимостями.
  2. Установите Expo Client на ваше устройство iOS или Android . Это используется для предварительного просмотра приложения во время его разработки.
  3. Установите инструмент командной строки. Это позволяет генерировать новый проект Expo, инициировать процесс сборки и многое другое. Для установки выполните следующую команду:
1
npm install exp —global

После того, как вы установили все зависимости, вы можете создать новое приложение Expo:

1
exp init MemoryGame

Как только это будет сделано, он создаст новую папку с именем MemoryGame . Перейдите внутрь и запустите сервер разработки:

1
2
cd MemoryGame
exp start

Кроме того, вы также можете использовать Expo XDE. Это позволяет создавать и запускать приложения Expo через графический интерфейс. Вы можете скачать установщик из репо Expo GitHub . В настоящее время он поддерживает только Windows и Mac. Так что, если вы работаете в Ubuntu или Linux, лучше придерживаться командной строки.

После запуска сервера разработки вы должны увидеть что-то вроде этого:

Running the dev server

Это QR-код, который указывает на предварительный просмотр проекта. Откройте клиентское приложение Expo на своем телефоне и отсканируйте код с помощью QR-сканера. На этом этапе вы теперь сможете просматривать экран по умолчанию. Каждый раз, когда вы нажимаете Control-S в любом из файлов проекта, предварительный просмотр должен автоматически перезагружаться, чтобы отразить изменения.

Вы можете найти полный исходный код проекта в репозитории GitHub . Или, если вы хотите попробовать приложение, вы можете проверить демо . Просто выберите QR-код и отсканируйте его на своем телефоне с помощью клиентского приложения Expo.

Теперь мы готовы закодировать приложение. Давайте начнем с некоторых компонентов пользовательского интерфейса, прежде чем мы вернемся к реализации основного компонента.

Заголовок используется для отображения заголовка приложения. Создайте папку компонентов . Внутри него создайте файл Header.js и добавьте следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from ‘react’;
import { StyleSheet, Text, View } from ‘react-native’;
 
export default class Header extends React.Component {
   
  render() {
    return (
      <View style={styles.header}>
        <Text style={styles.header_text}>MemoryGame</Text>
      </View>
    );
  }
 
}
 
const styles = StyleSheet.create({
  header: {
    flex: 1,
    flexDirection: ‘column’,
    alignSelf: ‘stretch’,
    paddingTop: 20,
    paddingBottom: 5,
    backgroundColor: ‘#f3f3f3’
  },
  header_text: {
    fontWeight: ‘bold’,
    fontSize: 17,
    textAlign: ‘center’
  }
});

Это просто базовый компонент React Native с некоторыми стилями, соответствующими интерфейсу нашего приложения.

Далее идет компонент для отображения оценки ( компоненты / Score.js ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from ‘react’;
import { StyleSheet, Text, View } from ‘react-native’;
 
export default class Score extends React.Component {
   
  render() {
    return (
      <View style={styles.score_container}>
        <Text style={styles.score}>{this.props.score}</Text>
      </View>
    );
  }
 
}
 
const styles = StyleSheet.create({
  score_container: {
    flex: 1,
    alignItems: ‘center’,
    padding: 10
  },
  score: {
    fontSize: 40,
    fontWeight: ‘bold’
  }
});

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

Компонент карты ( компоненты / Card.js ) будет отображать карты. Эти карты используют значки из набора векторных иконок Экспо . Это одна из функций, которая появляется сразу после использования Expo: она включает в себя значки из наборов значков , таких как FontAwesome , Entypo и Ionicons .

В приведенном ниже коде вы можете видеть, что мы используем только FontAwesome. У него есть значок, который нам нужен для отображения состояния карты по умолчанию: знак вопроса. Как вы увидите позже в основном компоненте приложения, мы также будем использовать иконки от Entypo и Ionicons. Ссылка на эти источники значков будет передана этому компоненту, поэтому нет необходимости указывать их здесь:

1
2
3
import React from ‘react’;
import { StyleSheet, Text, View, TouchableHighlight } from ‘react-native’;
import { FontAwesome } from ‘@expo/vector-icons’;

Внутри метода render() мы используем источник и значок, переданные в качестве реквизита, только если карта открыта. По умолчанию будет отображаться только значок вопросительного знака из FontAwesome. Но если карта открыта, она будет использовать значок источника, значок и цвет, которые были переданы в качестве реквизита.

Каждая из карт может быть нажата. При clickCard() функция clickCard() , которая также передается через реквизит. Позже вы увидите, что делает функция, но сейчас просто знайте, что она обновляет состояние, чтобы показать значок на карте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default class Card extends React.Component {
 
  render() {
     
    let CardSource = FontAwesome;
    let icon_name = ‘question-circle’;
    let icon_color = ‘#393939’;
     
    if(this.props.is_open){
      CardSource = this.props.src;
      icon_name = this.props.name;
      icon_color = this.props.color;
    }
     
    return (
      <View style={styles.card}>
        <TouchableHighlight onPress={this.props.clickCard} activeOpacity={0.75} underlayColor={«#f1f1f1»}>
          <CardSource
            name={icon_name}
            size={50}
            color={icon_color}
          />
        </TouchableHighlight>
      </View>
    );
  }
}

Не забудьте добавить стили:

01
02
03
04
05
06
07
08
09
10
const styles = StyleSheet.create({
  card: {
    flex: 1,
    alignItems: ‘center’
  },
  card_text: {
    fontSize: 50,
    fontWeight: ‘bold’
  }
});

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

01
02
03
04
05
06
07
08
09
10
11
Array.prototype.shuffle = function() {
  var i = this.length, j, temp;
  if(i == 0) return this;
  while(—i){
   j = Math.floor(Math.random() * (i + 1));
   temp = this[i];
   this[i] = this[j];
   this[j] = temp;
  }
  return this;
}

Основной компонент ( App.js ) содержит основную логику приложения и объединяет все. Начните с включения пакетов React и Expo, которые мы будем использовать. На этот раз мы используем все источники иконок из векторных иконок Экспо:

1
2
3
import React from ‘react’;
import { StyleSheet, View, Button } from ‘react-native’;
import { Ionicons, FontAwesome, Entypo } from ‘@expo/vector-icons’;

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

1
2
3
4
5
import Header from ‘./components/Header’;
import Score from ‘./components/Score’;
import Card from ‘./components/Card’;
 
import helpers from ‘./helpers’;

Внутри конструктора мы сначала создаем массив, который представляет уникальные карты. src — источник значков, name — имя значка (вы можете найти имена на GitHub, если вы хотите использовать другие значки), а color — это, естественно, цвет значка:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
export default class App extends React.Component {
 
  constructor(props) {
    super(props);
    // bind the functions to the class
    this.renderCards = this.renderCards.bind(this);
    this.resetCards = this.resetCards.bind(this);
     
    // icon sources
    let sources = {
      ‘fontawesome’: FontAwesome,
      ‘entypo’: Entypo,
      ‘ionicons’: Ionicons
    };
 
    // the unique icons to be used
    let cards = [
      {
        src: ‘fontawesome’,
        name: ‘heart’,
        color: ‘red’
      },
      {
        src: ‘entypo’,
        name: ‘feather’,
        color: ‘#7d4b12’
      },
      {
        src: ‘entypo’,
        name: ‘flashlight’,
        color: ‘#f7911f’
      },
      {
        src: ‘entypo’,
        name: ‘flower’,
        color: ‘#37b24d’
      },
      {
        src: ‘entypo’,
        name: ‘moon’,
        color: ‘#ffd43b’
      },
      {
        src: ‘entypo’,
        name: ‘youtube’,
        color: ‘#FF0000’
      },
      {
        src: ‘entypo’,
        name: ‘shop’,
        color: ‘#5f5f5f’
      },
      {
        src: ‘fontawesome’,
        name: ‘github’,
        color: ‘#24292e’
      },
      {
        src: ‘fontawesome’,
        name: ‘skype’,
        color: ‘#1686D9’
      },
      {
        src: ‘fontawesome’,
        name: ‘send’,
        color: ‘#1c7cd6’
      },
      {
        src: ‘ionicons’,
        name: ‘ios-magnet’,
        color: ‘#d61c1c’
      },
      {
        src: ‘ionicons’,
        name: ‘logo-facebook’,
        color: ‘#3C5B9B’
      }
    ];
 
    // next: add code creating the clone and setting the cards in the state
  }
 
}

Обратите внимание, что вместо непосредственного указания src как FontAwesome , Entypo или Ionicons для каждого из объектов, мы используем имена свойств, используемые в объекте sources . Это потому, что нам нужно будет создать копию массива карт, чтобы каждая карта имела пару. Создание копии с использованием методов массива, таких как slice() , создаст копию массива, но проблема заключается в том, что как только отдельные объекты модифицируются либо в копии, либо в оригинале, оба массива также изменяются.

Это подводит нас к решению, приведенному ниже, которое заключается в создании совершенно нового объекта путем преобразования массива cards в строку и последующего его анализа для преобразования обратно в массив. По этой причине мы используем строки, поскольку функции не могут быть преобразованы в строки. Затем мы объединяем два, чтобы получить массив, который содержит все карты, которые нам нужны:

1
2
3
let clone = JSON.parse(JSON.stringify(cards));
 
this.cards = cards.concat(clone);

Затем просмотрите этот массив и сгенерируйте уникальный идентификатор для каждого, установите источник значков, а затем установите его в закрытое состояние по умолчанию:

1
2
3
4
5
6
7
// add the ID, source and set default state for each card
this.cards.map((obj) => {
  let id = Math.random().toString(36).substring(7);
  obj.id = id;
  obj.src = sources[obj.src];
  obj.is_open = false;
});

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

1
2
3
4
5
6
7
8
9
this.cards = this.cards.shuffle();
 
// set the default state
this.state = {
  current_selection: [], // this array will contain an array of card objects which are currently selected by the user.
  selected_pairs: [], // the names of the icons.
  score: 0, // default user score
  cards: this.cards // the shuffled cards
}

Метод render() визуализирует заголовок, карточки, счет и кнопку для сброса текущей игры. Он использует renderRows() для визуализации отдельных строк карты. На экране будет шесть строк по четыре карты в каждой:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
render() {
  return (
    <View style={styles.container}>
      <Header />
      <View style={styles.body}>
        {
          this.renderRows.call(this)
        }
      </View>
      <Score score={this.state.score} />
      <Button
        onPress={this.resetCards}
        title=»Reset»
        color=»#008CFA»
      />
    </View>
  );
}

Вот код для функции renderRows() . При этом используется getRowContents() , которая отвечает за создание массива массивов по четыре элемента в каждом. Это позволяет нам рендерить каждую строку, а затем использовать другую функцию для рендеринга карт для каждой итерации функции map() :

01
02
03
04
05
06
07
08
09
10
11
12
renderRows() {
  
  let contents = this.getRowContents(this.state.cards);
  return contents.map((cards, index) => {
    return (
      <View key={index} style={styles.row}>
        { this.renderCards(cards) }
      </View>
    );
  });
  
}

Вот getRowContents() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
getRowContents(cards) {
  let contents_r = [];
  let contents = [];
  let count = 0;
  cards.forEach((item) => {
    count += 1;
    contents.push(item);
    if(count == 4){
      contents_r.push(contents)
      count = 0;
      contents = [];
    }
  });
 
  return contents_r;
}

Далее идет renderCards() . Это принимает массив объектов карты и визуализирует их через компонент Card . Все, что нам нужно сделать здесь, это передать отдельные свойства каждого объекта карты в качестве реквизита. Затем он используется для визуализации правильного значка, как вы видели в коде для компонента Card . Функция clickCard() также передается как реквизит. Идентификатор карты передается этой функции, чтобы можно было идентифицировать и обновить уникальную карту:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
renderCards(cards) {
  return cards.map((card, index) => {
    return (
      <Card
        key={index}
        src={card.src}
        name={card.name}
        color={card.color}
        is_open={card.is_open}
        clickCard={this.clickCard.bind(this, card.id)}
      />
    );
  });
}

Внутри функции clickCard() мы получаем детали выбранной карты и проверяем, будет ли она обрабатываться дальше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
clickCard(id) {
  let selected_pairs = this.state.selected_pairs;
  let current_selection = this.state.current_selection;
  let score = this.state.score;
   
  // get the index of the currently selected card
  let index = this.state.cards.findIndex((card) => {
    return card.id == id;
  });
 
  let cards = this.state.cards;
   
  // the card shouldn’t already be opened and is not on the array of cards whose pairs are already selected
  if(cards[index].is_open == false && selected_pairs.indexOf(cards[index].name) === -1){
 
    // next: add code for processing the selected card
 
  }
 
}

Теперь давайте введем код для обработки выбранной карты.

Сначала мы открываем карту и добавляем ее в массив выбранных в данный момент карт:

1
2
3
4
5
6
7
8
cards[index].is_open = true;
     
current_selection.push({
  index: index,
  name: cards[index].name
});
 
// next: add code for determining whether the user has selected the correct pair or not

Когда в массиве выбранных в данный момент карт есть два элемента, мы проверяем, совпадают ли имена иконок. Если они есть, то это означает, что пользователь выбрал правильную пару. Если они не совпадают, то это неверная пара. В этом случае мы закрываем первую выбранную карту, а затем добавляем немного задержки перед закрытием второй карты. (Таким образом, пользователь может увидеть значок карты, прежде чем она вернется в закрытое состояние.)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
if(current_selection.length == 2){
  if(current_selection[0].name == current_selection[1].name){
    score += 1;
    selected_pairs.push(cards[index].name);
  }else{
    cards[current_selection[0].index].is_open = false;
     
    // delay closing the currently selected card by half a second.
    setTimeout(() => {
      cards[index].is_open = false;
      this.setState({
        cards: cards
      });
    }, 500);
  }
 
  current_selection = [];
}
 
// next: add code for updating the state

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

1
2
3
4
5
this.setState({
  score: score,
  cards: cards,
  current_selection: current_selection
});

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
resetCards() {
  // close all cards
  let cards = this.cards.map((obj) => {
    obj.is_open = false;
    return obj;
  });
 
  cards = cards.shuffle();
   
  // update to default state
  this.setState({
    current_selection: [],
    selected_pairs: [],
    cards: cards,
    score: 0
  });
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignSelf: ‘stretch’,
    backgroundColor: ‘#fff’
  },
  row: {
    flex: 1,
    flexDirection: ‘row’
  },
  body: {
    flex: 18,
    justifyContent: ‘space-between’,
    padding: 10,
    marginTop: 20
  }
});

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

Это оно! Из этого руководства вы узнали, как использовать Expo XDE для быстрого подключения приложения React Native. Expo — это действительно хороший способ начать разработку приложений React Native, поскольку он устраняет необходимость в установке большого количества программного обеспечения, что часто является причиной разочарований, особенно для начинающих. Он также предоставляет инструменты, которые действительно упрощают предварительный просмотр приложения во время его разработки. Обязательно ознакомьтесь с ресурсами, указанными на сайте Expo, если вы хотите узнать больше.

А пока взгляните на некоторые другие наши посты о разработке приложений React Native!

  • React Native
    Практические примеры анимации в React Native
  • GraphQL
    Кодирование приложения с помощью GraphQL, React Native и AWS AppSync: серверная часть
    Надер Дабит
  • Мобильная разработка
    Инструменты для React Native Development
  • React Native
    Начало работы с шаблоном приложения React Native