Статьи

Создайте автономное приложение React Native с помощью WatermelonDB

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

Другие приложения, такие как клоны Twitter, получают данные с сервера и напрямую показывают их пользователю. Они поддерживают кэш данных и, если пользователю необходимо взаимодействовать с каким-либо документом, они напрямую вызывают API.

Так что не все приложения требуют базы данных.

Хотите узнать React Native с нуля? Эта статья является выдержкой из нашей Премиум библиотеки. Получите полную коллекцию книг React Native, охватывающую основы, проекты, советы и инструменты и многое другое с SitePoint Premium. Присоединяйтесь сейчас всего за $ 9 / месяц .

Когда нам нужна база данных

Такие приложения, как Nozbe (приложение для ведения дел), Expense (средство отслеживания) и SplitWise (для покупок из приложения), должны работать в автономном режиме. И для этого им нужен способ локального хранения данных и их синхронизации с сервером. Этот тип приложения называется первым автономным приложением. Со временем эти приложения собирают много данных, и становится сложнее управлять этими данными напрямую, поэтому для эффективного управления необходима база данных.

Параметры в React Native

При разработке приложения выберите базу данных, которая наилучшим образом соответствует вашим требованиям. Если доступны два варианта, выберите тот, который имеет лучшую документацию и быстрее реагирует на проблемы. Ниже приведены некоторые из наиболее известных вариантов, доступных для React Native:

  • WatermelonDB : реактивная база данных с открытым исходным кодом, которую можно использовать с любой базой данных. По умолчанию он использует SQLite в качестве основной базы данных в React Native.
  • SQLite ( React Native , Expo ): самое старое, наиболее используемое, проверенное в бою и известное решение. Он доступен для большинства платформ, поэтому, если вы разработали приложение в другой среде разработки мобильных приложений, вы, возможно, уже знакомы с ним.
  • Realm ( React Native ): решение с открытым исходным кодом, но оно также имеет корпоративную версию со множеством других функций . Они проделали большую работу, и многие известные компании используют ее.
  • FireBase ( React Native , Expo ): сервис Google специально для платформы мобильной разработки. Он предлагает множество функций, хранилище является лишь одним из них. Но это требует от вас оставаться в их экосистеме, чтобы использовать его.
  • RxDB : база данных в реальном времени для Интернета. Он имеет хорошую документацию, хороший рейтинг на GitHub (> 9K звезд), а также реагирует.

Предпосылки

Я предполагаю, что у вас есть знания об основном React Native и процессе его сборки. Мы собираемся использовать response-native-cli для создания нашего приложения.

Я бы также предложил настроить среду разработки Android или iOS при настройке проекта, так как вы можете столкнуться со многими проблемами, и первым шагом в отладке является сохранение IDE (Android Studio или Xcode) открытым для просмотра журналов.

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

Чтобы настроить виртуальное или физическое устройство, выполните следующие действия.

Примечание: есть более удобный для JavaScript набор инструментов под названием Expo . Сообщество React Native также начало продвигать его, но я еще не сталкивался с крупномасштабным, готовым к работе приложением, которое использует Expo, и порт Expo в настоящее время недоступен для тех, кто использует базу данных, такую ​​как Realm, или в наш случай, арбузный дБ.

Требования к приложению

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

Приложение будет иметь три экрана .

На главной странице появятся две кнопки — одна для создания фиктивных записей, а вторая для добавления нового фильма. Под ним будет один поисковый ввод, который можно использовать для запроса названий фильмов из базы данных. Он покажет список фильмов под строкой поиска. При поиске по любому имени в списке будут отображаться только найденные фильмы.

вид на домашний экран

Нажатие на любой фильм откроет панель инструментов фильма , откуда можно будет проверить все его отзывы. Фильм можно редактировать или удалять, или на этом экране можно добавить новый отзыв.

панель инструментов фильма

Третий экран будет Форма Фильма , которая используется для создания / обновления фильма.

форма фильма

Исходный код доступен на GitHub .

Почему мы выбрали WatermelonDB (особенности)

Нам нужно создать автономное первое приложение, поэтому база данных обязательна.

Особенности WatermelonDB

Давайте посмотрим на некоторые особенности WatermelonDB.

Полностью наблюдаемый
Отличительной особенностью WatermelonDB является его реактивный характер. Любой объект можно наблюдать с помощью наблюдаемых, и он будет автоматически перерисовывать наши компоненты при изменении данных. Нам не нужно прилагать никаких дополнительных усилий для использования WatermelonDB. Мы обертываем простые компоненты React и улучшаем их, чтобы сделать их реактивными. По моему опыту, это просто работает без проблем , и нам больше не нужно заботиться ни о чем другом. Мы вносим изменения в объект, и наша работа выполнена! Он сохраняется и обновляется во всех местах приложения.

SQLite под капотом для React Native
В современном браузере сборка точно в срок используется для повышения скорости, но она недоступна на мобильных устройствах. Кроме того, аппаратное обеспечение в мобильных устройствах медленнее, чем в компьютерах. Из-за всех этих факторов приложения JavaScript работают медленнее в мобильном приложении. Чтобы преодолеть это, WatermelonDB не получает ничего, пока это не нужно. Он использует ленивую загрузку и SQLite в качестве базовой базы данных в отдельном потоке для обеспечения быстрого ответа.

