Обложка статьи «Как разработать своё первое приложение на React Native»

Как разработать своё первое приложение на React Native

Никита Кольцов

Никита Кольцов, технический директор Secreate

Привет я Никита, занимаюсь разработкой на React/React Native и на всяких других штуках в Secreate. Хочу немного поговорить о том, что же всё-таки такое React Native, почему он сейчас важен и популярен, а также вместе с вами попробую написать небольшой проект для демонстрации основ.

Что такое React Native?

Сначала нужно разобраться, что есть React. React — это инструмент для создания пользовательских интерфейсов. Его основная задача — обеспечить отображение на экране того, что можно увидеть на веб-страницах. React позволяет легко создавать интерфейсы, разделяя каждую страницу на небольшие фрагменты и компоненты. Он очень удобен для создания веб-приложений и не требует большого порога вхождения. Так вот, ребята из Facebook подумали, что было бы круто, если бы React можно было бы использовать для создания кросс-платформенных мобильных приложений, и в 2015 году React Native был представлен публике и появился на GitHub, где уже каждый мог внести свой вклад. React популярен по нескольким причинам. Он компактен и отличается высокой производительностью, в особенности при работе с быстро меняющимися данными. За счет своей компонентной структуры, React поощряет вас писать модульный и многократно используемый код.

В мире кроссплатформенной мобильной разработки уже были свои решения, к примеру Apache Cordova — технология, которая позволяла использовать HTML + CSS + JavaScript + нативный функционал платформы, на которой приложение было запущено, для его работы. Однако, технология имеет большую проблему — быстродействие. Так же на данный момент существуют и другие, такие как Xamarin, Flutter, QT и так далее.

В React Native код пишется на JavaScript, и исполняется при помощи JavaScriptCore — движка, который использует Safari. Так же можно использовать нативные модули платформы, к примеру камеру или Bluetooth. Для этого пишется код, реализующий функциональность на языке, который предназначается для разработки под конкретную платформу (Java/Swift/Objective C) и сообщается со средой JavaScript посредством bridge.

Если сравнивать с типовой разработкой для iOS и Android, React Native предлагает много других возможностей. Так как ваше приложение в основном состоит из JavaScript, можно воспользоваться многими преимуществами. Например, для того чтобы увидеть добавленные в код изменения, вы можете немедленно «обновить» приложение, не дожидаясь завершения билда.

Для большинства стандартных элементов UI есть компоненты в RN, к примеру View = UIView в iOS, так что реализовывать изыски интерфейса должно быть несложно, если есть знания React.

Почему же RN стал таким популярным?

  • для написания кода используется JavaScript, в частности React;
  • есть возможность быстро написать приложение под обе платформы. Меньше затраты — выгоднее бизнесу;
  • большая библиотека нативных и не-нативных компонентов;
  • для отладки можно использовать браузер, так же есть hot-reload для быстрого просмотра изменений. Нет необходимости пересобирать приложение при внесении изменений в код;
  • для отрисовки компонентов используются нативные компоненты используемой системы (например UIImage/ImageView), соответственно производительность такого UI выше, чем при использовании webview;
  • просто написать свою библиотеку для RN, использующую нативный функционал системы;
  • ещё одна причиной почему RN набрал популярность в последние годы является то, что его используют такие гиганты как Facebook, Instagram, Skype, Tesla, Baidu, Bloomberg и т. д.

Первое приложение

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

Нам понадобится cli утилита npx, которая поставляется вместе с Node.js, Android Studio — если мы хотим протестировать приложение на Android, либо Xcode — если на iPhone. Как установить указанные инструменты подробнее можно почитать здесь.

Начнем с того, что создадим проект командой npx react-native init RandomGuysApp. В RN приложение также можно добавить TypeScript, но мы этого здесь делать не будем.

Спустя какое-то время у нас создалась такая структура папок

В консоли пишем yarn add @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view. Это установит библиотеку для навигации, что-бы можно было переходить между экранами приложения и зависимости для неё. Redux, или другую библиотеку для управления состоянием я решил не подключать, так-как думаю, что в нашем примере это будет немного излишне. После установки, если вы собираетесь запускать приложение на iOS, нужно зайти в папку ios и написать pod install (предварительно установив cocoapods на компьютер).

В index.js написано:

AppRegistry.registerComponent(appName, () => App);

это показывает, что компонент App является точкой входа в наше приложение. Я предпочитаю хранить код в отдельной папке, поэтому перенесу App.js в новую папку — src, сменив в index.js путь до него на

import {App} from './src/App';

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

Создадим папку navigation, а в ней RootNavigator.js, в который поместим такой код

import * as React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import {PersonListScreen} from '../screens/PersonListScreen';

const Stack = createStackNavigator();

export const RootNavigator = () => {
	return (
	<Stack.Navigator initialRouteName={'list'}>
		<Stack.Screen name={'list'} component={PersonListScreen} />
	</Stack.Navigator>
	);
};

