使用WatermelonDB创建脱机第一反应本机应用程序

Reaction本机有不同的数据库存储机制,用于不同的移动应用程序。简单的结构-如用户设置、应用程序设置和其他键值对数据-可以使用异步存储或安全存储轻松处理。

其他应用程序-比如Twitter克隆-从服务器获取数据并直接显示给用户。它们维护数据缓存,如果用户需要与任何文档交互,则直接调用API。

所以并不是所有的应用程序都需要一个数据库。

想要从地面上学到本地人的反应吗?这篇文章是我们高级图书馆的摘录。获得一个完整的集合反应本土书籍涵盖基础,项目,技巧和工具&更多与SitePoint高级。现在就加入吧,每月只需9美元.

当我们需要一个数据库

应用程序,例如诺兹贝(一个待办的应用程序),费用(跟踪器),以及分而治之(在应用程序内购买),需要离线工作。要做到这一点,他们需要一种本地存储数据并与服务器同步的方法。这种类型的应用程序称为离线第一阿普。随着时间的推移,这些应用程序收集了大量的数据,直接管理这些数据变得更加困难-因此需要一个数据库来有效地管理这些数据。

反应中的选项

在开发应用程序时,选择最适合您需求的数据库。如果有两个选项可用,那么就选择一个有更好的文档和更快地响应问题的选项。下面是反应本机可用的一些最著名的选项:

  • 西瓜DB一个可与任何底层数据库一起使用的开源反应性数据库。默认情况下,它使用SQLite作为Reaction本机中的底层数据库。
  • SQLite(反应本土化世博):最古老、最常用的、经过战斗测试的、众所周知的解决方案。它可以用于大多数平台,所以如果您已经在另一个移动应用程序开发框架中开发了一个应用程序,您可能已经熟悉它了。
  • 境界(反应本土化):一个开放源码的解决方案,但它也有一个企业版,其中包含大量的其他特征..他们做得很好,还有很多知名公司用它。
  • 火力基地(反应本土化世博):专门为移动开发平台提供的Google服务。它提供了很多功能,存储只是其中之一。但它确实要求你留在他们的生态系统中去利用它。
  • RxDB一个用于网络的实时数据库。它有很好的文档,对GitHub(>9K星)有很好的评价,而且也是反应性的。

先决条件

我假设您已经了解了基本的ReactiveNativeandBuildProcess。我们要用反应-原生-cli来创建我们的应用程序。

我还建议在设置项目时设置Android或IOS开发环境,因为您可能面临许多问题,调试的第一步是打开IDE(AndroidStudio或Xcode)查看日志。

注意:您可以查看安装依赖项的官方指南。这里想了解更多信息。由于官方的指导方针是非常简洁和明确的,我们将不会在这里讨论这个问题。

若要设置虚拟设备或物理设备,请遵循以下指南:

注意:有一个更适合JavaScript的工具链,名为世博..Reaction原住民社区也已经开始推广它,但是我还没有遇到一个大规模的、可以生产的应用程序,该应用程序使用了世博,并且世博港对于那些使用领域之类的数据库的用户,或者在我们的例子中,WatermelonDB是不可用的。

APP需求

我们将创建一个具有标题、海报图像、类型和发布日期的电影搜索应用程序。每部电影都会有很多评论。

应用程序将三屏.

家将显示两个按钮-一个用于生成虚拟记录,另一个用于添加新电影。在它下面,将有一个搜索输入,可以用来从数据库中查询电影标题。它将显示搜索栏下面的电影列表。如果搜索到任何名称,列表将只显示搜索的电影。

 

单击任何电影都会打开电影仪表板,从哪里可以检查所有的评论。可以编辑或删除电影,也可以从此屏幕添加新的评论。

 

第三个屏幕是电影形式,用于创建/更新电影。

 

源代码可在GitHub.

为什么我们选择西瓜数据库(特写)

我们需要创建一个离线第一应用程序,所以数据库是必须的。

西瓜数据库的特点

让我们看一下西瓜数据库的一些特性。

完全可观测
西瓜数据库的一个很大的特点是它的反应性质。任何对象都可以使用可观察到的数据来观察,而且每当数据发生变化时,它都会自动重新修改我们的组件。我们不需要做任何额外的努力来使用西瓜数据库。我们包装简单的反应组件,并增强它们,使它们具有反应性。以我的经验来看,它只是无缝地工作我们不需要关心任何其他的事情。我们改变目标,我们的工作就完成了!它在应用程序中的所有位置被持久化和更新。

反应本机引擎盖下的SQLite
在现代浏览器中,使用即时编译来提高速度,但在移动设备中却没有。此外,移动设备中的硬件比计算机中的硬件慢。由于所有这些因素,JavaScript应用程序在移动应用程序中运行得更慢。为了克服这一问题,WatermelonDB在需要之前不会获取任何信息。它使用延迟加载和SQLite作为单独线程上的底层数据库来提供快速响应。

同步原语和同步适配器
尽管WatermelonDB只是一个本地数据库,但它也提供同步原语和同步适配器。它使它非常容易使用任何我们自己的后端数据库。我们只需要遵守后端的WatermelonDB同步协议并提供端点。