Синхронизировать примитивы и адаптер синхронизации
Хотя WatermelonDB — это просто локальная база данных, она также предоставляет примитивы синхронизации и адаптеры синхронизации. Это делает его довольно простым в использовании с любой из наших собственных баз данных. Нам просто нужно соответствовать протоколу синхронизации WatermelonDB на внутренней стороне и предоставить конечные точки.

Другие функции включают в себя:

  • Статически напечатано с использованием Flow
  • Доступно для всех платформ

Настройка Dev Env и WatermelonDB (v0.0)

Мы собираемся использовать react-native-cli для создания нашего приложения.

Примечание: вы можете использовать его вместе с ExpoKit или Ejecting from Expo.

Если вы хотите пропустить эту часть, v0.0 и v0.0 ветку v0.0 .

Начать новый проект:

 react-native init MovieDirectory cd MovieDirectory 

Установить зависимости:

 npm i @nozbe/watermelondb @nozbe/with-observables react-navigation react-native-gesture-handler react-native-fullwidth-image native-base rambdax 

Ниже приведен список установленных зависимостей и их использование:

  • native-base : библиотека пользовательского интерфейса, которая будет использоваться для внешнего вида нашего приложения.
  • react-native-fullwidth-image : для отображения полноэкранных react-native-fullwidth-image изображений. (Иногда бывает сложно вычислить ширину, высоту, а также сохранить соотношение сторон. Поэтому лучше использовать существующее решение сообщества.)
  • @nozbe/watermelondb : база данных, которую мы будем использовать.
  • @nozbe/with-observables : содержит декораторы ( @ ), которые будут использоваться в наших моделях.
  • react-navigation : используется для управления маршрутами / экранами
  • react-native-gesture-handler : зависимость для react-navigation .
  • rambdax : используется для генерации случайного числа при создании фиктивных данных.

Откройте ваш package.json и замените scripts следующим кодом:

 "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", "start:ios": "react-native run-ios", "start:android": "react-native run-android", "test": "jest" } 

Это будет использоваться для запуска нашего приложения на соответствующем устройстве.

Настроить WatermelonDB

Нам нужно добавить плагин Babel для преобразования наших декораторов, поэтому установите его как зависимость dev:

 npm install -D @babel/plugin-proposal-decorators 

Создайте новый файл .babelrc в корне проекта:

 // .babelrc { "presets": ["module:metro-react-native-babel-preset"], "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] } 

Теперь используйте следующие руководства для вашей целевой среды:

Откройте папку Android в Android Studio и синхронизируйте проект. В противном случае он выдаст ошибку при первом запуске приложения. Сделайте то же самое, если вы ориентируетесь на iOS .

Перед тем, как запустить приложение, нам нужно связать пакет обработчика react-native-gesture , зависимость react-navigation и react-native-vector-icons , зависимость native-base . По умолчанию, чтобы сохранить бинарный размер приложения небольшим, React Native не содержит весь код для поддержки нативных функций. Поэтому, когда нам нужно использовать определенную функцию, мы можем использовать команду link для добавления собственных зависимостей. Итак, давайте свяжем наши зависимости:

 react-native link react-native-gesture-handler react-native link react-native-vector-icons 

Запустите приложение:

 npm run start:android # or npm run start:ios 

Если вы получили ошибку за отсутствующие зависимости, запустите npm i .

Код до здесь доступен в ветке v0.0 .

версия 0

Руководство

Поскольку мы будем создавать приложение базы данных, большая часть кода будет только бэкэндом, и мы не сможем многое увидеть во внешнем интерфейсе. Это может показаться длинным, но наберитесь терпения и следуйте инструкциям до конца. Вы не пожалеете об этом!

Рабочий процесс WatermelonDB можно разделить на три основные части:

  • Схема : используется для определения схемы таблицы базы данных.
  • Модели : сопоставленный объект ORM. Мы будем взаимодействовать с ними на протяжении всего нашего приложения.
  • Действия : используется для выполнения различных операций CRUD с нашим объектом / строкой. Мы можем напрямую выполнить действие, используя объект базы данных, или мы можем определить функции в нашей модели для выполнения этих действий. Определение их в моделях — лучшая практика, и мы собираемся использовать это только.

Давайте начнем с нашего приложения.

Инициализация схемы БД и WatermelonDB (v0.1)

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

Структура проекта

Создайте новую папку src в корне. Это будет корневая папка для всего нашего кода React Native. Папка models используется для всех наших файлов, связанных с базой данных. Он будет вести себя как наша папка DAO (Data Access Object). Этот термин используется для интерфейса с базой данных определенного типа или другим механизмом сохранения. В папке components будут все наши компоненты React. Папка screens будет иметь все экраны нашего приложения.

 mkdir src && cd src mkdir models mkdir components mkdir screens 

схема

Перейдите в папку models , создайте новый файл schema.js и используйте следующий код:

 // schema.js import { appSchema, tableSchema } from "@nozbe/watermelondb"; export const mySchema = appSchema({ version: 2, tables: [ tableSchema({ name: "movies", columns: [ { name: "title", type: "string" }, { name: "poster_image", type: "string" }, { name: "genre", type: "string" }, { name: "description", type: "string" }, { name: "release_date_at", type: "number" } ] }), tableSchema({ name: "reviews", columns: [ { name: "body", type: "string" }, { name: "movie_id", type: "string", isIndexed: true } ] }) ] }); 

Мы определили две таблицы — одну для фильмов, а другую для обзоров. Сам код говорит сам за себя. Обе таблицы имеют связанные столбцы.

Обратите внимание, что в соответствии с соглашением об именах WatermelonDB все идентификаторы заканчиваются суффиксом _id , а поле даты заканчивается суффиксом _at .