Пара моментов:

  • компонента ListScreen еще не существует;
  • в приложении ожидается всего два экрана, так что структурно оно несколько упрощено, для демонстрации работы с RN.

Что здесь происходит? Создаётся новый стек навигации, в котором указывается список экранов, находящихся в нем. А также указывается стартовый экран стека. Т. к. у нас будет всего один стек, то по сути это и будет стартовый экран приложения.

Перейдем к созданию первого экрана. Добавим папку screens, в которой создадим файл PersonListScreen

Там разместим такой код:

import React, {Component} from 'react';
import {FlatList, View, StyleSheet} from 'react-native';
import {PersonListItem} from '../components/PersonListItem';

export class PersonListScreen extends Component {
	state = {
		list: [],
		isLoading: false,
	};

	componentDidMount = () => {
		this.onRefresh();
	};

	getMoreData = (isRefreshing) => {};

	onRefresh = () => {
		this.getMoreData(true);
	};

	onScrollToEnd = ({distanceFromEnd}) => {
		if (distanceFromEnd < 0) {
			return;
		}
		this.getMoreData(false);
	};

	onItemPress = (item) => {
		this.props.navigation.navigate('info', {person: item});
	};

	keyExtractor = (person) => person.login.uuid;

	renderItem = ({item}) => {
	return (
		<PersonListItem
			person={item}
			onPress={this.onItemPress.bind(this, item)}
		/>
		);
	};

	render = () => {
	const {isLoading, list} = this.state;

	return (
		<View style={styles.container}>
		<FlatList
			data={list}
			renderItem={this.renderItem}
			keyExtractor={this.keyExtractor}
			refreshing={isLoading}
			onRefresh={this.onRefresh}
			onEndReached={this.onScrollToEnd}
			onEndReachedThreshold={0.2}
		/>
		</View>
	);
	};
}

Опять же, PersonListItem еще не существует, но это временно. FlatList, это эффективный компонент для рендера списков — он поддерживает PullToRefresh, прокрутку к элементу списка по индексу, производительный рендер за счет отключения рендера компонентов, которые слишком далеко от видимой области экрана и многое другое. Конечно, одинаковые элементы в столбик/ряд можно рисовать и другим образом, например через ScrollView, но это самый эффективный способ.

В renderItem передается каждый элемент массива из пропсы data, и возвращается списковый компонент. keyExtractor это по сути аналог свойства key, только в виде функции, вызываемой для каждого элемента.

Итак, при заходе на страницу мы собираемся получить список юзеров, и отрисовать его. Когда список тянется вниз — обновить его, а когда мы достигаем его конца — подгрузить новые данные. Вернёмся пока к PersonListItem, добавим в components файл PersonListItem.js и заполним его таким кодом

import React, {Component} from 'react';
import {StyleSheet, TouchableOpacity, Image, Text, View} from 'react-native';
export class PersonListItem extends Component {
	render = () => {
	const {onPress, person} = this.props;

	return (
		<TouchableOpacity style={styles.container} onPress={onPress}>
		<Image
			source={{uri: person.picture.medium}}
			resizeMode={'contain'}
			style={styles.avatar}
		/>
		<View style={styles.col}>
			<Text style={styles.name}>
			{person.name.first} {person.name.last}
			</Text>
			<Text style={styles.email}>{person.email}</Text>
		</View>
		</TouchableOpacity>
	);
	};
}

const styles = StyleSheet.create({
	container: {
		flexDirection: 'row',
		alignItems: 'center',
		padding: 12,
		borderBottomColor: '#b0b0b0',
		borderBottomWidth: 0.4,
	},
		avatar: {
		width: 50,
		height: 50,
		borderRadius: 25,
	},
	col: {
		marginLeft: 10,
	},
	name: {
		fontSize: 16,
		color: '#2e2e2e',
	},
	email: {
		marginTop: 10,
		fontSize: 13,
		color: '#b0b0b0',
	},
});

В RN нет пропсы className и стили передаются в пропсе style, а объявляются через StyleSheet.create(). Можно, конечно, использовать объект как стиль, но в этом случае не будут применяться некоторые внутренние оптимизации производительности.

Заметили, что вместо пикселей используются голые числа? Потому что по умолчанию, как единица измерения используются dp (density-independent pixels), а для конвертации в px можно использовать PixelRatio.getPixelSizeForLayoutSize(), но это нам не особо нужно. В принципе, практически все свойства стилей дублируют CSS, так-что особо проблем быть не должно. Но есть такая особенность — все компоненты в RN имеют display: flex. Ещё есть display: none, других вариантов нет. Главное отличие, что все компоненты по умолчанию имеют flexDirection: column, в отличие от браузерной версии. Есть еще несколько отличий, но перечислять я их не буду, в целом можно сказать, что в RN display: flex несколько урезанный.