其他特点包括:

  • 静态类型使用流
  • 适用于所有平台

Dev Env和WatermelonDB设置(v0.0)

我们要用react-native-cli来创建我们的应用程序。

注:您可以使用它与ExpoKit或退出世博会。

如果要跳过此部分,则克隆源回购并签出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一个UI库,将用于我们的应用程序的外观和感觉。
  • 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"
}

这将用于在相应的设备中运行我们的应用程序。

建立西瓜数据库

我们需要添加一个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文件夹并同步项目。否则,它将在第一次运行应用程序时给出一个错误。如果你的目标是iOS.

在运行应用程序之前,我们需要将react-native-gesture处理程序包,依赖项为react-navigation,和react-native-vector-icons,依附于.native-base..默认情况下,为了使应用程序的二进制大小保持较小,Reaction本机不包含支持本机特性的所有代码。因此,无论何时我们需要使用特定的特性,我们都可以使用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分支。

 

补习

由于我们将创建一个数据库应用程序,许多代码将只用于后端,而且在前端看不到多少。它可能看起来很长,但要有耐心,并遵循教程,直到结束。你不会后悔的!

WatermelonDB工作流可分为三个主要部分:

  • 图式用于定义数据库表架构。
  • 模型:ORM映射的对象。我们将在整个应用程序中与它们交互。
  • 行为用于对象/行执行各种CRUD操作。我们可以使用数据库对象直接执行操作,也可以在模型中定义函数来执行这些操作。在模型中定义它们是更好的实践,我们将只使用它。

让我们开始我们的应用程序。

初始化DB Schema和WatermelonDB(v0.1)

我们将在应用程序中定义模式、模型和数据库对象。我们将无法在应用程序中看到什么,但这是最重要的一步。在这里,我们将检查我们的应用程序在定义所有内容之后是否正确工作。如果出了什么问题,在这个阶段很容易调试它。

项目结构

创建一个新的src根目录中的文件夹。这将是所有响应本机代码的根文件夹。这个models文件夹用于所有与数据库相关的文件。它会表现得像我们的道(数据访问对象)文件夹。这是一个术语,用于某种类型的数据库或其他持久性机制的接口。这个components文件夹将有我们所有的反应组件。这个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 }
      ]
    })
  ]
});

我们定义了两个表-一个用于电影,另一个用于它的评论。代码本身是不言自明的。两个表都有相关的列。

注意,如西瓜数据库命名规则,所有ID都以_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外键。这些检查外键值与idmovie表将评论模型链接到电影模型。

对于日期,我们还需要使用@date装饰师让西瓜数据库给我们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

我们还没有在UI中做任何更改,所以如果一切顺利的话,屏幕将看起来类似于以前。

到此部分的所有代码都在v0.1分支。

添加操作和虚拟数据生成器(v0.2)

让我们向应用程序添加一些虚拟数据。

行为

为了执行CRUD操作,我们将创建一些操作。打开models/Movie.jsmodels/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.jsmodels/randomData.jsgenerate.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详情如下:

// 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是呈现电影列表的简单Reaction组件,但请注意enhance打电话withObservables..这个withObservables特制(高阶组分)使任何反应组分在西瓜DB中发生反应。如果我们在应用程序中的任何地方更改电影的值,它将重新修改它以反映更改。第二个论点,({ database, search }),包括组件道具。searchRoot.jsdatabaseNavigation.js..第一个论点["search"]触发观察重新启动的道具列表。所以如果search变化,我们的可观测对象被重新计算和观察。在函数中,我们使用database对象获取电影集合,其中title就像逝去了search..特殊人物%_不会自动转义,因此建议始终使用经过消毒的用户输入。

打开AndroidStudio或Xcode,同步项目,然后运行应用程序。单击生成虚拟记录纽扣。它将生成虚拟数据并显示列表。

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

此代码可在v0.2分支。

 

添加所有CRUD操作(V1)

现在让我们添加创建/更新/删除电影和评论的功能。我们将添加一个新按钮来添加一个新电影,并创建一个TextInput若要将搜索关键字传递给查询,请执行以下操作。如此开放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,也可以使用相同的组件编辑电影。注意到我们只是打电话给handleSubmit方法,该方法依次调用handleAddNewMoviehandleUpdateMoviehandleUpdateMovie调用我们前面在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;

我们会把我们的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..我们将在其中编写我们的渲染方法。注意我们是如何包装我们的RawMovieItemwithObservables..它是用来使它成为反应性的。如果我们不使用它,那么我们必须在数据库更新时手动强制更新。

注意:创建简单的Reaction组件然后观察它们是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.jscomponents/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是一个简单的Reaction组件,用于呈现单个评审。

// 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

最终代码可在师父分支。

视频播放器

00:00




 

00:33

运动

下面是一些后续的步骤来练习你刚刚学到的东西。你可以随意地按你喜欢的顺序接近他们。

  • 对查询进行排序,以便将新电影放在首位。
  • 添加功能以更新考核。
  • 在主屏幕中添加类型和日期筛选器。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值