一、购物清单

大多数现代语言和框架都使用待办事项列表作为示例应用。这是一个很好的方法来理解框架的基础知识,如用户交互、基本导航或如何构造代码。我们将从更务实的方式开始:构建购物清单应用。

您将能够使用 React 本机代码开发此应用,为 iOS 和 Android 构建它,最后将其安装到您的手机上。这样,您不仅可以向朋友展示您构建的功能,还可以了解您可以自己构建的缺失功能,思考用户界面的改进,最重要的是,激励自己不断学习 React Native,因为您感觉到它的真正潜力

到本章结束时,您将建立一个功能齐全的购物清单,可以在手机上使用,并拥有创建和维护简单有状态应用所需的所有工具。

概述

React Native 最强大的功能之一是其跨平台功能;我们将为 iOS 和 Android 构建购物清单应用,重用 99%的代码。让我们来看看应用将如何看待这两个平台:

网间网操作系统:

添加更多产品后,其外观如下所示:

安卓:

添加更多产品后,其外观如下所示:

这两个平台上的应用的用户界面非常相似,但我们不需要太在意这些差异(例如,添加产品屏幕上的后退按钮),因为它们将由 React Native 自动处理。

了解每个平台都有自己的用户界面模式是很重要的,遵循这些模式是一个很好的实践。例如,导航通常是通过 iOS 中的标签来处理的,而 Android 更喜欢抽屉式菜单,所以如果我们希望两个平台上都有快乐的用户,那么我们应该构建这两种导航模式。无论如何,这只是一个建议,任何用户界面模式都可以在每个平台上构建。在后面的章节中,我们将看到如何在同一个代码库中以最有效的方式处理两种不同的模式。

该应用由两个屏幕组成:您的购物清单和可添加到购物清单中的产品清单。用户可以通过蓝色圆形按钮从购物列表屏幕导航到添加产品屏幕,然后通过

本章将介绍以下主题:

  • 基本项目的文件夹结构
  • React Native 的基本 CLI 命令
  • 基本导航
  • JS 调试
  • 实时重新加载
  • 使用 NativeBase 设置样式
  • 列表
  • 基本国家管理
  • 处理事件
  • AsyncStorage
  • 提示弹出窗口
  • 分发应用

建立我们的项目

React Native 有一个非常强大的 CLI,我们需要安装它才能开始我们的项目。要安装,如果您没有足够的权限,只需在命令行中运行以下命令(您可能需要使用sudo运行此命令):

npm install -g react-native-cli

安装完成后,我们可以通过键入react-native开始使用 React Native CLI。要启动项目,我们将运行以下命令:

react-native init --version="0.49.3" GroceriesList

此命令将创建一个名为GroceriesList的基本项目,其中包含在 iOS 和 Android 上构建应用所需的所有依赖项和库。CLI 完成所有软件包的安装后,您的文件夹结构应与以下类似:

我们项目的输入文件为index.js。如果希望看到初始应用在模拟器上运行,可以再次使用 React-Native 的 CLI:

react-native run-ios

react-native run-android

如果您安装了 XCode 或 Android Studio 和 Android 模拟器,编译后您应该能够在模拟器上看到示例屏幕:

我们已经有了开始实现我们的应用所需的一切设置,但是为了在模拟器中轻松调试和查看我们的更改,我们需要启用另外两个功能:远程 JS 调试和实时重新加载。