Вернёмся к App.js. Заменим содержимое файла следующим кодом

import React, {Component} from 'react';
import {SafeAreaView} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {RootNavigator} from './navigators/RootNavigator';

export class App extends Component {
	render = () => {
	return (
		<SafeAreaView>
		<NavigationContainer>
			<RootNavigator />
		</NavigationContainer>
		</SafeAreaView>
	);
	};
}

const styles = StyleSheet.create({
	container: {
	flex: 1,
	},
});

Используем наш созданный роутер, что-бы у нас работала навигация. Так же здесь используется компонент SafeAreaView, он нужен что-бы контент в iOS отрисовывался внутри «безопасных» границ экрана и, к примеру, не наезжал на «монобровь» в iPhone X.

Сборка и запуск

Попробуем собрать и запустить то, что у нас есть. Если у вас macOS, то можно использовать Xcode. Для этого при открытии проекта нужно указать файл ios/RandomGuysApp.xcworkspace внутри проекта. Если Android, то нужно установить JDK и необходимые SDK. Проще всего открыть папку android в Android Studio, и IDE установит необходимое ПО. А после этого написать в корневой папке проекта npx react-native run-android.

Когда проект запустится, то на экране должно появиться, что-то вроде этого.

Это экран с названием list, который мы зарегистрировали в навигаторе. Но там пока что пусто, так как нам еще нечего отрисовывать. Поэтому вернемся в PersonListScreen.js и добавим в функцию getMoreData() код для получения данных.

getMoreData = (isRefreshing) => {
	const limit = 15;
	const offset = isRefreshing ? 0 : this.state.list.length;
	const page = Math.ceil(offset / limit) + 1;
	fetch(`https://randomuser.me/api/?seed=foobar&results=15&page=${page}`)
	  .then((r) => r.json())
	  .then((json) => {
		this.setState({
		  list: isRefreshing
			? this.state.list.concat(json.results)
			: json.results,
		});
	  })
	  .catch((e) => {
		console.log(e);
	  });
  };

Для отладки приложения можно использовать react-native-debugger, либо браузер. Включается дебаггер комбинацией клавиш ctrl(cmd) + M для Android, cmd + D для iOS. Там же можно включить live reload.

Сверху к import из react-native добавим импорт модуля StyleSheet

import {FlatList, View, StyleSheet} from 'react-native';

Вниз этого же файла добавим

const styles = StyleSheet.create({
	container: {
	  flex: 1,
	},
  });

и добавим этот стиль нашему View. Получаем что-то вроде такого

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

onItemPress = (item) => {
	this.props.navigation.navigate('info', {person: item});
};

Это переход в экран с названием info с передачей выбранного юзера как параметра. В дальнейшем его можно будет получить из компонента экрана. Теперь добавим этот экран в навигатор RootNavigator.js и напишем для него код.

...
import {PersonInfoScreen} from '../screens/PersonInfoScreen';
...
<Stack.Screen name={'list'} component={PersonListScreen} />
<Stack.Screen name={'info'} component={PersonInfoScreen} />
...

Так же создадим файл src/screens/PersonInfoScreen.js и добавим туда следующий код.

import React, {Component} from 'react';
import {StyleSheet, View, Image, Text} from 'react-native';

export class PersonInfoScreen extends Component {
	renderRow = (cells) => {
	return cells.map((cell) => (
		<View style={styles.cell} key={cell.title}>
		<Text style={styles.cellTitle}>{cell.title}</Text>
		<Text style={styles.cellValue}>{cell.value}</Text>
		</View>
	));
	};

	render = () => {
	const {person} = this.props.route.params;

	return (
		<View style={styles.container}>
		<Image
			source={{uri: person.picture.large}}
			style={styles.avatar}
			resizeMode={'contain'}
		/>
		{this.renderRow([
			{title: 'login', value: person.login.username},
			{title: 'name', value: person.name.first},
			{title: 'surname', value: person.name.last},
			{title: 'email', value: person.email},
			{title: 'phone', value: person.cell},
		])}
		</View>
	);
	};
}

const styles = StyleSheet.create({
	container: {
		flex: 1,
		alignItems: 'center',
	},
	avatar: {
		width: 100,
		height: 100,
		borderRadius: 50,
		marginTop: 20,
	},
	cell: {
		marginTop: 15,
		justifyContent: 'center',
		alignItems: 'center',
	},
	cellTitle: {
		fontSize: 13,
		color: '#b0b0b0',
	},
	cellValue: {
		marginTop: 10,
		fontSize: 16,
		color: '#2e2e2e',
	},
});

Вот что у нас получилось.

Таким образом мы сделали приложение для вывода и просмотра некоторой информации по разным людям.

Ресурсы

Для изучения RN можно найти много ресурсов, вот небольшой список:

В итоге

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

Код проекта можно найти здесь.