isIndexed используется для добавления индекса к столбцу. Индексирование ускоряет запросы по столбцу за счет небольшой скорости создания / обновления и размера базы данных. Мы будем запрашивать все отзывы по movie_id , поэтому мы должны пометить его как проиндексированный. Если вы хотите делать частые запросы к любому логическому столбцу, вы должны также проиндексировать его. Однако вы никогда не должны индексировать столбцы даты ( _at ).

модели

Создайте новый файл models/Movie.js и вставьте в него код:

 // models/Movie.js import { Model } from "@nozbe/watermelondb"; import { field, date, children } from "@nozbe/watermelondb/decorators"; export default class Movie extends Model { static table = "movies"; static associations = { reviews: { type: "has_many", foreignKey: "movie_id" } }; @field("title") title; @field("poster_image") posterImage; @field("genre") genre; @field("description") description; @date("release_date_at") releaseDateAt; @children("reviews") reviews; } 

Здесь мы сопоставили каждый столбец таблицы movies с каждой переменной. Обратите внимание, как мы сопоставили отзывы с фильмом. Мы определили это в ассоциациях и также использовали @children вместо @field . Каждый отзыв будет иметь внешний ключ movie_id . Эти значения внешнего ключа обзора сопоставляются с id в таблице movie чтобы связать модель обзоров с моделью фильма.

Также для даты нам нужно использовать декоратор @date чтобы WatermelonDB давал нам объект Date вместо простого числа.

Теперь создайте новый файл models/Review.js . Это будет использоваться для сопоставления каждого обзора фильма.

 // models/Review.js import { Model } from "@nozbe/watermelondb"; import { field, relation } from "@nozbe/watermelondb/decorators"; export default class Review extends Model { static table = "reviews"; static associations = { movie: { type: "belongs_to", key: "movie_id" } }; @field("body") body; @relation("movies", "movie_id") movie; } 

Мы создали все наши необходимые модели. Мы можем напрямую использовать их для инициализации нашей базы данных, но если мы хотим добавить новую модель, нам снова нужно внести изменения в место, где мы инициализируем базу данных. Чтобы преодолеть это, создайте новый файл models/index.js и добавьте следующий код:

 // models/index.js import Movie from "./Movie"; import Review from "./Review"; export const dbModels = [Movie, Review]; 

Таким образом, нам нужно только внести изменения в нашу папку models . Это делает нашу папку DAO более организованной.

Инициализировать базу данных

Теперь, чтобы использовать нашу схему и модели для инициализации нашей базы данных, откройте index.js , который должен находиться в корне нашего приложения. Добавьте код ниже:

 // index.js import { AppRegistry } from "react-native"; import App from "./App"; import { name as appName } from "./app.json"; import { Database } from "@nozbe/watermelondb"; import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite"; import { mySchema } from "./src/models/schema"; import { dbModels } from "./src/models/index.js"; // First, create the adapter to the underlying database: const adapter = new SQLiteAdapter({ dbName: "WatermelonDemo", schema: mySchema }); // Then, make a Watermelon database from it! const database = new Database({ adapter, modelClasses: dbModels }); AppRegistry.registerComponent(appName, () => App); 

Мы создаем адаптер, используя нашу схему для базовой базы данных. Затем мы передаем этот адаптер и наши dbModels для создания нового экземпляра базы данных.

На данный момент лучше проверить, нормально ли работает наше приложение или нет. Итак, запустите ваше приложение и проверьте:

 npm run start:android # or npm run start:ios 

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

Весь код до этой части находится под веткой v0.1 .

Добавить действия и фиктивный генератор данных (v0.2)

Давайте добавим некоторые фиктивные данные в наше приложение.

действия

Для выполнения операций CRUD мы собираемся создать некоторые действия. Откройте models/Movie.js и models/Review.js и обновите их, как models/Review.js ниже:

 // models/Movie.js import { Model } from "@nozbe/watermelondb"; import { field, date, children } from "@nozbe/watermelondb/decorators"; export default class Movie extends Model { static table = "movies"; static associations = { reviews: { type: "has_many", foreignKey: "movie_id" } }; @field("title") title; @field("poster_image") posterImage; @field("genre") genre; @field("description") description; @date("release_date_at") releaseDateAt; @children("reviews") reviews; // add these: getMovie() { return { title: this.title, posterImage: this.posterImage, genre: this.genre, description: this.description, releaseDateAt: this.releaseDateAt }; } async addReview(body) { return this.collections.get("reviews").create(review => { review.movie.set(this); review.body = body; }); } updateMovie = async updatedMovie => { await this.update(movie => { movie.title = updatedMovie.title; movie.genre = updatedMovie.genre; movie.posterImage = updatedMovie.posterImage; movie.description = updatedMovie.description; movie.releaseDateAt = updatedMovie.releaseDateAt; }); }; async deleteAllReview() { await this.reviews.destroyAllPermanently(); } async deleteMovie() { await this.deleteAllReview(); // delete all reviews first await this.markAsDeleted(); // syncable await this.destroyPermanently(); // permanent } } 
 // models/Review.js import { Model } from "@nozbe/watermelondb"; import { field, relation } from "@nozbe/watermelondb/decorators"; export default class Review extends Model { static table = "reviews"; static associations = { movie: { type: "belongs_to", key: "movie_id" } }; @field("body") body; @relation("movies", "movie_id") movie; // add these: async deleteReview() { await this.markAsDeleted(); // syncable await this.destroyPermanently(); // permanent } } 

