Введение
Плюсы языка:
Основные различия в синтаксисе:
Во что переводится View
Так выглядит пример нативной и браузерной вёрстки
Реакт нэйтив переводится в чистый JS, а затем движок Bridge выполняет операции в нужной ОС по инструкции JS
Для создания первых приложений очень удобно будет использовать Expo CLI для развёртывания приложения на React
Создаем проект с помощью expo-cli
Так же можно в наличии шаблоны на ts
npx create-expo-app .
Изучаем структуру проекта
app.json
- настройки expo
app.js
- корневой компонент приложения (вместо index.js
, как в обычном реакт)
Дальше всё как в обычном веб-приложении
Запуск проекта
Запускаем приложение классической командой
npm run start
Про Metro Bundler
Metro Bundler позволяет интерпретировать любой системе (ios/android/pc), что написано на JS и выполнить заданные действия. JS будет выполнять определённое действие и MB будет объяснять всем ОС, что нужно будет сделать, чтобы отобразить данные нативные элементы
Скачиваем Android Studio и настраиваем
Лучше молить бога, чтобы всё запустилось - смотрим туториалы
Запускаем на реальном устройстве приложение
Устанавливаем Expo Go на своё устройство
Сканируем QR или вставляем ссылку
И получаем итоговое приложение на Андроид
Скачиваем scrcpy для шаринга экрана устройства на ПК
Тут находится приложение для шаринга экрана на компьютер. У телефона должен быть включён режим отладки, чтобы получить к нему доступ по кабелю
Запускаем Expo в устройстве и открываем наше приложение
В терминале нужно нажать m
и в телефоне нужно подключить горячую перезагрузку, чтобы изменения применялись автоматически после сохранения
Начальная разработка приложения и стилизация
Изначально мы имеем два элемента, которые работают аналогично и в браузере:
View
- это замена для элементаdiv
, который хранит в себе элементыText
- это замена для элементаp
, который хранит в себе текст, но с одной оговоркой - мы обязательно должны использовать этот элемент для хранения текста
Если нам нужно будет передать классы или напрямую стили, нам нужно будет использовать атрибут style
.
Для генерации стилей мы можем использовать StyleSheet.create()
, что является аналогом createStyles
из materialui
App.js
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
// import './styles.css'
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.textStyles}>Hello</Text>
<Text
style={{
fontSize: 16,
}}
>
world!
</Text>
<StatusBar style='auto' />
</View>
);
}
const styles = StyleSheet.create({
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
},
textStyles: {
color: 'purple',
fontSize: 24,
},
});
Устанавливаем styled-components для стилизации компонентов
Устанавливаем стилизованные компоненты
npm i styled-components
И далее можем реализовывать более простым образом компоненты, как на сайтах. Но с одним отличием - импортируем стилевые компоненты из /native
import { View } from 'react-native';
import styled from 'styled-components/native';
const Post = styled.View`
padding: 15px;
width: 100px;
height: 100px;
background: red;
border-radius: 15px;
`;
export default function App() {
return (
<View>
<Post />
</View>
);
}
Начинаем верстать статью
Компонент StatusBar
отодвигает все элементы от статусбара
Создаём пост в отдельном файле
Так же тут нужно обратить внимание, что для отображения изображений, нужно использовать компонент Image
components / Post / Post.jsx
import styled from 'styled-components/native';
const PostView = styled.View`
display: flex;
flex-direction: row;
padding: 15px;
/* border-bottom: 1px solid rgba(0, 0, 0, 0.1); - так написать нельзя в styled-components */
border-bottom-width: 1px;
border-bottom-color: rgba(0, 0, 0, 0.1);
border-bottom-style: solid;
`;
const PostDetails = styled.View`
justify-content: center;
`;
const PostImage = styled.Image`
margin-right: 12px;
width: 100px;
height: 100px;
border-radius: 12px;
`;
const PostTitle = styled.Text`
font-size: 16px;
font-weight: 700;
`;
const PostDate = styled.Text`
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
`;
export const Post = ({ title, imageUri, createdAt }) => {
return (
<PostView>
<PostImage
source={{
uri: imageUri,
}}
/>
<PostDetails>
<PostTitle>{title}</PostTitle>
<PostDate>{createdAt}</PostDate>
</PostDetails>
</PostView>
);
};
И далее используем данный компонент с обычной передачей пропсов
App.js
import { StatusBar, View } from 'react-native';
import { Post } from './components/Post/Post';
export default function App() {
return (
<View>
<Post
title={'yes'}
createdAt={'yes'}
imageUri={
'https://media.kg-portal.ru/anime/j/jojosbizarreadventurediamondisunbreakable/images/jojosbizarreadventurediamondisunbreakable_32.jpg'
}
/>
<StatusBar theme={'auto'} />
</View>
);
}
Устанавливаем Axios и делаем запрос на получение статей
Воспользуемся mockAPI для генерации данных.
Далее мы просто воспользуемся useEffect
и useState
, как в обычном реакте, чтобы получить данные и отобразить их через map
.
Так же отдельно можно отметить, что мы можем выводить сообщение об ошибке, если воспользуемся Alert
App.js
import { Alert, StatusBar, View } from 'react-native';
import { Post } from './components/Post/Post';
import { useEffect, useState } from 'react';
import axios from 'axios';
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export default function App() {
const [posts, setPosts] = useState([]);
useEffect(() => {
axios
.get(API)
.then(({ data }) => setPosts(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статьи!'));
console.log(posts);
}, []);
return (
<View>
{posts.map((post) => (
<Post
key={post.id}
title={post.title}
imageUri={post.imageUrl}
createdAt={post.createdAt}
/>
))}
<StatusBar theme={'auto'} />
</View>
);
}
Тут нужно отметить, что без flex: 1
текст будет уходить за границы экрана
Как правильно рендерить список с возможностью скролла (FlatList)
Однако для рендера экрана, который будет поддерживать скролл, нужно будет воспользоваться компонентом FlatList
, который предоставит нам рендер большого списка.
Атрибут data
принимает в себя сам массив данных, а атрибут renderItem
уже принимает в себя объект с полем item
, в котором и хранятся наши данные. Тут нам для отрисовки не нужно передавать атрибут key
в компонент, так как FlatList
делает это автоматически
App.js
import { Alert, FlatList, StatusBar, View } from 'react-native';
import { Post } from './components/Post/Post';
import { useEffect, useState } from 'react';
import axios from 'axios';
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export default function App() {
const [posts, setPosts] = useState([]);
useEffect(() => {
axios
.get(API)
.then(({ data }) => setPosts(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статьи!'));
}, []);
return (
<View>
<FlatList
data={posts}
renderItem={({ item }) => (
<Post title={item.title} imageUri={item.imageUrl} createdAt={item.createdAt} />
)}
/>
<StatusBar theme={'auto'} />
</View>
);
}
Делаем рендер иконки загрузки контента (ActivityIndicator)
Сделать иконку загрузки мы можем через использование компонента ActivityIndicator
и useState
, который будет хранить статус загрузки изменяемый в fetchPosts
App.js
import { Alert, FlatList, StatusBar, View, Text, ActivityIndicator } from 'react-native';
import { Post } from './components/Post/Post';
import { useEffect, useState } from 'react';
import axios from 'axios';
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export default function App() {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const fetchPosts = () => {
setIsLoading(true);
axios
.get(API)
.then(({ data }) => setPosts(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статьи!'))
.finally(() => setIsLoading(false));
};
useEffect(fetchPosts, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator style={{ marginBottom: 15 }} size={'large'} />
<Text>Загрузка...</Text>
</View>
);
}
return (
<View>
<FlatList
data={posts}
renderItem={({ item }) => (
<Post title={item.title} imageUri={item.imageUrl} createdAt={item.createdAt} />
)}
/>
<StatusBar theme={'auto'} />
</View>
);
}
Перезагрузка контента по свайпу (RefreshController)
Далее нам нужно реализовать функционал, когда по свайпу сверху вниз, мы будем подгружать новые данные пользователя
Для этого уже используется атрибут refreshControl
внутри FlatList
и компонент RefreshControl
refreshing
- boolean обновления данных
onRefresh
- отвечает за действие во время перезагрузки
import {
Alert,
FlatList,
StatusBar,
View,
Text,
ActivityIndicator,
RefreshControl,
} from 'react-native';
import { Post } from './components/Post/Post';
import { useEffect, useState } from 'react';
import axios from 'axios';
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export default function App() {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const fetchPosts = () => {
setIsLoading(true);
axios
.get(API)
.then(({ data }) => setPosts(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статьи!'))
.finally(() => setIsLoading(false));
};
useEffect(fetchPosts, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator style={{ marginBottom: 15 }} size={'large'} />
<Text>Загрузка...</Text>
</View>
);
}
return (
<View>
<FlatList
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={fetchPosts} />}
data={posts}
renderItem={({ item }) => (
<Post title={item.title} imageUri={item.imageUrl} createdAt={item.createdAt} />
)}
/>
<StatusBar theme={'auto'} />
</View>
);
}
Делаем статью кликабельной (TouchableOpacity)
Для отслеживания нажатия на элемент и реагирования на это действие, мы можем использовать TouchableOpacity
- он автоматически будет менять отображение при нажатии на элемент и так же через onPress
позволит вызывать функционал
<FlatList
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={fetchPosts} />}
data={posts}
renderItem={({ item }) => (
<TouchableOpacity onPress={/* тут можно поместить действие при клике */}>
<Post
title={item.title}
imageUri={item.imageUrl}
createdAt={item.createdAt}
/>
</TouchableOpacity>
)}
/>
И теперь происходит изменение opacity у активного элемента, на который кликают
Создаем экран отображения полной статьи FullPostScreen
Далее стоит разделить приложение на отдельные страницы, которые принято называть скринами и далее реализуем страницу отдельного поста
screens > PostScreen.jsx
import { ActivityIndicator, Alert, Text, View } from 'react-native';
import styled from 'styled-components/native';
import { useEffect, useState } from 'react';
import axios from 'axios';
import { Loading } from '../components/Loading/Loading';
const PostImage = styled.Image`
width: 100%; height: 250px;
margin-bottom: 20px;
border-radius: 15px;`;
const PostText = styled.Text`
font-size: 18px; line-height: 24px;`;
const PostTitle = styled.Text`
font-size: 24px; font-weight: 700; line-height: 24px;`;
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export const PostScreen = () => {
const [post, setPost] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const fetchPosts = () => {
setIsLoading(true);
axios
.get(API + '/1')
.then(({ data }) => setPost(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статью!'))
.finally(() => setIsLoading(false));
};
useEffect(fetchPosts, []);
if (isLoading) {
return <Loading />;
}
return (
<View style={{ padding: 20 }}>
<PostTitle>{post.title}</PostTitle>
<PostImage source={{
uri: post.imageUrl,
}}
/>
<PostText>{post.text}</PostText>
</View>
);
};
Подключаем роутинг с помощью React Navigation
Для реализации стандартной навигации потребуются следующие пакеты:
npm i @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context react-native-gesture-handler
И уже таким образом реализуется навигация по приложению. Все имена роутов должны быть уникальными. Самый верхний скрин отобразится первым. В options
мы передаём тот заголовок, который отобразится на странице приложения
screens > Navigation.jsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NavigationContainer } from '@react-navigation/native';
import { HomeScreen } from './HomeScreen';
import { PostScreen } from './PostScreen';
const Stack = createNativeStackNavigator();
export const Navigation = () => {
return (
/* нужен для корректного отображения скринов */
<NavigationContainer>
{/* аналог routes */}
<Stack.Navigator>
<Stack.Screen component={HomeScreen} name={'Home'} options={{ title: 'Новости' }} />
<Stack.Screen component={PostScreen} name={'Post'} options={{ title: 'Пост' }} />
</Stack.Navigator>
</NavigationContainer>
);
};
И далее нам нужно в главном компоненте передать чистый навигатор без всяких обёрток и статусбара, так как за нас всё настроить навигационный контейнер
App.js
import { Navigation } from './screens/Navigation';
export default function App() {
return <Navigation />;
}
Делаем переход на экран полной записи при клике на статью
Первым делом нужно получить объект navigation
из пропсов, который попадает туда из роутера приложения. Этот объект хранит функции для работы с роутингом нашего приложения. Далее нужно на TouchableOpacity
повесить функцию navigate
screens > HomeScreen.jsx
import {
Alert,
FlatList,
View,
Text,
ActivityIndicator,
RefreshControl,
TouchableOpacity,
} from 'react-native';
import { Post } from '../components/Post/Post';
import { useEffect, useState } from 'react';
import axios from 'axios';
import { Loading } from '../components/Loading/Loading';
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export const HomeScreen = ({ navigation }) => {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const fetchPosts = () => {
setIsLoading(true);
axios
.get(API)
.then(({ data }) => setPosts(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статьи!'))
.finally(() => setIsLoading(false));
};
useEffect(fetchPosts, []);
if (isLoading) {
return <Loading />;
}
return (
<View>
<FlatList
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={fetchPosts} />}
data={posts}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => navigation.navigate('Post', { id: item.id })}>
<Post
title={item.title}
imageUri={item.imageUrl}
createdAt={item.createdAt}
/>
</TouchableOpacity>
)}
/>
</View>
);
};
screens > PostScreen.jsx
import { ActivityIndicator, Alert, Text, View } from 'react-native';
import styled from 'styled-components/native';
import { useEffect, useState } from 'react';
import axios from 'axios';
import { Loading } from '../components/Loading/Loading';
const PostImage = styled.Image`
width: 100%;
height: 250px;
margin-bottom: 20px;
border-radius: 15px;
`;
const PostText = styled.Text`
font-size: 18px;
line-height: 24px;
`;
const PostTitle = styled.Text`
font-size: 24px;
font-weight: 700;
line-height: 24px;
margin-bottom: 20px;
`;
const API = 'https://64db11b4593f57e435b06489.mockapi.io/api/posts/hasles';
export const PostScreen = ({ route, navigation }) => {
const [post, setPost] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// достаём те параметры, которые сюда передал родитель
const { id } = route.params;
const fetchPosts = () => {
setIsLoading(true);
axios
.get(API + `/${id}`)
.then(({ data }) => setPost(data))
.catch((e) => Alert.alert('Ошибка', 'Не получается получить статью!'))
.finally(() => setIsLoading(false));
// тут мы можем указать тот же объект options, что и в роутере
navigation.setOptions({
title: post.title,
});
};
useEffect(fetchPosts, []);
if (isLoading) {
return <Loading />;
}
return (
<View style={{ padding: 20 }}>
<PostTitle>{post.title}</PostTitle>
<PostImage
source={{
uri: post.imageUrl,
}}
/>
<PostText>{post.text}</PostText>
</View>
);
};
И теперь мы можем с родительского экрана перейти в дочерний и имеем возможность выйти из нужного нам места