В этой статье рассказывается о том, как создать React-Native приложение с использованием Strapi в качестве бэкенда.
Автор учебника: Chigozie Oduah
Strapi — это безголовая CMS, которая позволяет легко создавать настраиваемые бэкенд-сервисы. Вы можете интегрировать приложения Strapi с любым типом фронтенда и развернуть приложение в облаке.
Это руководство проведет вас через процесс создания простого React-native приложения для выполнения дел с использованием Strapi в качестве бэкенда. Вы сможете создавать, редактировать и удалять дела по каждому пользователю, взаимодействуя с REST API Strapi.
Примечание: В этом руководстве предполагается, что у вас уже запущен экземпляр Strapi и создан тестовый пользователь. Если это не так, прочитайте руководство по началу работы.
Пример приложения доступен на Github.
- Настройка бэкенда
- Создание мобильного приложения
- Интерфейс
- Управление состоянием
- Создание моделей и контроллеров
- Создание магазина Redux
- Шаги
- Создание навигации
- Экраны
- Навигация
- Построение логики навигации
- Включение навигации в наше приложение
- Кодирование и стилизация экрана входа в систему
- Кодирование и стилизация экрана обзора
- Компонент списка
- Компонент просмотра
- Резюме
Настройка бэкенда
Для начала нам нужно создать проект Strapi с помощью команды ниже:
npx create-strapi-app@latest strapi-backend
После создания проекта Strapi автоматически запустит сервер разработки. Мы всегда можем запустить сервер разработки самостоятельно, открыв корневую папку в терминале и выполнив команду yarn develop
:
yarn develop
Нам нужно создать новый тип содержимого «todo». Для этого нам нужно зайти в Content-Type Builder и нажать на + Create new collection type.
Теперь, когда мы успешно создали наш новый Content-Type, нам нужно добавить в него некоторые поля:
Когда вы успешно добавите поля в Content-Type, он должен выглядеть следующим образом:
После сохранения изменений нам нужно зарегистрировать конечные точки для коллекции ToDo. Конечные точки позволят нашему react-native приложению взаимодействовать с коллекцией. Чтобы зарегистрировать конечные точки, нам нужно:
- Перейти в Настройки → Пользователи и плагин разрешений → Роли.
- Нажмите на Authenticated.
- Переключите выпадающий список Todo в разделе Permissions.
- Поставьте галочку Select All (Выбрать все).
- Нажмите Сохранить.
Создание мобильного приложения
Теперь, когда мы настроили наш API, мы можем сосредоточиться на нашем мобильном приложении. Если вы еще не знакомы с React native, рекомендую ознакомиться с руководством по началу работы с React native. Нам нужно инициализировать новый проект React-native, запустив React-native CLI.
react-native init TodoApp
Выполнение этой команды создаст новый каталог с указанным именем приложения, который станет корнем вашего проекта. В базовом каталоге вашего проекта вам нужно будет создать следующую структуру папок:
src
|-- app
|-- controllers
|-- models
|-- views
|-- components
|-- redux
|-- actions
|-- reducers
|-- screens
Теперь, когда наш проект инициализирован, структура оптимизирована, а бэкенд запущен, мы можем добавить некоторые пакеты в наше приложение. Мы будем использовать предопределенный список пакетов, который я нашел. Возможно, есть лучшие альтернативы, но вы можете решить это сами!
Интерфейс
Управление состоянием
Чтобы добавить пакеты, установите их с помощью yarn:
yarn add react-native-paper react-native-vector-icons react-navigation redux react-redux redux-persist @react-native-community/async-storage react-native-gesture-handler
Создание моделей и контроллеров
Прежде чем перейти к интерфейсу приложения, мы создадим модель для нашего TODO. Для этого создайте новый файл в ./src/app/models/TodoModel.js
. Поскольку этот файл содержит модель для Content-Type, которую мы создали ранее, поля должны быть точно такими же.
- Создайте файл
./src/app/models/TodoModel.js
, содержащий следующий код:
// TodoModel.js
/**
* TODO Model as defined in Strapi
*/
import {edit, save, dismiss} from '../controllers/TodoController';
class TodoModel {
constructor(user, title, description, finished = false, id = undefined) {
this.user = user;
this.title = title;
this.description = description;
this.finished = finished;
this.id = id;
// save function adds id property later
}
async save() {
// save the todo to Strapi
const id = await save(this);
// should no id be returned throw an error
if (!id) {
throw new Error('Todo could not be saved');
}
// set id and return true
this.id = id;
return true;
}
async edit() {
if (!this.id) {
throw new Error('Cannot edit TODO before it was saved.');
}
const edited = await edit(this);
// check if the edit returned false
if (!edited) {
throw new Error('Todo could not be edited.');
}
return true;
}
async dismiss() {
if (!this.id) {
throw new Error('Cannot delete TODO before it was saved.');
}
const dismissed = await dismiss(this);
// check if the dismiss returned false
if (!dismissed) {
throw new Error('Todo could not be deleted.');
}
return true;
}
}
export default TodoModel;
- Сделайте то же самое для нашего
User
Content-Type из плагина User-Permissions в Strapi. Создайте файл в./src/app/models/UserModel.js
, содержащий следующий код:
// UserModel.js
/**
* User model as defined in Strapi
*/
import {login, logout} from '../controllers/UserController';
class UserModel {
constructor(identifier, password) {
this.identifier = identifier;
this.password = password;
}
async login() {
const result = await login(this);
if (!result) {
throw new Error('Unable to login user.');
}
return true;
}
async logout() {
const result = await logout(this);
if (!result) {
throw new Error('Unable to logout user.');
}
return true;
}
}
export default UserModel;
Теперь, когда мы закодировали наши модели, вы можете заметить, что мы импортировали файл, который мы еще не создали, поэтому давайте создадим два необходимых файла:
./src/app/controllers/UserController.js
./src/app/controllers/TodoController.js
Эти файлы являются нашими контроллерами, где мы храним логику нашего приложения, которая будет выполняться при вызове функций нашей модели.
// TodoController.js
import {store} from '../../redux/Store';
/**
* if you have an instance of Strapi running on your local
* machine:
*
* 1. Run `adb reverse tcp:8163 tcp:8163` (only on android)
*
* 2. You have to change the access IP from localhost
* to the IP of the machine Strapi is running on.
*/
const url = 'http://localhost:1337/todos';
/**
* add a todo to Strapi
*/
export const save = async todo => {
const requestBody = JSON.stringify({
title: todo.title,
description: todo.description,
finished: todo.finished,
user: todo.user.id,
});
const requestConfig = {
method: 'POST',
headers: {
Authorization: `Bearer ${store.getState().jwt}`,
'Content-Type': 'application/json',
},
body: requestBody,
};
const response = await fetch(url, requestConfig);
const json = await response.json();
if (json.error) {
return null;
}
return json._id;
};
/**
* add a todo to Strapi
*/
export const edit = async todo => {
const requestBody = JSON.stringify({
title: todo.title,
description: todo.description,
due: todo.due,
finished: todo.finished ? 1 : 0,
user: todo.user.id,
});
const requestConfig = {
method: 'PUT',
headers: {Authorization: `Bearer ${store.getState().jwt}`},
body: requestBody,
};
const response = await fetch(`${url}/${todo.id}`, requestConfig);
const json = await response.json();
if (json.error) {
return false;
}
return true;
};
/**
* delete a todo from Strapi
*/
export const dismiss = async todo => {
const response = await fetch(`${url}/${todo.id}`, {
headers: {Authorization: `Bearer ${store.getState().jwt}`},
});
const json = response.json();
if (json.error) {
return false;
}
return true;
};
Теперь второй контроллер:
// UserController.js
import {saveUser, deleteUser} from '../../redux/actions/UserActions';
/**
* if you have an instance of Strapi running on your local
* machine:
*
* 1. Run `adb reverse tcp:8163 tcp:8163` (only on android)
*
* 2. You have to change the access IP from localhost
* to the IP of the machine Strapi is running on.
*/
const url = 'http://192.168.0.57:1337';
/**
* @param {UserModel} user
*/
export const login = async user => {
const requestConfig = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: user.identifier,
password: user.password,
}),
};
try {
const response = await fetch(`${url}/auth/local`, requestConfig);
const json = await response.json();
if (json.error) {
return false;
}
saveUser(json.jwt, json.user);
return true;
} catch (err) {
alert(err);
return false;
}
};
/**
* @param {UserModel} user
*/
export const logout = async user => {
deleteUser();
};
Как видно, мы вызываем наш redux store в конце UserController.login()
и UserController.logout()
. Через несколько минут все станет более понятно.
Создание магазина Redux
Чтобы иметь возможность обновлять наш пользовательский интерфейс, нам нужно создать магазин Redux. Это хранилище будет хранить данные пользователя и сохраняться в случае изменения. Удивительно, правда?
Шаги
Создайте следующие файлы:
./src/redux/Store.js
./src/redux/reducers/UserReducer.js
./src/redux/actions/UserActions.js
Теперь, когда мы создали файлы, мы можем приступить к созданию логики нашего магазина. Логика каждого магазина хранится в так называемом редукторе.
Редуктор может получать действие; это действие имеет тип и необязательную полезную нагрузку, которую вы можете определить для каждого запроса. Нам понадобятся два типа действий USER_SAVE
и USER_DELETE
, которые символизируют соответствующие входы/выходы пользователей. Однако мы не будем реализовывать USER_DELETE
.
// UserReducer.js
const defaultState = {
jwt: null,
user: null,
};
/**
* This is a reducer, a pure function with (state, action) => state signature.
* It describes how an action transforms the state into the next state.
*
* The shape of the state is up to you: it can be a primitive, an array, an object,
* or even an Immutable.js data structure. The only important part is that you should
* not mutate the state object, but return a new object if the state changes.
*
* In this example, we use a `switch` statement and strings, but you can use a helper that
* follows a different convention (such as function maps) if it makes sense for your
* project.
*/
const UserReducer = (state = defaultState, action) => {
switch (action.type) {
case 'USER_SAVE': {
return {
...state,
...{jwt: action.payload.jwt, user: action.payload.user},
};
}
case 'USER_DELETE': {
return defaultState;
}
default:
return defaultState;
}
};
export default UserReducer;
Чтобы вызвать этот редуктор, мы обратимся к ранее созданному файлу UserActions.js
. В нем содержатся два действия: saveUser()
и deleteUser()
.
// UserActions.js
import {store} from '../Store';
// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
export const saveUser = (jwt, user) => {
store.dispatch({
type: 'USER_SAVE',
payload: {
jwt,
user,
},
});
};
export const deleteUser = () => {
store.dispatch({type: 'USER_DELETE'});
};
И, наконец, мы должны написать наш файл Store.js
. Этот файл не только включает редуктор, но и обеспечивает персистентность с помощью ранее установленной библиотеки redux-persist
.
// Store.js
import {createStore} from 'redux';
import {persistStore, persistReducer} from 'redux-persist';
import AsyncStorage from '@react-native-community/async-storage';
import rootReducer from '../redux/reducers/UserReducer';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
const createdStore = createStore(persistedReducer);
const createdPersistor = persistStore(createdStore);
export const store = createdStore;
export const persistor = createdPersistor;
Еще один шаг, и ваше приложение готово к работе с redux! Добавьте компоненты PersistorGate
и Provider
в файл App.js
.
// App.js
import React from 'react';
import {PersistGate} from 'redux-persist/integration/react';
import {Provider} from 'react-redux';
import {store, persistor} from './src/redux/Store';
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor} />
</Provider>
);
};
export default App;
Создание навигации
Для создания экранов мы будем использовать ранее установленный пакет react-navigation. Нам придется создать кучу файлов; надеюсь, вы готовы запачкать руки!
Экраны
./src/screens/Overview.js
./src/screens/Login.js
Навигация
./src/components/navigation/Authentication.js
После создания заполните все файлы Screen макетным содержимым, чтобы можно было различать, на каком экране вы находитесь в данный момент.
// Overview.js & Login.js
import React from 'react';
import {View, StyleSheet} from 'react-native';
import {Text} from 'react-native-paper';
const SCREEN_NAME = props => {
return (
<View style={styles.base}>
<Text style={styles.text}>SCREEN_NAME</Text>
</View>
);
};
const styles = StyleSheet.create({
base: {
flex: 1,
alignContent: 'center',
justifyContent: 'center',
},
text: {
textAlign: 'center',
},
});
export default SCREEN_NAME;
Примечание: Замените
SCREEN_NAME
на название экрана.
Построение логики навигации
Откройте файл Authentication.js
, созданный в предыдущем шаге, и создайте новый SwitchNavigator с помощью метода createStackNavigator()
. Мы используем SwitchNavigator в сочетании с redux для перенаправления пользователя на страницу входа или страницу обзора в зависимости от состояния его аутентификации.
// Authentication.js
import React from 'react';
// navigation components
import {createSwitchNavigator, createAppContainer} from 'react-navigation';
import Login from '../../screens/Login';
import Overview from '../../screens/Overview';
import {store} from '../../redux/Store';
const Authentication = () => {
const [route, setRoute] = React.useState(
store.getState().jwt ? 'Overview' : 'Login',
);
const Navigator = createAppContainer(
createSwitchNavigator(
{
Login: {
screen: Login,
},
Overview: {
screen: Overview,
},
},
{
initialRouteName: route,
},
),
);
// on mount subscribe to store event
React.useEffect(() => {
store.subscribe(() => {
setRoute(store.getState().jwt ? 'Overview' : 'Login');
});
}, []);
return <Navigator />;
};
export default Authentication;
Включение навигации в наше приложение
Импортируйте файл навигации в файл App.js
и отобразите его как компонент. Также добавьте компонент Provider react-native-paper
.
// App.js
import React from 'react';
import {Provider as PaperProvider} from 'react-native-paper';
import {PersistGate} from 'redux-persist/integration/react';
import {Provider} from 'react-redux';
import {store, persistor} from './src/redux/Store';
import Authentication from './src/components/navigation/Authentication';
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<PaperProvider>
<Authentication />
</PaperProvider>
</PersistGate>
</Provider>
);
};
export default App;
Теперь запустите ваш проект и посмотрите на ваше устройство/эмулятор, вы должны увидеть следующий экран:
Кодирование и стилизация экрана входа в систему
Наш макет экрана прекрасен, но нам нужно добавить к нему некоторую функциональность.
// Login.js
import React from 'react';
import {View, StyleSheet, StatusBar} from 'react-native';
import {
Headline,
Paragraph,
TextInput,
Button,
Snackbar,
Portal,
} from 'react-native-paper';
import UserModel from '../app/models/UserModel';
const Login = props => {
const [identifier, setIdentifier] = React.useState('');
const [password, setPassword] = React.useState('');
const [visible, setVisible] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(false);
const validateInput = () => {
let errors = false;
if (!identifier || identifier.length === 0) {
errors = true;
}
if (!password || password.length === 0) {
errors = true;
}
return !errors;
};
const authenticateUser = async () => {
if (validateInput()) {
setLoading(true);
const user = new UserModel(identifier, password);
try {
await user.login();
} catch (err) {
setError(err.message);
setVisible(true);
setLoading(false);
}
} else {
setError('Please fill out all *required fields');
setVisible(true);
setLoading(false);
}
};
return (
<View style={styles.base}>
<>
<StatusBar backgroundColor="#ffffff" barStyle="dark-content" />
</>
<View style={styles.header}>
<Headline style={styles.appTitle}>TodoApp</Headline>
<Paragraph style={styles.appDesc}>
Authenticate with Strapi to access the TodoApp.
</Paragraph>
</View>
<>
<View style={styles.divider} />
<TextInput
onChangeText={text => setIdentifier(text)}
label="*Username or email"
placeholder="*Username or email">
{identifier}
</TextInput>
</>
<>
<View style={styles.divider} />
<TextInput
onChangeText={text => setPassword(text)}
label="*Password"
placeholder="*Password"
secureTextEntry>
{password}
</TextInput>
</>
<>
<View style={styles.divider} />
<Button
loading={loading}
disabled={loading}
style={styles.btn}
onPress={() => authenticateUser()}
mode="contained">
Login
</Button>
<View style={styles.divider} />
<View style={styles.divider} />
</>
<>
{/**
* We use a portal component to render
* the snackbar on top of everything else
* */}
<Portal>
<Snackbar visible={visible} onDismiss={() => setVisible(false)}>
{error}
</Snackbar>
</Portal>
</>
</View>
);
};
const styles = StyleSheet.create({
base: {
flex: 1,
paddingLeft: 16,
paddingRight: 16,
alignContent: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff',
},
divider: {
height: 16,
},
headline: {
fontSize: 30,
},
appDesc: {
textAlign: 'center',
},
header: {
padding: 32,
},
appTitle: {
textAlign: 'center',
fontSize: 35,
lineHeight: 35,
fontWeight: '700',
},
btn: {
height: 50,
paddingTop: 6,
},
});
export default Login;
Попробуйте войти в систему под пользователем Strapi, и вы попадете прямо на страницу обзора. Закройте приложение, откройте его снова, и вы увидите, что попали прямо на экран обзора. Это происходит благодаря тому, что redux-persist загружает ваше сохраненное состояние и передает его нашему SwitchNavigator в Authentication.js
.
Кодирование и стилизация экрана обзора
Знаете ли вы, что является одной из величайших особенностей мобильной разработки? Бесконечные списки! Мы собираемся создать список, который будет создан для нашего приложения. Поскольку длина такого списка не определена, количество возможных макетов тоже.
Компонент списка
Давайте начнем с нашего компонента списка, для чего создадим новый файл ./src/components/TodoList.js
и вставим в него следующее:
// TodoList.js
import React from 'react';
import {View, StyleSheet} from 'react-native';
import {
Text,
IconButton,
ActivityIndicator,
Button,
Portal,
Dialog,
Paragraph,
TextInput,
HelperText,
Divider,
} from 'react-native-paper';
import {FlatList} from 'react-native-gesture-handler';
import {store} from '../../redux/Store';
import TodoView from '../../app/views/TodoView';
import TodoModel from '../../app/models/TodoModel';
/**
* the footer also acts as the load more
* indicator.
*/
export const TodoFooter = props => {
return (
<>
{props.shouldLoadMore ? (
<View style={styles.loaderView}>
<ActivityIndicator animating />
</View>
) : null}
</>
);
};
/**
* This is our header for the list that also
* includes the todo.add action.
*/
export const TodoHeader = props => {
const [error, setError] = React.useState('');
const [title, setTitle] = React.useState('');
const [visible, setVisible] = React.useState(false);
const [description, setDescription] = React.useState('');
const createTodoFromDialog = async () => {
if (title.length === 0 || description.length === 0) {
setError('Title and description are required.');
return;
}
const user = store.getState().user;
const todo = new TodoModel(user, title, description);
try {
await todo.save();
} catch (err) {
setError(err.message);
}
props.addTodo(todo);
};
return (
<View style={styles.header}>
<Text style={styles.text}>{props.text || "Your to do's"}</Text>
<View style={styles.buttonFrame}>
{!props.text ? (
<Button
onPress={() => setVisible(true)}
style=
mode="outlined">
Add a todo
</Button>
) : null}
</View>
<Portal>
<Dialog visible={visible} onDismiss={() => setVisible(false)}>
<Dialog.Title>Create a new todo</Dialog.Title>
<Dialog.Content>
<Paragraph>
Adding a new todo will save to in Strapi so you can use it later.
</Paragraph>
<View style={styles.divider} />
<TextInput
label="title"
placeholder="title"
onChangeText={text => {
setTitle(text);
setError(false);
}}>
{title}
</TextInput>
<View style={styles.divider} />
<TextInput
label="description"
placeholder="description"
multiline={true}
numberOfLines={4}
onChangeText={text => {
setDescription(text);
setError(false);
}}>
{description}
</TextInput>
<HelperText type="error">{error}</HelperText>
</Dialog.Content>
<Dialog.Actions>
<Button
onPress={() => {
setVisible(false);
setTitle('');
setDescription('');
setError('');
}}>
Cancel
</Button>
<Button onPress={() => createTodoFromDialog()}>Add</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</View>
);
};
/**
* in case no todos were fetched on initial fetch
* we can assume that there are none for this specific
* user.
*/
export const EmptyTodo = props => {
const [error, setError] = React.useState('');
const [title, setTitle] = React.useState('');
const [visible, setVisible] = React.useState(false);
const [description, setDescription] = React.useState('');
const createTodoFromDialog = async () => {
if (title.length === 0 || description.length === 0) {
setError('Title and description are required.');
return;
}
const user = store.getState().user;
const todo = new TodoModel(user, title, description);
try {
await todo.save();
} catch (err) {
setError(err.message);
}
props.addTodo(todo);
};
return (
<View style={styles.emptyBase}>
<TodoHeader text={'Pretty empty here ..'} />
<Button
onPress={() => setVisible(true)}
style={styles.btn}
mode="contained">
Create a new todo
</Button>
<Portal>
<Dialog visible={visible} onDismiss={() => setVisible(false)}>
<Dialog.Title>Create a new todo</Dialog.Title>
<Dialog.Content>
<Paragraph>
Adding a new todo will save to in Strapi so you can use it later.
</Paragraph>
<View style={styles.divider} />
<TextInput
label="title"
placeholder="title"
onChangeText={text => {
setTitle(text);
setError(false);
}}>
{title}
</TextInput>
<View style={styles.divider} />
<TextInput
label="description"
placeholder="description"
multiline={true}
numberOfLines={4}
onChangeText={text => {
setDescription(text);
setError(false);
}}>
{description}
</TextInput>
<HelperText type="error">{error}</HelperText>
</Dialog.Content>
<Dialog.Actions>
<Button
onPress={() => {
setVisible(false);
setTitle('');
setDescription('');
setError('');
}}>
Cancel
</Button>
<Button onPress={() => createTodoFromDialog()}>Add</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</View>
);
};
/**
* the main list component holding all of the loading
* and pagination logic.
*/
export const TodoList = props => {
const [data, setData] = React.useState([]);
const [limit] = React.useState(10);
const [start, setStart] = React.useState(0);
const [loading, setLoading] = React.useState(true);
const [loadingMore, setLoadingMore] = React.useState(true);
const [shouldLoadMore, setShouldLoadMore] = React.useState(true);
/**
* get the data from the server in a paginated manner
*
* 1. should no data be present start the normal loading
* animation.
*
* 2. should data be present start the loading more
* animation.
*/
const getTodosForUser = React.useCallback(async () => {
if (!shouldLoadMore) {
return;
}
if (!loading && data.length === 0) {
setLoading(true);
}
if (!loadingMore && data.length > 0) {
setLoadingMore(true);
}
const url = `http://192.168.0.57:1337/todos?_start=${start}&_limit=${limit}`;
const jwt = store.getState().jwt;
const response = await fetch(url, {
headers: {Authorization: `Bearer ${jwt}`},
});
const json = await response.json();
if (json.length < 10) {
setShouldLoadMore(false);
} else {
setStart(start + limit);
}
setData([...data, ...json]);
setLoading(false);
setLoadingMore(false);
}, [data, limit, loading, loadingMore, shouldLoadMore, start]);
/**
* saves a new todo to the server by creating a new TodoModel
* from the dialog data and calling Todo.save()
*/
const addTodo = todo => {
const {title, description, finished, user, id} = todo;
setData([...data, ...[{title, description, finished, user, id}]]);
};
/**
* callback method for the todo view. Deletes a todo from the list
* after it has been deleted from the server.
*/
const removeTodo = id => {
setData(data.filter(item => item.id !== id));
};
React.useEffect(() => {
getTodosForUser();
}, [getTodosForUser]);
if (loading) {
return (
<View style={styles.loaderBase}>
<ActivityIndicator animating size="large" />
</View>
);
}
if (!shouldLoadMore && !loading && !loadingMore && data.length === 0) {
return <EmptyTodo addTodo={addTodo} />;
}
return (
<>
<FlatList
style={styles.base}
data={data}
ItemSeparatorComponent={() => <Divider />}
ListHeaderComponent={() => <TodoHeader addTodo={addTodo} />}
ListFooterComponent={() => (
<TodoFooter shouldLoadMore={shouldLoadMore} />
)}
onEndReachedThreshold={0.5}
onEndReached={() => getTodosForUser()}
renderItem={({item, index}) => (
<TodoView removeTodo={removeTodo} item={item} index={index} />
)}
/>
</>
);
};
const styles = StyleSheet.create({
base: {
flex: 1,
backgroundColor: '#fff',
},
emptyBase: {
flex: 1,
backgroundColor: '#fff',
},
text: {
fontSize: 35,
lineHeight: 35,
fontWeight: '700',
padding: 32,
paddingLeft: 16,
},
header: {
flexDirection: 'row',
alignContent: 'center',
},
btn: {
height: 50,
paddingTop: 6,
marginLeft: 16,
marginRight: 16,
},
loaderBase: {
padding: 16,
alignContent: 'center',
justifyContent: 'center',
flex: 1,
},
divider: {
height: 16,
},
buttonFrame: {
justifyContent: 'center',
},
});
Компонент просмотра
Теперь, когда мы создали наш список, нам осталось сделать еще один шаг до завершения нашего приложения, и это — представление, которое будет повторно использоваться для каждого отдельного дочернего элемента набора данных.
Создайте файл ./src/app/views/TodoView.js
, содержащий следующий код:
// TodoView.js
import React from 'react';
import {StyleSheet, View} from 'react-native';
import {
List,
Colors,
Portal,
Dialog,
Paragraph,
TextInput,
HelperText,
Button,
Checkbox,
} from 'react-native-paper';
import TodoModel from '../models/TodoModel';
import {store} from '../../redux/Store';
export const TodoView = props => {
const {
title: passedPropsTitle,
description: passedPropsDesc,
finished: passedPropsFinished,
id,
} = props.item;
const [passedTitle, setPassedTitle] = React.useState(passedPropsTitle);
const [passedDesc, setPassedDesc] = React.useState(passedPropsDesc);
const [passedFinished, setPassedFinished] = React.useState(
passedPropsFinished,
);
const [error, setError] = React.useState('');
const [title, setTitle] = React.useState(passedTitle);
const [visible, setVisible] = React.useState(false);
const [description, setDescription] = React.useState(passedDesc);
const [finished, setFinished] = React.useState(passedFinished);
const editTodoFromDialog = async () => {
if (title.length === 0 || description.length === 0) {
setError('Title and description are required.');
return;
}
const user = store.getState().user;
const todo = new TodoModel(user, title, description, finished, id);
try {
await todo.edit();
} catch (err) {
setError(err.message);
return;
}
setPassedTitle(title);
setPassedDesc(description);
setPassedFinished(finished);
setVisible(false);
};
const deleteTodoFromDialog = () => {
const user = store.getState().user;
const todo = new TodoModel(user, title, description, finished, id);
try {
todo.dismiss();
} catch (err) {
setError(err.message);
return;
}
setVisible(false);
props.removeTodo(id);
};
return (
<>
<List.Item
onPress={() => {
setVisible(true);
}}
title={passedTitle}
description={passedDesc}
right={pprops => {
if (passedFinished) {
return (
<List.Icon
{...pprops}
color={Colors.green300}
icon="check-circle"
/>
);
}
return null;
}}
/>
<Portal>
<Dialog visible={visible} onDismiss={() => setVisible(false)}>
<Dialog.Title>Edit your todo</Dialog.Title>
<Dialog.Content>
<Paragraph>
Editing your todo will also change it in Strapi.
</Paragraph>
<View style={styles.divider} />
<TextInput
label="title"
placeholder="title"
onChangeText={text => {
setTitle(text);
setError(false);
}}>
{title}
</TextInput>
<View style={styles.divider} />
<TextInput
label="description"
placeholder="description"
multiline={true}
numberOfLines={4}
onChangeText={text => {
setDescription(text);
setError(false);
}}>
{description}
</TextInput>
<HelperText type="error">{error}</HelperText>
{error.length > 0 ? <View style={styles.divider} /> : null}
<View
style={{
flexDirection: 'row',
alignContent: 'center',
}}>
<Checkbox
status={finished ? 'checked' : 'unchecked'}
onPress={() => {
setFinished(!finished);
}}
/>
<Paragraph style=>
Finished
</Paragraph>
</View>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={() => deleteTodoFromDialog()}>delete</Button>
<View style=/>
<Button
onPress={() => {
setVisible(false);
setError('');
}}>
Cancel
</Button>
<Button onPress={() => editTodoFromDialog()}>Save</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</>
);
};
const styles = StyleSheet.create({
divider: {
height: 16,
},
});
export default TodoView;
Наконец, включите представления в созданный ранее экран обзора.
// Overview.js
import React from 'react';
import {StatusBar} from 'react-native';
import {TodoList} from '../components/lists/TodoList';
const Overview = props => {
return (
<>
<StatusBar backgroundColor="#ffffff" barStyle="dark-content" />
<TodoList />
</>
);
};
export default Overview;
Резюме
Мы создали мобильное приложение, которое поддерживает создание, редактирование, удаление дел на основе пользователя и может отображать их в виде постраничного списка, который всегда обновляется в соответствии с данными на сервере и, таким образом, синхронизируется на всех устройствах.
[