Мы собираемся использовать все функции, определенные для операций обновления и удаления. У нас не будет объекта модели при создании, поэтому мы будем напрямую использовать объект базы данных для создания новых строк.

Создайте два файла, models/generate.js и models/randomData.js . generate.js будет использоваться для создания функции generateRecords которая будет генерировать фиктивные записи. randomData.js содержит различные массивы с фиктивными данными, которые используются в generate.js для генерации фиктивных записей.

 // models/generate.js import { times } from "rambdax"; import { movieNames, movieGenre, moviePoster, movieDescription, reviewBodies } from "./randomData"; const flatMap = (fn, arr) => arr.map(fn).reduce((a, b) => a.concat(b), []); const fuzzCount = count => { // Makes the number randomly a little larger or smaller for fake data to seem more realistic const maxFuzz = 4; const fuzz = Math.round((Math.random() - 0.5) * maxFuzz * 2); return count + fuzz; }; const makeMovie = (db, i) => { return db.collections.get("movies").prepareCreate(movie => { movie.title = movieNames[i % movieNames.length] + " " + (i + 1) || movie.id; movie.genre = movieGenre[i % movieGenre.length]; movie.posterImage = moviePoster[i % moviePoster.length]; movie.description = movieDescription; movie.releaseDateAt = new Date().getTime(); }); }; const makeReview = (db, movie, i) => { return db.collections.get("reviews").prepareCreate(review => { review.body = reviewBodies[i % reviewBodies.length] || `review#${review.id}`; review.movie.set(movie); }); }; const makeReviews = (db, movie, count) => times(i => makeReview(db, movie, i), count); // Generates dummy random records. Accepts db object, no. of movies, and no. of reviews for each movie to generate. const generate = async (db, movieCount, reviewsPerPost) => { await db.action(() => db.unsafeResetDatabase()); const movies = times(i => makeMovie(db, i), movieCount); const reviews = flatMap( movie => makeReviews(db, movie, fuzzCount(reviewsPerPost)), movies ); const allRecords = [...movies, ...reviews]; await db.batch(...allRecords); return allRecords.length; }; // Generates 100 movies with up to 10 reviews export async function generateRecords(database) { return generate(database, 100, 10); } 
 // models/randomData.js export const movieNames = [ "The Shawshank Redemption", "The Godfather", "The Dark Knight", "12 Angry Men" ]; export const movieGenre = [ "Action", "Comedy", "Romantic", "Thriller", "Fantasy" ]; export const moviePoster = [ "https://m.media-amazon.com/images/M/MV5BMDFkYTc0MGEtZmNhMC00ZDIzLWFmNTEtODM1ZmRlYWMwMWFmXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_UX182_CR0,0,182,268_AL__QL50.jpg", "https://m.media-amazon.com/images/M/MV5BM2MyNjYxNmUtYTAwNi00MTYxLWJmNWYtYzZlODY3ZTk3OTFlXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_UY268_CR3,0,182,268_AL__QL50.jpg", "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_UX182_CR0,0,182,268_AL__QL50.jpg", "https://m.media-amazon.com/images/M/MV5BMWU4N2FjNzYtNTVkNC00NzQ0LTg0MjAtYTJlMjFhNGUxZDFmXkEyXkFqcGdeQXVyNjc1NTYyMjg@._V1_UX182_CR0,0,182,268_AL__QL50.jpg" ]; export const movieDescription = "Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna. Vestibulum dapibus, mauris nec malesuada fames ac turpis velit, rhoncus eu, luctus et interdum adipiscing wisi. Aliquam erat ac ipsum. Integer aliquam purus. Quisque lorem tortor fringilla sed, vestibulum id, eleifend justo vel bibendum sapien massa ac turpis faucibus orci luctus non, consectetuer lobortis quis, varius in, purus. Integer ultrices posuere cubilia Curae, Nulla ipsum dolor lacus, suscipit adipiscing. Cum sociis natoque penatibus et ultrices volutpat."; export const reviewBodies = [ "First!!!!", "Cool!", "Why dont you just…", "Maybe useless, but the article is extremely interesting and easy to read. One can definitely try to read it.", "Seriously one of the coolest projects going on right now", "I think the easiest way is just to write a back end that emits .NET IR since infra is already there.", "Open source?", "This article is obviously wrong", "Just Stupid", "The general public won't care", "This is my bear case for Google.", "All true, but as a potential advertiser you don't really get to use all that targeting when placing ads", "I wonder what work environment exists, that would cause a worker to hide their mistakes and endanger the crew, instead of reporting it. And how many more mistakes go unreported? I hope Russia addresses the root issue, and not just fires the person responsible." ]; 

Теперь нам нужно вызвать функцию generateRecords для генерации фиктивных данных.

Мы будем использовать react-navigation для создания маршрутов. Откройте index.js из корня и используйте следующий код:

 // index.js import { AppRegistry } from "react-native"; import { name as appName } from "./app.json"; import { Database } from "@nozbe/watermelondb"; import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite"; import { mySchema } from "./src/models/schema"; import { dbModels } from "./src/models/index.js"; // Added new import import { createNavigation } from "./src/screens/Navigation"; // First, create the adapter to the underlying database: const adapter = new SQLiteAdapter({ dbName: "WatermelonDemo", schema: mySchema }); // Then, make a Watermelon database from it! const database = new Database({ adapter, modelClasses: dbModels }); // Change these: const Navigation = createNavigation({ database }); AppRegistry.registerComponent(appName, () => Navigation); 

