React Native для самых маленьких. Опыт мобильной разработки
Однажды, в одной продуктовой команде захотели разработать мобильное приложение, чтобы проверить гипотезу востребованности продукта и его удобства для пользователей. И то, что в команде не было мобильных разработчиков, не помешало. Два фронтенд-разработчика взяли React Native и за три месяца написали приложение. Тестирование гипотезы прошло успешно, приложение продолжает развивается и вызывает интерес других команд в компании.
Изображение из описания инструмента: www.semrush.com/news/position-tracking-on-the-go.
Это краткое описание истории команды Артёма Лашевского, который на три месяца из фронтенд-разработчика стал мобильным. Подробнее, как это произошло, читайте в расшифровке доклада Артёма на FrontendConf 2019: что такое React Native, почему именно он, пошаговая инструкция создания приложения и выбор нужных библиотек и компонентов.
Артем Лашевский — ведущий фронтенд-разработчик в SEMrush и специалист в области информационной безопасности автоматизированных систем.
Зачем нам приложение
SEMrush — международная IT-компания, которая разрабатывает крупную онлайн-платформу для маркетологов. Входит в ТОП-3 платформ для маркетинга и SEO. У компании 5 млн пользователей, 7 офисов на двух континентах, 800 человек в штате и 30 команд разработки.
Каждая команда разработки занимается своим инструментом в рамках платформы. Я работаю в одной из таких команд из 8 человек: два на фронтенде, три на бэкенде, QA, DevOps-инженер и PO. Инструмент, над которым работает наша команда, позволяет пользователям проверять позиции страниц их сайта по определенным запросам в Google. Инструмент помогает проверять позиции по ключевым словам не только своего сайта, но и конкурентов и проводить анализ и сравнение.
Платформа работает в браузере. Запуск мобильного приложения позволит получить конкурентное преимущество, потому что приложение помогает дольше удерживать старых пользователей и повышает конверсию из бесплатных в платных.
Для пользователей приложение повышает удобство работы с платформой. Через телефон клиент может воспользоваться SEMrush там, где раньше был нужен компьютер. Зайти в приложение, увидеть, упал ли его сайт в выдаче Google или поднялся — ключевые метрики всегда под рукой. Если они просели, то уже в веб-версии можно посмотреть детали и понять, как исправить ситуацию.
Наша команда видела потенциал мобильного приложения и решила попробовать его разработать, но мобильных разработчиков среди нас не было. Мы стали сами изучать инструменты и технологии для разработки приложений.
Нативные приложения — первое, на что обратили внимание. Изучив, поняли, что для нас это долго, дорого и неудобно: нужно изучить два стека технологий, дублировать функциональность и тестировать её два раза.
От нативной разработки перешли к изучению WebView (Apache Cordova). У коллеги по фронтенду был опыт разработки на Cordova, поэтому в первом спринте изучения технологий и решений один из прототипов разработали на Cordova.
Но мы хотели что-то современнее — «модно-молодежно», поэтому перешли к кроссплатформенным решениям.
Кроссплатформенная разработка. Рассматривали несколько вариантов: Flutter, Xamarin, Native Script и React Native. Для нас каждый из вариантов имел свой минус, поскольку мы хотели как можно быстрее стартануть:
- Flutter требует знания Dart (альтернатива JavaScript). Времени на изучение смежных технологий не было — хотелось быстро «взлететь», чтобы проверить гипотезу.
- Xamarin требует знания C#. У нас есть бэкенд-разработчики, но под iOS и Android требуются разные интерфейсы. Всё равно придется дублировать код и реализации.
- Native Script рассчитан на связку с Angular. В нашем проекте используется ReactJS и Redux, поэтому нет.
- React Native показался классным, его и выбрали. К тому же, мы просто хотели его попробовать, несмотря на минусы и плюсы других решений.
Во время отбора мы не столкнулись с Ionic. Возможно, его бы тоже рассмотрели, но всё равно бы выбрали React Native.
React Native
Это фреймворк от Facebook для разработки кроссплатформенных нативных приложений. Построен на базе ReactJS, под капотом не использует WebView, поэтому нет DOM API. В React Native нет HTML и CSS, но есть некоторые компоненты платформы в JSX и CSS-like полифилы.
React Native содержит некоторый JavaScript API над нативными компонентами. Это значит, что нативные компоненты имеют некоторый «биндинг» в JavaScript-компоненты на ReactJS. Взаимосвязь между нативным и JavaScript-бандлом осуществляется через bridge с помощью JavaScript API. Поверхностно, это вся архитектура.
Плюсы React Native:
- Кроссплатформенность. Мы хотели писать приложение изначально под iOS, но дополнительный запуск под Android — огромный плюс.
- Быстро и дешево. Как следствие первого пункта, не нужно поддерживать две платформы. Добавляем новую функциональность на React Native под одну платформу, а 80% кода работает и на другой.
- Большое сообщество. В Telegram есть сообщество React Native на 3000 разработчиков. Можно не только получить ответ на любой вопрос, но и посмотреть, кто на какие грабли наступал, и от чего стоит отказаться.
- Богатый выбор доступных компонентов и готовых решений. Главное условие задачи — быстрый запуск, поэтому не было времени писать библиотеки и решения с нуля. Готовые компоненты и решения нам в этом помогли, как и в переходе от ReactJS к React Native.
Два способа написать приложение
С React Native есть два способа разработать приложение: использовать Expo или React Native CLI.
Expo. Это слой абстракций — набор инструментов, библиотек и сервисов для быстрого запуска. Это некоторый API, который «из коробки» даёт доступ к возможностям устройства: к камере, геолокации, push-сообщениям.
В Expo есть инструменты отладки и тестирования приложения. Алгоритм простой: устанавливаем на телефон приложение Expo Client, подключаем телефон с компьютером в одну локальную сеть, заходим в Expo на телефоне, открываем разрабатываемое приложение. Все изменения в коде отображаются моментально. Это помогает тестировать и расшаривать на команду.
Взлет на Expo быстрый и стремительный. С Expo не нужны Xcode и Android Studio. Пишем в нашей среде разработки, собираем приложение с помощью сервиса Expo, подписываем и обновляем на лету.
Но есть и минусы.
- Нельзя добавлять кастомные нативные модули.
- Expo — слой абстракций над React Native. Нельзя обновиться на новую версию фреймворка, пока не обновится Expo.
React Native CLI. Его преимущество в возможности кастомизации, добавления нативных модулей.
Но для подписи и сборки приложения (и публикации) нужен Xcode или Android Studio. Чтобы протестировать приложение, необходимо сделать сборку и загрузить на телефон, например, с помощью TestFlight для iOS или прямой установки с ПК на телефон. Стандартная схема, но неудобно и долго.
В целом, выбор простой: хотим разработать прототип — выбираем Expo, хотим приложение с перспективой — React Native CLI.
Мы выбрали React Native CLI. В SEMrush мы наверняка захотим писать свои кастомные нативные части, поэтому решающим фактором стал пункт с нативными модулями и сторонними библиотеками с модулями. С React Native CLI старт медленнее из-за большой подготовки, но в дальнейшем возможностей больше.
Многие коллеги, используя React Native, сталкивались с необходимостью кастомизации, и им приходилось специальными командами делать Eject с Expo на React Native CLI. Структура каталогов файлов простого приложения перенесётся с обычного набора JavaScript-файлов по папкам под Android, iOS и JavaScript. Но со сложным приложением возникнут трудности.
Пишем приложение
В React Native нет привычных CSS, DOM и веб-элементов: div
, span
, li
, button
. Но в React Native есть два типа компонентов платформы.
- Кроссплатформенные:
View
,Text
,Image
и другие. - Специфичные для каждой платформы, например,
DatePickerIOS
илиDatePickerAndroid
.
Переход от React к React Native простой: блоки div
это View
, строковые span
это Text
, а списки li
или ul
— FlatList
. Для каждого элемента есть соответствие.
Примечание. Для переноса готовых веб-компонентов в «не веб» можно написать транспайлер, но обычно проще переписать заново.
Начать писать на React Native можно довольно быстро.
import React, {Component} from 'react';
import {Text, View, StyleSheet} from 'react-native';
export default class App extends Component {
render() {
return (
Hello, SEMrush!
);
}
}
Есть некоторый класс App
(контейнер приложения). В нём находится метод render
, в котором отрисовывается визуальная часть. Он содержит контейнер View
, внутри которого идет Image
и Text
.
Каждый из элементов, например, View
содержит атрибут style
, значение которого задается через объект styles
.
Объект styles
создаётся с помощью модуля StyleSheet
метода create
React Native. Данный объект — это CSS-объектная нотация, упрощенная модель CSS, которой хватает. Для разметки интерфейса используется Flexbox, тоже упрощённый, но его достаточно для выравнивания по горизонтали и вертикали.
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
image: {
marginBottom: 20,
},
});
Готово — мы написали приложение под iOS. Забегая вперед, уточню, что оно также работает и под Android.
Первое приложение «Hello, World!».
Кроссплатформенность
Мы хотели, чтобы наше приложение работало и под Android, и под iOS. При этом мы хотели иметь возможность делать различные реализации в зависимости от платформы. В React Native для этого есть два способа.
Первый способ — использовать модуль Platform. Например, создаём приложение и задаем высоту в зависимости от платформы. Под iOS высота 200, под Android — 100. С помощью метода Platform.OS
определяем, какую платформу использует пользователь.
import {Platform, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
height: Platform.OS === 'ios' & 200 : 100,
});
Если же у нас 40 подобных атрибутов, нет необходимости дублировать код. Для этого есть метод Platform.select
из этого же модуля. Он по ключу платформы возвращает набор свойств: под iOS возвращает backgroundColor = 'red'
, под Android — backgroundColor = 'blue'
. В итоге, мы можем под iOS возвращать свой набор атрибутов, (их может быть хоть 20, хоть 40), под Android — совершенно другой набор.
import {Platform, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'blue',
},
}),
},
});
Метод Platform.select
позволяет писать кроссплатформменные компоненты.
const Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid'),
})();
;
Некоторый компонент использует метод Platform.select
, который в зависимости от ключа платформы возвращает свою реализацию. Все отлично, берём и пишем кроссплатформенную библиотеку.
Если есть какие-то кастомные решения или ограничения на операционную систему, можем определять версию ОС.
import {Platform} from 'react-native';
if (Platform.Version ===25) {
console.log('Running on Nougat!');
}
import {Platform} from 'react-native';
const majorVersionIOS = parceInt(platform.Version, 10);
if (majorVersionIOS <= 9) {
console.log('Work around a change in behavior');
}
Второй способ — использование platform-specific file extension (*.android.js
и *.ios.js
). Это позволяет разрабатывать кроссплатформенные реализации для более громоздкого кода и сложной логики. Тогда разделяем весь код на два файла, например BigButton.ios.js
и BigButton.android.js
. В том коде, где хотим это использовать, используем интерфейс и импортируем BigButton
. React Native сам позаботится о нужном импорте в зависимости от платформы.
Компоненты
Мы можем реализовывать кроссплатформенные решения, но важна скорость. Поэтому мы двинулись в сторону готовых решений — библиотек компонентов. Исследовали, выделили много больших решений (UI kit) и выбрали NativeBase. Это огромная библиотека с десятком компонентов, 12 000 звезд на GitHub, которая позволяет решать большинство задач.
Пример UI для iOS слева, для Android — справа.
В нашем проекте мы частично используем эту библиотеку, кастомизируем элементы и задаем свои стили. Например, у нас есть компоненты Button
или Header
, которые мы импортируем из библиотеки NativeBase.
Пишем код один раз, при этом на каждой платформе он выглядит по-своему.
import React, { Component } from 'react';
import { Container, Header, Content, Button, Text } from 'native-base';
export default class ButtonThemeExample extends Component {
render() {
return (
);
}
}
Навигация
Для навигации мы также сравнили несколько возможных решений.
- react-navigation рекомендует Facebook в документации по React Native.
- wix/react-native-navigation — решение от Wix. Полностью нативное, но у нас, как у фронтенд-разработчиков, возникло опасение — хотим ли мы редактировать нативные файлы сразу под обе платформы.
- react-router — обычный react-router из ReactJS с некоторой оберткой. Все переходы между экранами пришлось бы реализовывать самостоятельно, а это отдаляло бы нас от цели.
- airbnb/native-navigation — бета-версия библиотеки от Airbnb. Отличная документация, но бета.
Мы выбрали первое решение — react-navigation. Оно полностью на JavaScript, поэтому нам не нужно заботиться о нативных файлах. Но с другой стороны — производительность под вопросом.
Мы обсудили с коллегами из сообщества React Native проблему, изучили статьи и поняли, что производительность react-navigation и wix/react-native-navigation примерно одинакова. Поэтому выбрали первое решение, а опыт использования только подтвердил правильность выбора.
Плюсы react-navigation:
- Гибкое кроссплатформенное решение.
- Нативный UI под каждую платформу и нативные переходы между экранами. Мы не хотели создавать приложение, которое на Android выглядит, как на iOS.
- Предоставляет все базовые типы переходов: нижние табы, свайпы между экранами, модальные окна,
SwitchNavigator
.
Пример структуры приложения с использованием react-navigation. Код удобнее читать снизу вверх.
const HomeStack = createStackNavigator({
Home: { screen: HomeScreen },
Details: {screen: DetailsScreen },
});
const SettingsStack = createStackNavigator({
Settings: { screen: SettingsScreen },
Details: { screen: DetailsScreen },
});
const TabNavigator = createBottomTabNavigator({
Home: { screen: HomeStack },
Settings: { screen: SettingsStack },
});
export default createAppContainer(TabNavigator);
В некотором createAppContainer
содержится createBottomTabNavigator
, состоящий из двух стеков. В каждом стеке — свой набор экранов.
У нас есть две кнопки — Home
и Settings
. Это два стека — HomeStack
и SettingsStack
. Внутри каждого стека по два экрана: HomeScreen
и DetailsScreen
, SettingsScreen
и DetailsScreen
соответственно. В обоих стеках используется общий компонент DetailsScreen
. Пишем компонент (экран) и переиспользуем его на разных стеках.
Код для трех экранов выглядят максимально просто.
class HomeScreen extends React.Component {
static navigationOptions = { title: 'Home' };
render() {
return (
Home!
);
}
}
class SettingsScreen extends React.Component {
static navigationOptions = { title: 'Settings' };
render() {
return (
Settings!
);
}
}
class DetailsScreen extends React.Component {
static navigationOptions = { title: 'Details' };
render() {
return (
Details!
);
}
}
Это тот же «Hello, World!», с которого мы начали, — ничего больше.
Добавляем Redux. Обворачиваем корневой навигатор в Provider
, прокидываем store
. Далее пишем reducer
, actions
, actionTypes
и остальное — всё знакомо.
const Navigation = createAppContainer(TabNavigator);
//Render the app container component with the provider around it
export default class App extends React.Component {
render() {
return (
);
}
}
Нужно больше компонентов
Выбирая итоговый UI kit, рассматривали многие возможные решения: React Native Elements, UI Kitten, React Native Material UI, React Native Material Kit, React Native UI Library, React Native Paper, Shoutem UI Toolkit, Nachos UI, Teaset. В итоге используем некоторые компоненты этих библиотек.
В React Native есть также платформно-ориентированные компоненты. Яркий пример — DatePickerAndroid
и DatePickerIOS
. Они отличаются визуально и по интерфейсу, поэтому приходится дублировать код для решения одной задачи под обе платформы.
Слева DatePickerAndroid, справа DatePickerIOS.
Например, в нашем приложении можно запросить отчет или график за определенный отрезок времени. Пользователь указывает две даты, чтобы выбрать период. Но на iOS нативный выбор даты неудобен, особенно по-сравнению с веб-версией нашего инструмента.
Нам бы пришлось делать два поля: для начальной и конечной даты. Выбирать даты в одном поле как на Android удобнее, но мы не сможем настроить всё также — кастомизация минимальна.
Мы нашли решение — wix/react-native-calendars.
Справа пример календаря из нашего приложения с цветами компании.
В нём есть горизонтальный и вертикальный календари, детализация по датам, возможность отображения стиля компании, выбор диапазона дат, блокировка выбора определённых дат и поддержка Android и iOS.Удобно, что в решении один интерфейс, — пишем код один раз, без дублирования и переписывания.
Хранилище
В какой-то момент нам понадобилось хранить данные, чтобы показывать их в офлайне или при возобновлении работы. Для этого использовали библиотеку Async Storage (key-value хранилище). Она работает с ключами и мультиключами (запись, чтение и удаление), а для взаимодействия с нативной частью предоставляет JavaScript API.
Данные под iOS хранятся в сериализованном виде. Под Android — например, в SQLite. Как разработчикам нам не нужно об этом заботиться: есть API, храним данные, всё хорошо.
Анимации
React Native не использует WebView-технологии, основанные на HTML, поэтому у нас нет анимации. Здесь нам помогает компонент Animated. Поддерживает некоторые обертки базовых элементов: View
, Image
, Text
, ScrollView
. Он идёт в отдельном потоке и не тормозит приложение, но есть минус — «нечеловеческий» вид кода.
Ниже — пример экрана нашего приложения со списком проектов пользователей.
Сверху Header
с названием «Projects», в котором есть SearchBar
для фильтрации проектов. Выглядит так, будто SearchBar
находится под Header
, но в действительности он в общем потоке блоков, которые идут ниже со списком проектов. Благодаря компоненту Animated, реализована такая анимация, что SearchBar
«исчезает».
Сокращённая версия реализации.
export default class ProjectsScreen extends Component {
state = {
scrollY: new Animated.Value(
Platform.OS === 'ios' ? -HEADER_MAX_HEIGHT : 0,
),
};
render() {
const scrollY = Animated.add(
this.state.scrollY,
Platform.OS === 'ios' & HEADER_MAX_HEIGHT : 0,
);
const headerTranslate = scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
outputRange: [0, -HEADER_SCROLL_DISTANCE],
extrapolate: 'clamp',
});
return (
{this._renderScrollViewContent()}
);
}
}
Выглядит легко, но приходится «костылить»: сдвигать весь контент выше или ниже на iOS. На Android это не нужно. Ещё здесь интерполяция и разные методы из модуля Animated.
Проблема «мерцания» экрана при первом запуске
Мы написали приложение и решили добавить иконку приложения и Splash Screen (или же Launch Screen) — экран приложения, который появляется при первом запуске, если оно выгружено из памяти. У нас экран в оранжевом цвете и с логотипом SEMrush по центру.
В архитектуре React Native нативная часть взаимодействует с некоторым JavaScript бандлом по bridge. Мы столкнулись с проблемой, что в момент запуска нативная часть работает, а скриптовый бандл ещё не подгрузился. Между оранжевым Splash Screen и появлением списка проектов на экране сияла белая пустота (доли миллисекунд).
Проблему «мерцания» решили с помощью библиотеки react-native-splash-screen. Она предоставляет JavaScript API для программного отображения или скрытия экрана запуска приложения. Через этот API мы убрали нативный Splash Screen в компоненте со списком проектов в методе componentDidMount()
. Бонусом поставили спиннер между двумя экранами, чтобы выглядело симпатичнее.
Потеря связи
Мы хотели разработать дружелюбный и красивый интерфейс, позаботиться об UX и UI. Но когда пользователь ехал в метро, терял связь и обновлял список проектов, приложение показывало только пустой экран. Лучше, чтобы пользователь видел информацию, которая уже была подгружена, и сообщение о проблеме с интернет-соединением.
С работой в офлайне помогла библиотека Netinfo. Сейчас она вынесена в отдельный пакет, поскольку Facebook разгружает React Native, чтобы сделать его легковеснее.
Netinfo позволяет:
- подписываться на события и при изменении интернет-соединения выводить пользователю сообщения;
- проверять качество соединения;
- проверять тип соединения (LTE, WiFi, 3G) для сложных приложений;
- использовать запрос на проверку подключения к интернету.
Отладка приложения
Чтобы не получать ошибки после добавления новых зависимостей, особенно тех, что используют нативный код, нужен дебаггер. В React Native он есть — показан на изображении симулятора iPhone.
Для iOS использовали эмулятор из Xcode, в котором можно выбирать версию ОС и устройства. Симулятор под Android — Android Studio.
В диалоговом меню эмулятора есть режимы Debug JS Remotely и Live Reload. Позволяют видеть свойства компонентов, например, CSS-like стили и просматривать вложенность.
В нативной разработке нужно написать код, отправить в сборку, запустить — это долго. В дебаггере же всё как в ReactJS в вебе: включаем Live Reloading и видим пересборку в реальном времени в симуляторе без перезагрузки.
Кроме того, мы используем еще и React Developer Tools, чтобы инспектировать компоненты, смотреть иерархию и изменять их свойства и стили.
Но и этого мало, есть лучшее решение — React Native Debugger.
В нём есть React Inspector, Redux DevTools, взаимодействие через интернет, console-логирование, профилирования по памяти и приложению, просмотр всех actions. В отличии от первых двух двух решений, можно менять стиль в самом React Native Debugger.
Подключение нативных модулей
До текущего момента всё было легко и прозрачно: берём какое-то решение, подключаем, оно работает. Но есть нюанс — следствие архитектуры React Native. Кроме JavaScript части, все библиотеки используют и нативную, которую нужно связать с React Native. Для этого есть команда react-native link, которая автоматически производит все необходимые изменения и линковку нативного кода в проекте.
Но автоматическое подключение работает не всегда, поэтому иногда приходится выполнять изменения нативных файлов вручную. Если в инструкции к библиотеке не описана ручная установка, то подумайте, а стоит ли её использовать, и по возможности избегайте ручной линковки — пару раз мы наступали на такие грабли.
Результат
Изначально мы хотели написать приложение под iOS, но сделали и под Android, причем они выглядят по-разному, как и должны каждый на свой платформе. Переходы между экранами выглядит классно, а у нас примерно 30 экранов (переходов и состояний). Нам удалось избежать реализации кастомных нативных модулей и использовать сторонние решения.
Сроки: на разработку приложения ушло три месяца работы двух человек. С апреля 2019 выпустили еще несколько обновлений с новой функциональностью, приложение развивается, а команды других проектов хотят внедрить в него свои инструменты.
Производительность React Native нас полностью устраивает, потому что нам не нужна была сложная логика и у нас нет большой нагрузки.
Обратная связь: приятно получать благодарности от пользователей за приложение.
React Native подходит для быстрой разработки мобильных приложений и проверки гипотез, даже если вы не мобильный разработчик. Он постоянно развивается, появляются новые функции и библиотеки. Например, год назад не было поддержки 32 и 64-битных архитектур — сейчас с этим проблем нет.
Видите, каких классных результатов можно добиться, если не побояться взяться за что-то, что выходит за рамки привычных задач. Никогда заранее не известно, когда пригодятся знания, которые получил вроде как «для общего развития», но пригодятся они точно. Поэтому одна из задач РИТ++ 2020 — расширить кругозор и дать впечатление об актуальном положении дел в индустрии в целом.Смотрите программу, находите темы, которые помогут в работе уже завтра, и вопросы, до которых всё руки не доходили разобраться, и бронируйте билеты — билет на онлайн-конференцию можно позволить и без поддержки работодателя, сейчас для физлица он стоит 5900.