Как создать React-Native приложение с Strapi в качестве бэкенда

В этой статье рассказывается о том, как создать React-Native приложение с использованием Strapi в качестве бэкенда.

Автор учебника: Chigozie Oduah

Strapi — это безголовая CMS, которая позволяет легко создавать настраиваемые бэкенд-сервисы. Вы можете интегрировать приложения Strapi с любым типом фронтенда и развернуть приложение в облаке.

Это руководство проведет вас через процесс создания простого React-native приложения для выполнения дел с использованием Strapi в качестве бэкенда. Вы сможете создавать, редактировать и удалять дела по каждому пользователю, взаимодействуя с REST API Strapi.

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

Пример приложения доступен на Github.

Настройка бэкенда

Для начала нам нужно создать проект 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 приложению взаимодействовать с коллекцией. Чтобы зарегистрировать конечные точки, нам нужно:

  1. Перейти в Настройки → Пользователи и плагин разрешений → Роли.
  2. Нажмите на Authenticated.
  3. Переключите выпадающий список Todo в разделе Permissions.
  4. Поставьте галочку Select All (Выбрать все).
  5. Нажмите Сохранить.

Создание мобильного приложения

Теперь, когда мы настроили наш 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, которую мы создали ранее, поля должны быть точно такими же.

  1. Создайте файл ./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;
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Сделайте то же самое для нашего 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;
Войти в полноэкранный режим Выйти из полноэкранного режима

Резюме

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

Оцените статью
devanswers.ru
Добавить комментарий