Мы используем функцию createNavigation , но у нас ее сейчас нет, поэтому давайте создадим ее. Создайте src/screens/Navigation.js и используйте следующий код:

 // screens/Navigation.js import React from "react"; import { createStackNavigator, createAppContainer } from "react-navigation"; import Root from "./Root"; export const createNavigation = props => createAppContainer( createStackNavigator( { Root: { // We have to use a little wrapper because React Navigation doesn't pass simple props (and withObservables needs that) screen: ({ navigation }) => { const { database } = props; return <Root database={database} navigation={navigation} />; }, navigationOptions: { title: "Movies" } } }, { initialRouteName: "Root", initialRouteParams: props } ) ); 

Мы используем Root в качестве первого экрана, поэтому давайте создадим screens/Root.js и используем следующий код:

 // screens/Root.js import React, { Component } from "react"; import { generateRecords } from "../models/generate"; import { Alert } from "react-native"; import { Container, Content, Button, Text } from "native-base"; import MovieList from "../components/MovieList"; export default class Root extends Component { state = { isGenerating: false }; generate = async () => { this.setState({ isGenerating: true }); const count = await generateRecords(this.props.database); Alert.alert(`Generated ${count} records!`); this.setState({ isGenerating: false }); }; render() { const { isGenerating } = this.state; const { database, navigation } = this.props; return ( <Container> <Content> <Button bordered full onPress={this.generate} style={{ marginTop: 5 }} > <Text>Generate Dummy records</Text> </Button> {!isGenerating && ( <MovieList database={database} search="" navigation={navigation} /> )} </Content> </Container> ); } } 

Мы использовали MovieList чтобы показать список сгенерированных фильмов. Давайте создадим это. Создайте новый файл src/components/MovieList.js как src/components/MovieList.js ниже:

 // components/MovieList.js import React from "react"; import { Q } from "@nozbe/watermelondb"; import withObservables from "@nozbe/with-observables"; import { List, ListItem, Body, Text } from "native-base"; const MovieList = ({ movies }) => ( <List> {movies.map(movie => ( <ListItem key={movie.id}> <Body> <Text>{movie.title}</Text> </Body> </ListItem> ))} </List> ); // withObservables is HOC(Higher Order Component) to make any React component reactive. const enhance = withObservables(["search"], ({ database, search }) => ({ movies: database.collections .get("movies") .query(Q.where("title", Q.like(`%${Q.sanitizeLikeString(search)}%`))) })); export default enhance(MovieList); 

MovieList — это простой компонент React, отображающий список фильмов, но withObservables внимание на enhance , withObservables . withObservables — это HOC (компонент более withObservables порядка), чтобы сделать любой компонент React реактивным в WatermelonDB. Если мы изменим значение фильма в любом месте нашего приложения, оно будет перерисовано, чтобы отразить изменения. Второй аргумент ({ database, search }) содержит реквизиты компонента. search передается из Root.js и database передается из Navigation.js . Первый аргумент ["search"] — это список реквизитов, которые запускают повторный запуск наблюдения. Таким образом, если search меняется, наши наблюдаемые объекты пересчитываются и снова наблюдаются. В функции мы используем объект database чтобы получить коллекцию фильмов, где title похож на пропущенный search . Специальные символы, такие как % и _ , не экранируются автоматически, поэтому всегда рекомендуется использовать очищенный пользовательский ввод.

Откройте вашу Android Studio или Xcode, чтобы синхронизировать проект, а затем запустите приложение. Нажмите на кнопку GENERATE DUMMY RECORDS . Он сгенерирует фиктивные данные и покажет вам список.

 npm run start:android # or npm run start:ios 

Этот код доступен в ветке v0.2 .

версия 0.2

Добавить все операции CRUD (v1)

Давайте теперь добавим функциональность для создания / обновления / удаления фильмов и обзоров. Мы добавим новую кнопку, чтобы добавить новый фильм, а также создадим TextInput для передачи поискового ключевого слова в запрос. Итак, откройте Root.js и измените его содержимое, как Root.js ниже:

 // screens/Root.js import React, { Component } from "react"; import { generateRecords } from "../models/generate"; import { Alert } from "react-native"; import { View, Container, Content, Button, Text, Form, Item, Input, Label, Body } from "native-base"; import MovieList from "../components/MovieList"; import styles from "../components/styles"; export default class Root extends Component { state = { isGenerating: false, search: "", isSearchFocused: false }; generate = async () => { this.setState({ isGenerating: true }); const count = await generateRecords(this.props.database); Alert.alert(`Generated ${count} records!`); this.setState({ isGenerating: false }); }; // add these: addNewMovie = () => { this.props.navigation.navigate("NewMovie"); }; handleTextChanges = v => this.setState({ search: v }); handleOnFocus = () => this.setState({ isSearchFocused: true }); handleOnBlur = () => this.setState({ isSearchFocused: false }); render() { const { search, isGenerating, isSearchFocused } = this.state; const { database, navigation } = this.props; return ( <Container style={styles.container}> <Content> {!isSearchFocused && ( <View style={styles.marginContainer}> <Button bordered full onPress={this.generate} style={{ marginTop: 5 }} > <Text>Generate Dummy records</Text> </Button> {/* add these: */} <Button bordered full onPress={this.addNewMovie} style={{ marginTop: 5 }} > <Text>Add new movie</Text> </Button> <Body /> </View> )} {/* add these: */} <Form> <Item floatingLabel> <Label>Search...</Label> <Input onFocus={this.handleOnFocus} onBlur={this.handleOnBlur} onChangeText={this.handleTextChanges} /> </Item> </Form> {!isGenerating && ( <MovieList database={database} search={search} navigation={navigation} /> )} </Content> </Container> ); } } 