对于调试,我们将使用React Native Debugger,这是一个独立的应用,基于 React Native 的官方调试器,包括 React Inspector 和 Redux DevTools。可以按照其 GitHub 存储库(上的说明进行下载 https://github.com/jhen0409/react-native-debugger )。为了使这个调试器能够正常工作,我们需要通过在 iOS 上按命令+ctrl+Z或在 Android 上按命令+M在模拟器中打开 React Native development 菜单,从而在我们的应用中启用远程 JS 调试。

如果一切顺利,我们将看到出现以下菜单:

现在,我们将按下两个按钮:调试远程 JS 和启用实时重新加载。一旦我们完成了这项工作,我们就拥有了所有的开发环境,并准备好开始编写 React 代码。

设置文件夹结构

我们的应用只有两个屏幕:购物清单和添加产品。由于这种简单应用的状态应该易于管理,因此我们不会添加任何用于状态管理的库(例如,Redux),因为我们将通过导航组件发送共享状态。这将使我们的文件夹结构相当简单:

我们必须创建一个src文件夹,用于存储所有 React 代码。自建文件index.js的代码如下:

/*** index.js ***/

import { AppRegistry } from 'react-native';
import App from './src/main';
AppRegistry.registerComponent('GroceriesList', () => App);

简而言之,这些文件将为我们的应用导入公共根代码,将其存储在名为App的变量中,然后通过registerComponent方法将该变量传递给AppRegistryAppRegistry是我们应该注册根组件的组件。一旦我们这样做,React Native 将为我们的应用生成一个 JS 包,然后在应用准备就绪时通过调用AppRegistry.runApplication运行该应用。

我们将要编写的大部分代码都将放在src文件夹中。对于此应用,我们将在该文件夹中创建根组件(main.js)和一个子文件夹screens,其中存储两个屏幕(ShoppingListAddProduct

现在,让我们先安装应用的所有初始依赖项,然后再继续编码。在项目的根文件夹中,我们需要运行以下命令:

npm install

运行该命令将为每个 React 本机项目安装所有基本依赖项。现在,让我们安装将用于此特定应用的三个软件包:

npm install native-base --save
npm install react-native-prompt-android --save
npm install react-navigation --save

在本章后面,我们将解释每个包的用途。

添加导航组件

大多数移动应用由多个屏幕组成,因此我们需要能够在这些屏幕之间“旅行”。为了实现这一点,我们需要一个Navigation组件。React Native 附带了一个现成的NavigatorNavigatorIOS组件,尽管 React 维护人员建议使用由名为react-navigation的社区构建的外部导航解决方案 https://github.com/react-community/react-navigation ),性能非常好,维护良好,功能丰富,因此,我们将在我们的应用中使用它。

因为我们已经安装了导航模块(react-navigation,所以我们可以在main.js文件中设置并初始化Navigation组件:

/*** src/main.js ***/

import React from 'react';
import { StackNavigator } from 'react-navigation';
import ShoppingList from './screens/ShoppingList.js';
import AddProduct from './screens/AddProduct.js';

const Navigator = StackNavigator({
  ShoppingList: { screen: ShoppingList },
  AddProduct: { screen: AddProduct }
});

export default class App extends React.Component {
  constructor() {
    super();
  }

  render() {
    return <Navigator />;
  }
}

我们的根组件导入应用中的两个屏幕(ShoppingListAddProduct,并将它们传递给StackNavigator函数,该函数生成Navigator组件。让我们深入研究 T4 如何工作。

StackNavigator为任何应用提供了一种在屏幕之间转换的方式,其中每个新屏幕都放在堆栈的顶部。当我们请求导航到新屏幕时,StackNavigator将从右侧滑动新屏幕,并在右上角放置一个< Back按钮以返回到 iOS 中的上一个屏幕,或者,当新屏幕放置一个<-箭头以返回到 Android 中时,将从底部淡入。使用相同的代码库,我们将在 iOS 和 Android 中触发熟悉的导航模式。StackNavigator使用起来也非常简单,因为我们只需要将应用中的屏幕作为哈希图传递,其中键是我们想要的屏幕名称,值是作为组件导入的屏幕。结果是一个<Navigator/>组件,我们可以渲染它来初始化我们的应用。

使用 NativeBase 设计我们的应用

React Native 提供了一种使用 Flexbox 和类似 CSS 的 API 设计组件和屏幕样式的强大方法,但对于该应用,我们希望重点关注功能方面,因此我们将使用一个库,其中包括按钮、列表、图标、菜单、表单等基本样式组件。它可以被看作是 React Native 的 Twitter 引导。

有几个流行的 UI 库,NativeBase 和 React Native 元素是两个最流行和最受支持的元素。在这两个选项中,我们将选择 NativeBase,因为它的文档对于初学者来说稍微清晰一些。

您可以在他们的网站(上找到有关 NativeBase 如何工作的详细文档 https://docs.nativebase.io/ ),但我们将在本章中介绍安装和使用部分组件的基础知识。我们之前通过npm installnative-base安装为我们项目的依赖项,但 NativeBase 包含一些对等依赖项,需要链接并包含在我们的 iOS 和 Android 本机文件夹中。幸运的是,React Native 已经有了一个工具来查找这些依赖项并将它们链接起来;我们只需要运行:

react-native link

此时,我们的应用中完全可以使用 NativeBase 的所有 UI 组件。因此,我们可以开始构建我们的第一个屏幕。

构建购物清单屏幕

我们的第一个屏幕将包含我们需要购买的物品的列表,所以它将包含我们需要购买的每个物品的一个列表项目,包括一个按钮,用于将该物品标记为已购买。此外,我们需要一个按钮来导航到AddProduct屏幕,这将允许我们将产品添加到我们的列表中。最后,我们将添加一个按钮来清除产品列表,以防我们想要开始一个新的购物列表:

让我们首先在screens文件夹中创建ShoppingList.js并从native-basereact-native导入我们需要的所有 UI 组件(在清除所有项目之前,我们将使用警报弹出窗口警告用户)。我们将使用的主要 UI 组件是Fab(蓝色和红色圆形按钮)、ListListItemCheckBoxTextIcon。为了支持我们的布局,我们将使用BodyContainerContentRight,这是我们其余组件的布局容器。

有了所有这些组件,我们可以创建一个简单版本的ShoppingList组件:

/*** ShoppingList.js ***/

import React from 'react';
import { Alert } from 'react-native';
import {
  Body,
  Container,
  Content,
  Right,
  Text,
  CheckBox,
  List,
  ListItem,
  Fab,
  Icon
} from 'native-base';

export default class ShoppingList extends React.Component {
  static navigationOptions = {
    title: 'My Groceries List'
  };
  /*** Render ***/
  render() {
    return (
      <Container>
        <Content>
          <List>
            <ListItem>
              <Body>
                <Text>'Name of the product'</Text>
              </Body>
              <Right>
                <CheckBox
                  checked={false}
                />
              </Right>
            </ListItem>
          </List>
        </Content>
        <Fab
          style={{ backgroundColor: '#5067FF' }}
          position="bottomRight"
        >
          <Icon name="add" />
        </Fab>
        <Fab
          style={{ backgroundColor: 'red' }}
          position="bottomLeft"
        >
          <Icon ios="ios-remove" android="md-remove" />
        </Fab>
      </Container>
    );
  }
}

这只是一个静态显示我们将在此屏幕上使用的组件的哑组件。需要注意的一些事项:

  • navigationOptions是一个静态属性,<Navigator>将使用它来配置导航的行为方式。在我们的例子中,我们希望显示我的杂货清单作为这个屏幕的标题。
  • 为了让native-base发挥它的魔力,我们需要使用<Container><Content>来正确地形成布局。
  • Fab按钮放置在<Content>的外面,因此它们可以浮动在左下角和右下角。
  • 每个ListItem包含一个<Body>(主文本)和一个<Right>(向右对齐的图标)。

由于我们在第一步中启用了实时重新加载,我们应该看到在保存新创建的文件后应用正在重新加载。所有的 UI 元素现在都就位了,但它们不起作用,因为我们没有添加任何状态。这应该是我们的下一步。

向屏幕添加状态

让我们在ShoppingList屏幕上添加一些初始状态,用实际的动态数据填充列表。我们将首先创建一个构造函数并在其中设置初始状态:

/*** ShoppingList.js ***/

...
constructor(props) {
  super(props);
  this.state = {
    products: [{ id: 1, name: 'bread' }, { id: 2, name: 'eggs' }]
  };
}
...

现在,我们可以在<List>中呈现该状态(在render方法中):

/*** ShoppingList.js ***/

...
<List>
 {
   this.state.products.map(p => {
     return (
       <ListItem
         key={p.id}
       >
         <Body>
           <Text style={{ color: p.gotten ? '#bbb' : '#000' }}>
             {p.name}
           </Text>
         </Body>
         <Right>
           <CheckBox
             checked={p.gotten}
            />
         </Right>
       </ListItem>
     );
   }
  )}
</List>
...

我们现在依赖于组件状态内的产品列表,每个产品存储一个id、一个namegotten属性。修改此状态时,我们将自动重新呈现列表。

现在,是时候添加一些事件处理程序了,这样我们就可以根据用户的命令修改状态或导航到AddProduct屏幕。

添加事件处理程序

与用户的所有交互都将通过 React Native 中的事件处理程序进行。根据控制器的不同,我们可以触发不同的事件。最常见的事件是onPress,因为通常我们每次按下按钮、复选框或视图时都会触发它。让我们为屏幕中可以推送的所有组件添加一些onPress处理程序:

/*** ShoppingList.js ***/

...
render() {
 return (
   <Container>
     <Content>
       <List>
        {this.state.products.map(p => {
          return (
            <ListItem
              key={p.id}
              onPress={this._handleProductPress.bind(this, p)}
            >
              <Body>
                <Text style={{ color: p.gotten ? '#bbb' : '#000' }}>
                  {p.name}
                </Text>
              </Body>
              <Right>
                <CheckBox
                  checked={p.gotten}
                  onPress={this._handleProductPress.bind(this, p)}
                />
              </Right>
            </ListItem>
          );
       })}
       </List>
     </Content>
     <Fab
       style={{ backgroundColor: '#5067FF' }}
       position="bottomRight"
       onPress={this._handleAddProductPress.bind(this)}
     >
       <Icon name="add" />
     </Fab>
     <Fab
       style={{ backgroundColor: 'red' }}
       position="bottomLeft"
       onPress={this._handleClearPress.bind(this)}
     >
       <Icon ios="ios-remove" android="md-remove" />
     </Fab>
   </Container>
   );
 }
...

请注意,我们添加了三个onPress事件处理程序:

  • <ListItem>上,当用户点击列表中的一种产品时做出反应
  • <CheckBox>上,当用户点击列表中每个产品旁边的复选框图标时做出反应
  • 在两个<Fab>按钮上

如果您知道 React,您可能理解为什么我们在所有处理程序函数中使用.bind,但是,如果您有疑问,.bind将确保我们可以在处理程序的定义中使用this作为组件本身而不是全局范围的引用。这将允许我们调用组件内部的方法作为this.setState或读取组件的属性,如this.propsthis.state

对于用户点击特定产品的情况,我们还绑定产品本身,以便在事件处理程序中使用它们。

现在,让我们定义将用作事件处理程序的函数:

/*** ShoppingList.js ***/

...
_handleProductPress(product) {
 this.state.products.forEach(p => {
   if (product.id === p.id) {
     p.gotten = !p.gotten;
   }
   return p;
 });

 this.setState({ products: this.state.products });
}
...

首先,让我们为用户点击购物列表中的产品或其复选框时创建一个处理程序。我们希望将产品标记为gotten(如果已经是gotten,则取消标记),因此我们将使用正确标记的产品更新状态。

接下来,我们将为蓝色<Fab>按钮添加一个处理程序,以导航到AddProduct屏幕:

/*** ShoppingList.js ***/

...
_handleAddProductPress() {
  this.props.navigation.navigate('AddProduct', {
    addProduct: product => {
      this.setState({
        products: this.state.products.concat(product)
      });
    },
    deleteProduct: product => {
      this.setState({
        products: this.state.products.filter(p => p.id !== product.id)
      });
    },
    productsInList: this.state.products
  });
}
...

此处理程序使用this.props.navigation,这是Navigator组件从react-navigation自动传递的属性。此属性包含一个名为navigate的方法,该方法接收应用应导航到的屏幕的名称以及可用作全局状态的对象。对于此应用,我们将存储三个键:

  • addProduct:一个功能,允许AddProduct屏幕修改ShoppingList组件的状态,以反映向购物清单中添加新产品的动作。
  • deleteProduct:一个功能,允许AddProduct屏幕修改ShoppingList组件的状态,以反映从购物清单中删除产品的动作。
  • productsInList:购物清单上已经有一个保存商品清单的变量,因此AddProducts屏幕可以知道哪些商品已经添加到购物清单中,并显示为“已经添加”,防止添加重复的项目。

导航中的处理状态应该被视为包含有限屏幕数的简单应用的解决方法。在更大的应用中(我们将在后面的章节中看到),应该使用状态管理库(如 Redux 或 MobX)来保持纯数据和用户界面处理之间的分离。

我们将为蓝色<Fab>按钮添加最后一个处理程序,以便用户在想要开始新列表时清除购物列表中的所有项目:

/*** ShoppingList.js ***/

...
_handleClearPress() {
  Alert.alert('Clear all items?', null, [
    { text: 'Cancel' },
    { text: 'Ok', onPress: () => this.setState({ products: [] }) }
  ]);
}
...

我们正在使用Alert提示用户确认,然后再清除我们购物清单中的所有元素。一旦用户确认此操作,我们将清空组件状态中的products属性。

把它们放在一起

让我们看看将所有方法放在一起时整个组件的结构会是什么样子:

/*** ShoppingList.js ***/

import React from 'react';
import { Alert } from 'react-native';
import { ... } from 'native-base';

export default class ShoppingList extends React.Component {
 static navigationOptions = {
   title: 'My Groceries List'
 };

 constructor(props) {
   ...
 }

 /*** User Actions Handlers ***/
 _handleProductPress(product) {
   ...
 }

 _handleAddProductPress() {
   ...
 }

 _handleClearPress() {
   ...
 }

 /*** Render ***/
 render() {
   ...
 }
}

React 原生组分的结构与正常 React 组分非常相似。我们需要导入 React 本身,然后导入一些组件来构建屏幕。我们也有几个事件处理程序(我们只使用下划线作为前缀),最后还有一个render方法来使用标准 JSX 显示我们的组件。

React web 应用的唯一区别在于,我们使用的是 React 本机 UI 组件,而不是 DOM 组件

构建 AddProduct 屏幕

由于用户需要将新产品添加到购物列表中,我们需要构建一个屏幕,在该屏幕中,我们可以提示用户添加产品的名称,并将其保存在手机存储器中以备将来使用。

使用异步存储

在构建 React 本机应用时,了解移动设备如何处理每个应用使用的内存非常重要。我们的应用将与设备中的其他应用共享内存,因此,最终,使用我们应用的内存将由其他应用占用。因此,我们不能依赖于将数据放入内存供以后使用。如果我们想确保数据在应用的所有用户中都可用,我们需要将这些数据存储在设备的持久存储中。

React Native 提供了一个 API 来处理与移动设备中持久存储的通信,该 API 在 iOS 和 Android 上是相同的,因此我们可以轻松地编写跨平台代码。

API 名为AsyncStorage,从 React Native 导入后可以使用:

import { AsyncStorage } from 'react-native';

我们将只使用AsyncStorage中的两种方法:getItemsetItem。例如,我们将在屏幕中创建一个本地函数,用于将产品添加到完整的产品列表中:

/*** AddProduct ***/

...
async addNewProduct(name) {
  const newProductsList = this.state.allProducts.concat({
    name: name,
    id: Math.floor(Math.random() * 100000)
  });

  await AsyncStorage.setItem(
    '@allProducts',
    JSON.stringify(newProductsList)
  );

  this.setState({
    allProducts: newProductsList
  });
 }
...

这里有一些有趣的事情需要注意:

  • 我们正在使用 ES7 的特性,如asyncawait来处理异步调用,而不是承诺或回调。理解 ES7 不在本书的范围之内,但建议学习和理解asyncawait的使用,因为这是一个非常强大的功能,我们将在本书中广泛使用。
  • 每次我们向allProducts添加产品时,我们也会调用AsyncStorage.setItem将产品永久存储在我们设备的存储器中。此操作确保用户添加的产品即使在操作系统清除应用使用的内存时也可用。
  • 我们需要将两个参数传递给setItem(以及getItem:一个键和一个值。它们都必须是字符串,因此如果要存储 JSON 格式的数据,我们需要使用JSON.stringify

向屏幕添加状态

正如我们刚才看到的,我们将在组件的状态中使用一个名为allProducts的属性,该属性将包含用户可以添加到购物列表中的产品的完整列表

我们可以在组件的构造器中初始化此状态,以向用户提供即使在应用第一次运行时他/她将在屏幕上看到的内容的要点(这是许多现代应用通过假装used状态向车载用户使用的伎俩):

/*** AddProduct.js ***/

...
constructor(props) {
  super(props);
  this.state = {
    allProducts: [
      { id: 1, name: 'bread' },
      { id: 2, name: 'eggs' },
      { id: 3, name: 'paper towels' },
      { id: 4, name: 'milk' }
    ],
    productsInList: []
  };
}
...

除了allProducts之外,我们还将有一个productsInList阵列,容纳所有已经添加到当前购物清单中的产品。这将允许我们将产品标记为Already in shopping list,防止用户尝试在列表中添加同一产品两次。

此构造函数对于我们的应用的首次运行非常有用,但一旦用户添加了产品(并因此将其保存在持久存储中),我们希望显示这些产品,而不是此测试数据。为了实现此功能,我们应该从AsyncStorage读取保存的产品,并将其设置为我们状态中的初始allProducts值。我们将在componentWillMount上执行此操作:

/*** AddProduct.js ***/

...
async componentWillMount() {
  const savedProducts = await AsyncStorage.getItem('@allProducts');
  if(savedProducts) {
    this.setState({
      allProducts: JSON.parse(savedProducts)
    }); 
  }

  this.setState({
    productsInList: this.props.navigation.state.params.productsInList
  });
}
...

一旦屏幕准备好安装,我们将更新状态。首先,我们将通过从持久存储中读取allProducts值来更新它。然后,我们将根据ShoppingList屏幕在navigation属性中设置的状态更新列表productsInList

使用此状态,我们可以建立产品列表,并将其添加到购物列表中:

/*** AddProduct ***/

...
render(){
  <List>
    {this.state.allProducts.map(product => {
       const productIsInList = this.state.productsInList.find(
         p => p.id === product.id
       );
       return (
         <ListItem key={product.id}>
           <Body>
             <Text
               style={{
                color: productIsInList ? '#bbb' : '#000'
               }}
             >
               {product.name}
             </Text>
             {
               productIsInList &&
               <Text note>
                 {'Already in shopping list'}
               </Text>
             }
          </Body>
        </ListItem>
      );
    }
 )}
 </List>
}
...

在我们的render方法中,我们将使用Array.map函数迭代并打印每个可能的产品,检查该产品是否已添加到当前购物列表中,以显示一条注释,警告用户:Already in shopping list

当然,我们仍然需要为所有可能的用户操作添加更好的布局、按钮和事件处理程序。让我们开始改进我们的render方法,将所有功能都放在适当的位置

添加事件侦听器

正如ShoppingList屏幕一样,我们希望用户能够与AddProduct组件交互,因此我们将添加一些事件处理程序来响应一些用户操作。

我们的render方法应该是这样的:

/*** AddProduct.js ***/

...
render() {
  return (
    <Container>
      <Content>
        <List>
          {this.state.allProducts.map(product => {
            const productIsInList = this.state.productsInList.
            find(p => p.id === product.id);
            return (
              <ListItem
                key={product.id}
                onPress={this._handleProductPress.bind
                (this, product)}
              >
                <Body>
                  <Text
                    style={{ color: productIsInList? '#bbb' : '#000' }}
                  >
                    {product.name}
                  </Text>
                 {
                   productIsInList &&
                   <Text note>
                     {'Already in shopping list'}
                   </Text>
                 }
                 </Body>
                 <Right>
                   <Icon
                     ios="ios-remove-circle"
                     android="md-remove-circle"
                     style={{ color: 'red' }}
                     onPress={this._handleRemovePress.bind(this, 
                     product )}
                   />
                 </Right>
               </ListItem>
             );
           })}
         </List>
       </Content>
     <Fab
       style={{ backgroundColor: '#5067FF' }}
       position="bottomRight"
       onPress={this._handleAddProductPress.bind(this)}
     >
       <Icon name="add" />
     </Fab>
   </Container>
   );
 }
...

有三个事件处理程序响应此组件中的三个按下事件:

  • 蓝色<Fab>按钮,负责将新产品添加到产品列表中
  • 在每个<ListItem>上,将产品添加到购物列表中
  • 在每个<ListItem>内的删除图标上,从产品列表中删除此产品,可将其添加到购物列表中

一旦用户按下<Fab>按钮,我们就开始向可用产品列表添加新产品:

/*** AddProduct.js ***/

...
_handleAddProductPress() {
  prompt(
    'Enter product name',
    '',
    [
      { text: 'Cancel', style: 'cancel' },
      { text: 'OK', onPress: this.addNewProduct.bind(this) }
    ],
    {
      type: 'plain-text'
    }
  );
}
...

我们在这里使用来自react-native-prompt-android模块的prompt函数。尽管名称不同,但它是一个跨平台的弹出式提示库,我们将使用它通过之前创建的addNewProduct函数添加产品。我们需要先导入prompt函数才能使用,如下所示:

import prompt from 'react-native-prompt-android';

以下是输出:

一旦用户输入产品名称并按下 OK,产品将添加到列表中,以便我们可以移动到下一个事件处理程序,当用户点击产品名称时,将产品添加到购物列表中:

/*** AddProduct.js ***/

...
_handleProductPress(product) {
  const productIndex = this.state.productsInList.findIndex(
    p => p.id === product.id
  );
  if (productIndex > -1) {
    this.setState({
      productsInList: this.state.productsInList.filter(
        p => p.id !== product.id
      )
    });
    this.props.navigation.state.params.deleteProduct(product);
  } else {
    this.setState({
      productsInList: this.state.productsInList.concat(product)
    });
    this.props.navigation.state.params.addProduct(product);
 }
}
...

此处理程序检查所选产品是否已在购物列表中。如果是,它将通过从导航状态调用deleteProduct将其删除,也将通过调用setState将其从组件状态中删除。否则会在导航状态下通过调用addProduct将产品添加到购物列表中,并通过调用setState刷新本地状态。

最后,我们将为每个<ListItems>上的删除图标添加一个事件处理程序,以便用户可以从可用产品列表中删除产品:

/*** AddProduct.js ***/

...
async _handleRemovePress(product) {
  this.setState({
    allProducts: this.state.allProducts.filter(p => p.id !== product.id)
  });
  await AsyncStorage.setItem(
    '@allProducts',
    JSON.stringify(
      this.state.allProducts.filter(p => p.id !== product.id)
    )
  );
}
...

我们需要从组件的本地状态中删除产品,但也要从AsyncStorage中删除产品,这样它就不会在我们的应用的后续运行中显示。

把它们放在一起

我们有所有的碎片来构建我们的 Tur-T0 屏幕,所以让我们来看看这个组件的一般结构:

import React from 'react';
import prompt from 'react-native-prompt-android';
import { AsyncStorage } from 'react-native';
import {
 ...
} from 'native-base';

export default class AddProduct extends React.Component {
  static navigationOptions = {
    title: 'Add a product'
  };

  constructor(props) {
   ...
  }

  async componentWillMount() {
    ...
  }

  async addNewProduct(name) {
    ...
  }

  /*** User Actions Handlers ***/
  _handleProductPress(product) {
   ...
  }

  _handleAddProductPress() {
    ...
  }

  async _handleRemovePress(product) {
    ...
  }

  /*** Render ***/
  render() {
    ....
  }
}

我们有一个非常类似于我们为ShoppingList构建的结构:构建初始状态的navigatorOptions构造函数、用户操作处理程序和render方法。在本例中,我们添加了两个异步方法,以方便处理AsyncStorage

安装和分发应用

在模拟器/仿真器上运行我们的应用是一种非常可靠的方式,可以了解我们的应用在移动设备中的行为。在模拟器/仿真器中工作时,我们可以模拟触摸手势、糟糕的网络连接环境,甚至内存问题。但最终,我们希望将应用部署到物理设备上,这样我们就可以执行更深入的测试。

安装或分发内置 React Native 的应用有多种选择,直接电缆连接是最简单的选择。Facebook 保留了一份关于如何在 React Native 网站(上实现直接安装的更新指南 https://facebook.github.io/react-native/docs/running-on-device.html ),但在将应用分发给其他开发人员、测试人员或指定用户时,还有其他选择。

试飞

试飞(https://developer.apple.com/testflight/ 是一个非常棒的工具,用于将应用分发给测试版测试人员和开发人员,但它有一个很大的缺点——它只适用于 iOS。它的安装和使用非常简单,因为它集成到 iTunes Connect 中,苹果公司认为它是在开发团队中分发应用的官方工具。除此之外,它是绝对免费的,而且它的使用限制非常大:

  • 您的团队中最多有 25 名测试人员
  • 您团队中每个测试人员最多 30 台设备
  • 在您的团队之外最多 2000 名外部测试人员(具有分组功能)

简言之,当您的应用只针对 iOS 设备时,Testflight 是您可以选择的平台。

由于在本书中,我们希望关注跨平台开发,因此我们将介绍其他替代方案,将我们的应用从同一平台分发到 iOS 和 Android 设备。

迪亚维

Diawi(http://diawi.com 是一个网站,开发者可以上传他们的.ipa.apk文件(编译后的应用)并与任何人共享链接,因此该应用可以下载并安装在连接到互联网的任何 iOS 或 Android 设备上。过程很简单:

  1. 在 XCode/Android studio 中构建.ipa(iOS)/.apk(Android)。
  2. 将生成的.ipa/.apk文件拖放到 Diawi 的站点中。
  3. 通过电子邮件或任何其他方法将 Diawi 创建的链接与测试人员列表共享。

链接是私有的,可以为那些需要更高安全性的应用提供密码保护。主要的缺点是测试设备的管理,因为一旦链接被分发,Diawi 就会失去对它们的控制,因此开发人员无法知道下载和测试了哪些版本。如果手动管理测试人员列表是一个选项,那么 Diawi 是 Testflight 的一个很好的替代方案。

安装工

如果我们需要管理分发给哪些测试人员的版本,以及他们是否已经开始测试应用,我们应该给 Installer(https://www.installrapp.com )一次尝试,因为在功能上它与 Diawi 非常相似,但它还包括一个控制谁是用户的仪表板,分别向他们发送了哪些应用,以及测试设备中应用的状态(未安装、已安装或已打开)。当我们的需求之一是对我们的测试人员、设备和构建具有良好的可见性时,这个仪表板非常强大,并且肯定是一个巨大的优势。

Installr 的缺点是它的免费计划只包括每个版本的三个测试设备,尽管他们提供了一个便宜的一次性支付计划,以防我们真的想增加这个数字。当我们需要可见性和跨平台分布时,这是一个非常合理的选择。

总结

在本章中,我们学习了如何启动 React 原生项目,构建一个包含基本导航和处理多个用户交互的应用。我们看到了如何使用导航模块处理持久性数据和基本状态,因此我们可以在项目中的屏幕之间进行转换。

所有这些模式都可以用来构建许多简单的应用,但在下一章中,我们将深入探讨更复杂的导航模式,以及如何沟通和处理从互联网获取的外部数据,这将使我们能够构建和准备我们的应用,以适应不断增长的需求。除此之外,我们还将使用 MobX(一个 JavaScript 库)进行状态管理,这将使我们的域数据以一种非常简单有效的方式提供给我们应用中的所有屏幕。