Мы создадим новый экран MovieForm.js , а также MovieForm.js использовать этот же компонент для редактирования фильма. Заметьте, что мы просто вызываем метод handleSubmit , который, в свою очередь, вызывает handleAddNewMovie или handleUpdateMovie . handleUpdateMovie вызывает действие, которое мы определили ранее в нашей модели Movie . Вот и все. Это позаботится о сохранении этого и обновлении везде. Используйте следующий код для MovieForm.js :

 // screens/MovieForm.js import React, { Component } from "react"; import { View, Button, Container, Content, Form, Item, Input, Label, Textarea, Picker, Body, Text, DatePicker } from "native-base"; import { movieGenre } from "../models/randomData"; class MovieForm extends Component { constructor(props) { super(props); if (props.movie) { this.state = { ...props.movie.getMovie() }; } else { this.state = {}; } } render() { return ( <Container> <Content> <Form> <Item floatingLabel> <Label>Title</Label> <Input onChangeText={title => this.setState({ title })} value={this.state.title} /> </Item> <View style={{ paddingLeft: 15 }}> <Item picker> <Picker mode="dropdown" style={{ width: undefined, paddingLeft: 15 }} placeholder="Genre" placeholderStyle={{ color: "#bfc6ea" }} placeholderIconColor="#007aff" selectedValue={this.state.genre} onValueChange={genre => this.setState({ genre })} > {movieGenre.map((genre, i) => ( <Picker.Item key={i} label={genre} value={genre} /> ))} </Picker> </Item> </View> <Item floatingLabel> <Label>Poster Image</Label> <Input onChangeText={posterImage => this.setState({ posterImage })} value={this.state.posterImage} /> </Item> <View style={{ paddingLeft: 15, marginTop: 15 }}> <Text style={{ color: "gray" }}>Release Date</Text> <DatePicker locale={"en"} animationType={"fade"} androidMode={"default"} placeHolderText="Change Date" defaultDate={new Date()} onDateChange={releaseDateAt => this.setState({ releaseDateAt })} /> <Text> {this.state.releaseDateAt && this.state.releaseDateAt.toString().substr(4, 12)} </Text> <Text style={{ color: "gray", marginTop: 15 }}>Description</Text> <Textarea rowSpan={5} bordered placeholder="Description..." onChangeText={description => this.setState({ description })} value={this.state.description} /> </View> {!this.props.movie && ( <View style={{ paddingLeft: 15, marginTop: 15 }}> <Text style={{ color: "gray" }}>Review</Text> <Textarea rowSpan={5} bordered placeholder="Review..." onChangeText={review => this.setState({ review })} value={this.state.review} /> </View> )} <Body> <Button onPress={this.handleSubmit}> <Text>{this.props.movie ? "Update " : "Add "} Movie</Text> </Button> </Body> </Form> </Content> </Container> ); } handleSubmit = () => { if (this.props.movie) { this.handleUpdateMovie(); } else { this.handleAddNewMovie(); } }; handleAddNewMovie = async () => { const { database } = this.props; const movies = database.collections.get("movies"); const newMovie = await movies.create(movie => { movie.title = this.state.title; movie.genre = this.state.genre; movie.posterImage = this.state.posterImage; movie.description = this.state.description; movie.releaseDateAt = this.state.releaseDateAt.getTime(); }); this.props.navigation.goBack(); }; handleUpdateMovie = async () => { const { movie } = this.props; await movie.updateMovie({ title: this.state.title, genre: this.state.genre, posterImage: this.state.posterImage, description: this.state.description, releaseDateAt: this.state.releaseDateAt.getTime() }); this.props.navigation.goBack(); }; } export default MovieForm; по // screens/MovieForm.js import React, { Component } from "react"; import { View, Button, Container, Content, Form, Item, Input, Label, Textarea, Picker, Body, Text, DatePicker } from "native-base"; import { movieGenre } from "../models/randomData"; class MovieForm extends Component { constructor(props) { super(props); if (props.movie) { this.state = { ...props.movie.getMovie() }; } else { this.state = {}; } } render() { return ( <Container> <Content> <Form> <Item floatingLabel> <Label>Title</Label> <Input onChangeText={title => this.setState({ title })} value={this.state.title} /> </Item> <View style={{ paddingLeft: 15 }}> <Item picker> <Picker mode="dropdown" style={{ width: undefined, paddingLeft: 15 }} placeholder="Genre" placeholderStyle={{ color: "#bfc6ea" }} placeholderIconColor="#007aff" selectedValue={this.state.genre} onValueChange={genre => this.setState({ genre })} > {movieGenre.map((genre, i) => ( <Picker.Item key={i} label={genre} value={genre} /> ))} </Picker> </Item> </View> <Item floatingLabel> <Label>Poster Image</Label> <Input onChangeText={posterImage => this.setState({ posterImage })} value={this.state.posterImage} /> </Item> <View style={{ paddingLeft: 15, marginTop: 15 }}> <Text style={{ color: "gray" }}>Release Date</Text> <DatePicker locale={"en"} animationType={"fade"} androidMode={"default"} placeHolderText="Change Date" defaultDate={new Date()} onDateChange={releaseDateAt => this.setState({ releaseDateAt })} /> <Text> {this.state.releaseDateAt && this.state.releaseDateAt.toString().substr(4, 12)} </Text> <Text style={{ color: "gray", marginTop: 15 }}>Description</Text> <Textarea rowSpan={5} bordered placeholder="Description..." onChangeText={description => this.setState({ description })} value={this.state.description} /> </View> {!this.props.movie && ( <View style={{ paddingLeft: 15, marginTop: 15 }}> <Text style={{ color: "gray" }}>Review</Text> <Textarea rowSpan={5} bordered placeholder="Review..." onChangeText={review => this.setState({ review })} value={this.state.review} /> </View> )} <Body> <Button onPress={this.handleSubmit}> <Text>{this.props.movie ? "Update " : "Add "} Movie</Text> </Button> </Body> </Form> </Content> </Container> ); } handleSubmit = () => { if (this.props.movie) { this.handleUpdateMovie(); } else { this.handleAddNewMovie(); } }; handleAddNewMovie = async () => { const { database } = this.props; const movies = database.collections.get("movies"); const newMovie = await movies.create(movie => { movie.title = this.state.title; movie.genre = this.state.genre; movie.posterImage = this.state.posterImage; movie.description = this.state.description; movie.releaseDateAt = this.state.releaseDateAt.getTime(); }); this.props.navigation.goBack(); }; handleUpdateMovie = async () => { const { movie } = this.props; await movie.updateMovie({ title: this.state.title, genre: this.state.genre, posterImage: this.state.posterImage, description: this.state.description, releaseDateAt: this.state.releaseDateAt.getTime() }); this.props.navigation.goBack(); }; } export default MovieForm; 

Мы разделим наш MovieList.js чтобы мы могли контролировать рендеринг в компоненте без состояния. Обновите его следующим образом:

 // components/MovieList.js import React from "react"; import { Q } from "@nozbe/watermelondb"; import withObservables from "@nozbe/with-observables"; import RawMovieItem from "./RawMovieItem"; import { List } from "native-base"; // add these: const MovieItem = withObservables(["movie"], ({ movie }) => ({ movie: movie.observe() }))(RawMovieItem); const MovieList = ({ movies, navigation }) => ( <List> {movies.map(movie => ( // change these: <MovieItem key={movie.id} movie={movie} countObservable={movie.reviews.observeCount()} onPress={() => navigation.navigate("Movie", { movie })} /> ))} </List> ); const enhance = withObservables(["search"], ({ database, search }) => ({ movies: database.collections .get("movies") .query(Q.where("title", Q.like(`%${Q.sanitizeLikeString(search)}%`))) })); export default enhance(MovieList); 

Здесь мы использовали RawMovieItem . Мы напишем наш метод рендеринга в нем. Обратите внимание, как мы обернули наш RawMovieItem в withObservables . Он используется, чтобы сделать его реактивным. Если мы не используем его, нам придется вручную принудительно обновлять при обновлении базы данных.

Примечание: создание простых компонентов React и последующее их наблюдение — это суть WatermelonDB.

Создайте новый файл components/RawMovieItem.js и используйте следующий код:

 // components/RawMovieItem.js import React from "react"; import withObservables from "@nozbe/with-observables"; import { ListItem, Thumbnail, Text, Left, Body, Right, Button, Icon } from "native-base"; // We observe and render the counter in a separate component so that we don't have to wait for the database until we can render the component. You can also prefetch all data before displaying the list const RawCounter = ({ count }) => count; const Counter = withObservables(["observable"], ({ observable }) => ({ count: observable }))(RawCounter); const CustomListItem = ({ movie, onPress, countObservable }) => ( <ListItem thumbnail onPress={onPress}> <Left> <Thumbnail square source={{ uri: movie.posterImage }} /> </Left> <Body> <Text>{movie.title}</Text> <Text note numberOfLines={1}> Total Reviews: <Counter observable={countObservable} /> </Text> </Body> <Right> <Button transparent onPress={onPress}> <Icon name="arrow-forward" /> </Button> </Right> </ListItem> ); export default CustomListItem; 

Нам нужно видеть всю информацию о фильме, а также иметь возможность редактировать его, поэтому создайте новый экран, Movie.js , и получите все отзывы и сделайте его также реагирующим, создайте два новых компонента, components/ReviewList.js и components/RawReviewItem.js .

Используйте следующий код для уважаемых файлов:

 // screens/Movie.js import React, { Component } from "react"; import { View, Card, CardItem, Text, Button, Icon, Left, Body, Textarea, H1, H2, Container, Content } from "native-base"; import withObservables from "@nozbe/with-observables"; import styles from "../components/styles"; import FullWidthImage from "react-native-fullwidth-image"; import ReviewList from "../components/ReviewList"; class Movie extends Component { state = { review: "" }; render() { const { movie, reviews } = this.props; return ( <Container style={styles.container}> <Content> <Card style={{ flex: 0 }}> <FullWidthImage source={{ uri: movie.posterImage }} ratio={1} /> <CardItem /> <CardItem> <Left> <Body> <H2>{movie.title}</H2> <Text note textStyle={{ textTransform: "capitalize" }}> {movie.genre} </Text> <Text note> {movie.releaseDateAt.toString().substr(4, 12)} </Text> </Body> </Left> </CardItem> <CardItem> <Body> <Text>{movie.description}</Text> </Body> </CardItem> <CardItem> <Left> <Button transparent onPress={this.handleDelete} textStyle={{ color: "#87838B" }} > <Icon name="md-trash" /> <Text>Delete Movie</Text> </Button> <Button transparent onPress={this.handleEdit} textStyle={{ color: "#87838B" }} > <Icon name="md-create" /> <Text>Edit Movie</Text> </Button> </Left> </CardItem> <View style={styles.newReviewSection}> <H1>Add new review</H1> <Textarea rowSpan={5} bordered placeholder="Review..." onChangeText={review => this.setState({ review })} value={this.state.review} /> <Body style={{ marginTop: 10 }}> <Button bordered onPress={this.handleAddNewReview}> <Text>Add review</Text> </Button> </Body> </View> <ReviewList reviews={reviews} /> </Card> </Content> </Container> ); } handleAddNewReview = () => { let { movie } = this.props; movie.addReview(this.state.review); this.setState({ review: "" }); }; handleEdit = () => { let { movie } = this.props; this.props.navigation.navigate("EditMovie", { movie }); }; handleDelete = () => { let { movie } = this.props; movie.deleteMovie(); this.props.navigation.goBack(); }; } const enhance = withObservables(["movie"], ({ movie }) => ({ movie: movie.observe(), reviews: movie.reviews.observe() })); export default enhance(Movie); 

ReviewList.js — это реактивный компонент, отображающий список отзывов о фильме. Это улучшает компонент RawReviewItem и делает его реактивным.

 // components/ReviewList.js import React from "react"; import withObservables from "@nozbe/with-observables"; import { List, View, H1 } from "native-base"; import RawReviewItem from "./RawReviewItem"; import styles from "./styles"; const ReviewItem = withObservables(["review"], ({ review }) => ({ review: review.observe() }))(RawReviewItem); const ReviewList = ({ reviews }) => { if (reviews.length > 0) { return ( <View style={styles.allReviewsSection}> <H1>Reviews</H1> <List> {reviews.map(review => ( <ReviewItem review={review} key={review.id} /> ))} </List> </View> ); } else { return null; } }; export default ReviewList; 

RawReviewItem.js — это простой компонент React, используемый для визуализации одного отзыва.

 // components/RawReviewItem.js import React from "react"; import { ListItem, Text, Left, Right, Button, Icon } from "native-base"; // We observe and render the counter in a separate component so that we don't have to wait for the database until we can render the component. You can also prefetch all data before displaying the list. const RawReviewItem = ({ review }) => { handleDeleteReview = () => { review.deleteReview(); }; return ( <ListItem> <Left> <Text>{review.body}</Text> </Left> <Right> <Button transparent onPress={this.handleDeleteReview}> <Icon name="md-trash" /> </Button> </Right> </ListItem> ); }; export default RawReviewItem; 

Наконец, для маршрутизации двух новых экранов мы должны обновить Navigation.js следующим кодом:

 // screens/Navigation.js import React from "react"; import { createStackNavigator, createAppContainer } from "react-navigation"; import Root from "./Root"; import Movie from "./Movie"; import MovieForm from "./MovieForm"; export const createNavigation = props => createAppContainer( createStackNavigator( { Root: { // We have to use a little wrapper because React Navigation doesn't pass simple props (and withObservables needs that) screen: ({ navigation }) => { const { database } = props; return <Root database={database} navigation={navigation} />; }, navigationOptions: { title: "Movies" } }, Movie: { screen: ({ navigation }) => ( <Movie movie={navigation.state.params.movie} navigation={navigation} /> ), navigationOptions: ({ navigation }) => ({ title: navigation.state.params.movie.title }) }, NewMovie: { screen: ({ navigation }) => { const { database } = props; return <MovieForm database={database} navigation={navigation} />; }, navigationOptions: { title: "New Movie" } }, EditMovie: { screen: ({ navigation }) => { return ( <MovieForm movie={navigation.state.params.movie} navigation={navigation} /> ); }, navigationOptions: ({ navigation }) => ({ title: `Edit "${navigation.state.params.movie.title}"` }) } }, { initialRouteName: "Root", initialRouteParams: props } ) ); 

Все компоненты используют стили для отступов и полей. Итак, создайте файл с именем и используйте следующий код:components/styles.js

 // components/styles.js import { StyleSheet } from "react-native"; export default StyleSheet.create({ container: { flex: 1, paddingHorizontal: 10, marginVertical: 10 }, marginContainer: { marginVertical: 10, flex: 1 }, newReviewSection: { marginTop: 10, paddingHorizontal: 15 }, allReviewsSection: { marginTop: 30, paddingHorizontal: 15 } }); 

Запустите приложение:

 npm run start:android # or npm run start:ios 

Окончательный код доступен в основной ветке.

Упражнение

Вот несколько следующих шагов, чтобы применить на практике то, что вы только что узнали. Не стесняйтесь подходить к ним в любом порядке.

  • Сортируйте запрос так, чтобы новые фильмы вышли на первое место.
  • Добавьте функциональность, чтобы обновить обзор.
  • Добавьте фильтр жанра и даты на главном экране.

Вывод

Я надеюсь, что это руководство помогло вам начать работу с базами данных в React Native. Мы покрыли необходимость базы данных в приложении; доступные параметры базы данных; выбор базы данных для вашего приложения; и пример приложения, демонстрирующего, как использовать модели, схемы, действия и реактивные компоненты в WatermelonDB.

Проверьте репо кода приложения на GitHub / MovieDirectory .

Если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать. Мне может потребоваться некоторое время, чтобы ответить, но я постараюсь ответить на все вопросы. Ударь меня (или узнай больше обо мне) на GitHub и Twitter .