ReacteNative-示例-一-

56 阅读1小时+

ReacteNative 示例(一)

原文:zh.annas-archive.org/md5/6c72813d39d3c6a9eb1e81035f8a1a7b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

React Native 是一个非常强大的框架,它使得以网络为中心的程序员能够更容易地在多个平台上进行开发。在这本书中,你将学习如何使用 React Native 构建移动应用,这些应用可以部署到 iOS App Store 和 Google Play。

本书涵盖的内容

第一章,第一个项目 - 创建基本待办事项应用,开始了使用 React Native 编写待办事项应用的过程。你将规划该应用并了解 StyleSheet、Flexbox 和 ES6 的概览。你还将使用 React Native SDK 的四个不同部分来创建应用的构建块。

第二章,待办事项应用的高级功能,深入探讨了第一章中开始构建的应用。你将学习如何处理导航、日期和时间选择、构建按钮,并为应用创建一个自定义可折叠和动画组件。你还将将这些课程内容转化为应用的 Android 版本。

第三章,第二个项目 - 预算应用,将开始本书的第二个项目。你将规划一个支出跟踪应用,为 React Native 安装第三方矢量图标库,创建可以在整个应用中使用的工具文件,并创建一个模态组件。

第四章,预算应用的高级功能,是第二个项目的延续。你将学习如何为用户创建一个类似下拉组件,以便从项目列表中进行选择,并为应用创建标签导航。

第五章,第三个项目 - Facebook 客户端,将开始本书的第三个也是最后一个项目。你将规划一个连接到第三方 Facebook SDK 的应用,将 SDK 安装到你的项目中,允许用户使用 Facebook 凭证登录,然后进行信息请求。

第六章,使用 Facebook 客户端的高级功能,总结了上一章开始的项目。你将学习如何为应用构建下拉刷新机制,为用户渲染图片,允许用户在不离开应用的情况下打开链接,然后使用这些课程内容制作应用的 Android 版本。

第七章,添加 Redux,介绍了流行的 Redux 架构。你将学习如何将第二章中的待办事项应用转换为支持 Redux 原则的应用。

第八章,部署你的应用,展示了如何打包、上传并使你的应用可在 Apple iOS 应用商店和 Google Play 商店下载。你还将获得一些创建应用标志和截图的技巧,以及如何启动应用的 beta 测试。

第九章,额外的 React Native 组件,深入探讨了我们在本书其他部分未能涵盖的 React Native SDK 的部分。在其中,你将构建一个游乐场风格的 app,学习 SDK 的不同部分。你将从任何第三方端点获取数据,控制用户的振动马达,通过你的 app 中的链接打开其他已安装的应用,等等。你还将学习如何将第四章中的预算应用转换为 Android,因为那个章节的空间有限。

你需要这本书什么

在硬件方面,你需要一台 Mac 来使用这本书。本书的内容以 iOS 为主,要开发 iOS 应用,你必须拥有苹果电脑。可选地,iOS 和 Android 设备对于在设备上测试应用会有所帮助,但不是必需的。本书的最后一章有一个 API 需要物理设备来测试(振动),另一个 API 在物理设备上测试会更简单(链接)。

你需要为你的 Mac 安装 React Native SDK。安装说明可以在facebook.github.io/react-native/docs/getting-started.html找到。安装 React Native SDK 的先决条件可以在该页面上找到。

安装 Xcode 和 Android Studio 的说明也可以在该页面上找到,用于在你的机器上安装 React Native SDK。

这本书面向的对象

如果你热衷于学习使用革命性的移动开发工具 React Native 来构建原生移动应用,那么这本书就是为你准备的。具备 JavaScript 的经验会有所帮助。

习惯用法

在这本书中,你会找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"基于这种布局,我们可以看到我们应用的 iOS 版本入口点是index.ios.js,并且生成了一个特定的iOS文件夹(以及相应的Android文件夹)。"

代码块设置如下:

class Tasks extends Component { 
  render () { 
    return ( 
      <View style = {{ flex: 1, justifyContent: 'center',  
        alignItems: 'center', backgroundColor: '#F5FCFF'  
      }}> 
        <Text style = {{ fontSize: 20, textAlign:  
          'center', margin: 10 }}> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
}

任何命令行输入或输出如下所示:

react-native init Tasks

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"当你打开开发者菜单时,你会看到以下选项。"

警告或重要注意事项以如下框的形式出现。

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要发送一般反馈,请简单地将电子邮件发送到feedback@packtpub.com,并在邮件主题中提及本书的标题。

如果您在某个主题上具有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入本书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 点击“代码下载”。

文件下载完成后,请确保使用最新版本的软件解压缩或提取文件夹:

  • Windows 版的 WinRAR / 7-Zip

  • Mac 版的 Zipeg / iZip / UnRarX

  • Linux 版的 7-Zip / PeaZip

本书代码包也托管在 GitHub 上github.com/PacktPublishing/React-Native-By-Example。我们还有其他来自我们丰富图书和视频目录的代码包可供选择,请访问github.com/PacktPublishing/。查看它们吧!

下载本书的颜色图像。

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/ReactNativeByExample_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

在互联网上,版权材料盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过copyright@packtpub.com与我们联系,并附上涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面所提供的帮助。

询问

如果您对本书的任何方面有问题,您可以联系我们的questions@packtpub.com,我们将尽力解决问题。

第一章:第一个项目 - 创建基本待办事项应用程序

在前言中我们已经为 React Native 开发设置了环境,现在让我们开始开发应用程序。在整个本书中,我将使用我最初开始的项目名称——Tasks来指代这个应用程序。在本章中,我们将涵盖以下主题:

  • 规划待办事项应用程序应具备的功能

  • 基本项目架构

  • 介绍StyleSheet,这是 React Native 用于处理样式的组件

  • Flexbox 概述,这是一种受 CSS 启发的布局模式,用于在 React Native 中进行样式设计

  • 熟悉 ES6,我们将使用的新 JavaScript 语法

  • 使用TextInputListViewAsyncStorageInput、状态和属性创建Tasks的构建块

  • 了解 iOS 模拟器的开发者菜单,这有助于我们在编写应用程序时

初始化新项目

由于已经安装了 React Native SDK,因此初始化新的 React Native 项目就像使用以下命令行一样简单:

react-native init Tasks 

让 React Native 命令行界面工作一会儿,然后完成后再打开名为Tasks的目录。

从那里开始,在 iOS 模拟器中运行您的应用程序就像输入以下命令一样简单:

react-native run-ios 

这将启动构建和编译您的 React Native 应用程序的过程,启动 iOS 模拟器,将应用程序导入模拟器,并启动它。每次您对应用程序进行更改时,您都可以立即重新加载并看到这些更改。

功能规划

在编写任何代码之前,我想花时间规划我在项目中的目标,并确定一个最小可行产品(MVP)作为构建任何高级功能之前的目标。这有助于确定我们应用程序的关键组件,以便我们有一个可以运行的原型。

对我来说,最小可行产品(MVP)是一种将我的想法量化为可以互动并用于验证任何假设或捕捉任何边缘情况的方法,同时将所需的时间降到最低。以下是我如何进行功能规划:

  • 我正在构建的产品做什么?

  • 理想情况下,有哪些突出特点使这个应用程序脱颖而出?

  • 在前面的列表中,哪些功能是构建一个工作产品所必需的?一旦你知道了必要的功能,就删除所有不提供基本功能的东西。

  • 考虑其设计,但暂时不要对每个细节都过于纠结。

带着这些意图,以下是我想到的:

  • 这是一个让我能够创建和跟踪任务列表的应用程序。这些可以小到购物清单,也可以大到长期目标。

  • 我希望为每个独特的任务设置一个提醒,这样我就可以有序地完成每个任务。理想情况下,列表中的项目可以按类别分组。类别分组可能可以通过某种像图标这样的东西来简化。这样,我也可以通过图标对列表进行排序和筛选。

  • 从一开始,唯一必要的事情是,我可以用一个文本输入字段来输入任务,将其渲染到项目列表中,并在完成时标记它们;其他所有事情都是次要的。

现在我们对应用有了更清晰的了解,让我们分解一些我们可以采取的具体步骤来实现它:

  1. 让我们生成一个默认项列表。这些项不需要手动输入,因为我们只想在应用本身中看到我们的列表被填充。

  2. 之后,你的用户应该能够使用文本字段和原生键盘输入他们自己的任务

  3. 接下来,我想使那个列表可滚动,以防我的任务列表超过了整个垂直屏幕的高度。

  4. 然后,我们应该通过某种视觉指示器让项目标记为完成

就这些了!这是我们目前拥有的四个目标。正如我之前提到的,其他所有事情目前都是次要的。现在,我们只想尽快推出一个最小可行产品(MVP),然后我们将在之后根据我们的意愿对其进行调整。

让我们继续前进,开始思考架构。

项目架构

下一个重要的事情是我想要解决的问题是架构;这是关于我们的 React Native 应用如何布局的问题。虽然我们为这本书构建的项目旨在单独完成,但我坚信,始终以期望下一个人查看它时是一个脾气暴躁的斧头杀手的方式来编写和架构代码是很重要的。这里的想法是使任何人都能查看你的应用程序的结构,并能够跟随。

首先,让我们看看 React Native CLI 是如何构建我们的项目的;每个相关文件的注释都记在双斜杠(//)的右侧:

|Tasks // root folder
|__Android*
|__ios*
|__node_modules
|__.buckconfig
|__.flowconfig
|__.gitignore
|__.watchmanconfig
|__index.android.js // Android entry point
|__index.ios.js // iOS entry point
|__package.json // npm package list

AndroidiOS文件夹将深入几层,但这都是其构建过程的一部分,我们目前不需要担心这一点。

根据这个布局,我们可以看到我们应用的 iOS 版本入口是index.ios.js,并且生成了一个特定的iOS文件夹(以及相应的Android文件夹)。

而不是使用这些特定平台的文件夹来存储仅适用于一个平台的组件,我建议在这些文件夹旁边创建一个名为app的文件夹,它将封装我们编写的所有逻辑。

在这个app文件夹内,我们将有包含我们的组件和资源的子文件夹。对于组件,我希望将它的样式表与其 JS 逻辑耦合在其自己的文件夹中。

此外,组件文件夹不应该嵌套,否则会变得非常难以跟踪和搜索。相反,我更喜欢使用一种命名约定,使一个组件与其父/子/兄弟的关系立即显而易见。

这就是我的建议结构将看起来:

|Tasks 
|__app 
|____components 
|______TasksList 
|________index.js 
|________styles.js 
|______TasksListCell 
|________index.js 
|________styles.js 
|______TasksListInput 
|________index.js 
|________styles.js 
|____images 
|__Android 
|__ios 
|__node_modules 
|__.buckconfig 
|__.flowconfig 
|__.gitignore 
|__.watchmanconfig 
|__index.android.js 
|__index.ios.js 
|__package.json 

只从快速观察中,你可能能够推断出TasksList是处理屏幕上显示的任务列表的组件。TasksListCell将是列表中的每一行,而TasksListInput将处理键盘输入字段。

这非常基础,我们可以进行一些优化。例如,我们可以考虑 iOS 和 Android 的平台特定扩展,以及为 Redux 构建更进一步的架构;但出于这个特定应用的目的,我们只需从基础开始。

样式表

React Native 的核心视觉组件接受一个名为style的属性,其名称和值与 CSS 的命名约定大致相同,但有一个主要例外——kebab-case 被替换为 camelCase,这与 JavaScript 中的命名方式相似。例如,CSS 属性background-color在 React Native 中会转换为backgroundColor

为了可读性和重用,将内联样式拆分到自己的styles对象中是有益的,通过使用 React Native 的StyleSheet组件定义所有样式到styles对象中,并在组件的render方法中引用它。

再进一步,对于大型应用,最好将样式表分离到自己的 JavaScript 文件中以提高可读性。让我们看看这些如何比较,使用为我们生成的非常注解的 Hello World 示例。这些示例将只包含使我的观点成立的必要代码。

内联样式

内联样式是在你的代码标记内定义的样式。看看这个示例:

class Tasks extends Component { 
  render () { 
    return ( 
      <View style = {{ flex: 1, justifyContent: 'center',  
        alignItems: 'center', backgroundColor: '#F5FCFF'  
      }}> 
        <Text style = {{ fontSize: 20, textAlign:  
          'center', margin: 10 }}> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
} 

在前面的代码中,你可以看到内联样式如何创建一个非常复杂和混乱的混乱,特别是当我们想要将多个样式属性应用到每个组件上时。在大型应用中,我们这样编写样式并不实用,所以让我们将样式拆分成一个StyleSheet对象。

使用样式表,在同一个文件中

这就是组件如何访问同一文件中创建的StyleSheet

class Tasks extends Component { 
  render () { 
    return ( 
      <View style = { styles.container }> 
        <Text style = { styles.welcome }> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
} 

const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: '#F5FCFF' 
  }, 
  welcome: { 
    fontSize: 20, 
    textAlign: 'center', 
    margin: 10 
  } 
)}; 

这样要好得多。我们将样式移动到一个对象中,这样我们就可以引用它,而无需反复重写相同的内联样式。然而,我们面临的问题是文件非常长,包含大量的应用逻辑,未来的维护者可能需要滚动查看一行又一行的代码才能找到样式。我们可以更进一步,将样式分离到它们自己的模块中。

作为导入模块的样式表

在你的组件中,你可以像下面这样导入你的样式:

import styles from './styles.js'; 

class Tasks extends Component { 
  render(){ 
    return ( 
      <View style = { styles.container }> 
        <Text style = { styles.welcome }> 
          Welcome to React Native! 
        </Text> 
      </View> 
    ) 
  } 
} 

然后,你可以在一个单独的文件中定义它们:

const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: '#F5FCFF' 
  }, 
  welcome: { 
    fontSize: 20, 
    textAlign: 'center', 
    margin: 10 
  } 
)}; 

export default styles; 

这要好得多。通过将我们的样式逻辑封装到自己的文件中,我们正在分离我们的关注点,使每个人都能更容易地阅读它。

Flexbox

你可能注意到了我们的 StyleSheet 中有一个名为 flex 的属性。这与 Flexbox 有关,Flexbox 是一种 CSS 布局系统,它可以在不同屏幕尺寸之间提供布局的一致性。React Native 中的 Flexbox 与其 CSS 规范类似,只有一些差异。需要注意的最重要差异是,在 React Native 中,默认的 flex 方向被反转到 column,而在 Web 中是 row,默认将项目对齐到 stretch 属性,而不是浏览器中的 flex-start,React Native 中的 flex 参数只支持单个数字作为其值。

随着我们通过这些项目,我们将深入了解 Flexbox;我们将从查看基础知识开始。

flex

你的布局的 flex 属性在操作上与 CSS 中的操作略有不同。在 React Native 中,它接受一个单个数字。如果这个数字是正数(意味着大于 0),具有此属性的组件将变得灵活。

flexDirection

你的布局也接受一个名为 flexDirection 的属性。这个属性有四个选项:rowrow-reversecolumncolumn-reverse。这些选项决定了你的 flex 容器子项的布局方向。

使用 ES6 编写

ECMAScript 版本 6ES6)是 JavaScript 语言的最新规范。它也被称为 ES2016。它为 JavaScript 带来了新的特性和语法,这些是你在本书中取得成功应该熟悉的内容。

首先,require 语句现在是 import 语句。它们用于从外部模块或脚本中导入函数、对象等。在过去,为了在文件中包含 React,我们会写类似这样的内容:

var React = require('react'); 
var Component = React.Component; 

使用 ES6 的 import 语句,我们可以将其重写为:

import React, { Component } from 'react'; 

在花括号周围导入 Component 的操作称为解构赋值。这是一种赋值语法,允许我们从数组或对象中提取特定数据到变量中。通过解构赋值导入 Component,我们可以在代码中直接调用 Component;它自动声明为具有相同名称的变量。

接下来,我们将用两个不同的语句替换 varletconst。第一个语句 let 声明了一个块级作用域变量,其值可以被修改。第二个语句 const 声明了一个块级作用域变量,其值不能通过重新赋值或重新声明来改变。

在先前的语法中,导出模块通常使用 module.exports 完成。在 ES6 中,这通过 export default 语句来实现。

构建应用

回到几页前的列表,这是我想在应用中做的第一件事:

  • 让我们生成一个默认项的列表。这些项不必手动输入;我们只想看到我们的列表在应用本身中被填充。

ListView

当查看 React Native 组件的文档时,你可能会注意到一个名为ListView的组件。这是一个旨在显示垂直滚动数据列表的核心组件。

下面是如何ListView工作的。我们将创建一个数据源,用数据块数组填充它,创建一个以该数组作为数据源的ListView组件,并在其renderRow回调中传递一些 JSX,该回调将获取数据并为数据源中的每个数据块渲染一行。

从高层次来看,它看起来是这样的:

class TasksList extends Component { 
  constructor (props) { 

    super (props); 

    const ds = new ListView.DataSource({ 
      rowHasChanged: (r1, r2) => r1 !== r2 }); 

    this.state = { 
      dataSource: ds.cloneWithRows(['row 1', 'row 2']) 
    }; 
  } 

  render () { 
    return ( 
      <ListView 
        dataSource = { this.state.dataSource } 
        renderRow = { (rowData) => <Text>  
          { rowData } </Text> } 
      /> 
    ); 
  } 
} 

让我们看看发生了什么。在我们组件的constructor中,我们创建了一个ListViewDataSource的实例。一个新的ListViewDataSource的构造函数接受一个参数,该参数可以包含以下四个之一:

  • getRowData(dataBlob, sectionID, rowID)

  • getSectionHeaderData(dataBlob, sectionID)

  • rowHasChanged(previousRowData, nextRowData)

  • sectionHeaderHasChanged(previousSectionData, nextSectionData)

getRowData是一个获取渲染行所需数据的函数。你可以按自己的喜好自定义该函数,并将其传递给ListViewDataSource的构造函数,但如果未指定,ListViewDataSource将提供默认值。

getSectionHeaderData是一个函数,它接受一个数据块和一个部分 ID,并返回仅用于渲染部分标题所需的数据。像getRowData一样,如果没有指定,它将提供默认值。

rowHasChanged是一个函数,它作为性能优化设计,仅重新渲染其源数据已更改的任何行。与getRowDatagetSectionHeaderData不同,你需要传递自己的rowHasChanged版本。先前的示例,它接受当前和之前的行值并返回一个布尔值以显示它是否已更改,是最常见的实现。

sectionHeaderHasChanged是一个可选函数,它比较部分标题的内容以确定它们是否需要重新渲染。

然后,在我们的TasksView构造函数中,我们的状态接收一个名为dataSource的属性,其值等于调用我们之前创建的ListViewDataSource实例上的cloneWithRowscloneWithRows接受两个参数:一个dataBlob和一个rowIdentitiesdataBlob是传递给它的任何任意数据块,而rowIdentities代表行标识符的二维数组。rowIdentities是一个可选参数——它不包括在先前的示例代码中。我们的示例代码传递了一个硬编码的数据块——两个字符串:'row 1''row 2'

现在也很重要地提到,我们dataSource中的数据是不可变的。如果我们想稍后更改它,我们必须从dataSource中提取信息,对其进行修改,然后替换dataSource中的数据。

TasksList 中渲染的 ListView 组件本身可以接受许多不同的属性。其中最重要的一个,我们在我们的示例中使用,是 renderRow

renderRow 函数从你的 ListViewdataSource 中获取数据,并为你的 dataSource 中的每一行数据返回一个要渲染的组件。在我们的前一个例子中,renderRow 从我们的 dataSource 中的每个字符串中获取数据,并在 Text 组件中渲染它。

使用前面的代码,以下是 TasksList 将如何渲染。因为我们还没有给它添加样式,所以你会看到 iOS 状态栏覆盖了第一行:

太好了!没有太多可以看的,但我们已经完成了一些事情:我们创建了一个 ListView 组件,传递了一些数据,并将这些数据渲染到了我们的屏幕上。让我们退后一步,在我们的应用程序中正确地创建这个组件。

创建 TasksList 组件

回到之前提出的文件结构,你的项目应该看起来像这样:

让我们从编写我们的第一个组件——TasksList 模块开始。

我们首先需要做的是导入我们对 React 的依赖:

import React, { Component } from 'react'; 

然后,我们将从 React Native (react-native) 库中导入我们需要的构建块:

import { 
  ListView, 
  Text 
} from 'react-native'; 

现在,让我们编写组件。在 ES6 中创建新组件的语法如下:

export default class TasksList extends Component { 
  ... 
} 

从这里,让我们给它一个在创建时触发的构造函数:

export default class TasksList extends Component { 
  constructor (props) { 
    super (props); 
    const ds = new ListView.DataSource({ 
     rowHasChanged: (r1, r2) => r1 !== r2 
    }); 

    this.state = { 
     dataSource: ds.cloneWithRows([ 
        'Buy milk', 
        'Walk the dog', 
        'Do laundry', 
        'Write the first chapter of my book' 
      ]) 
    }; 
  } 
} 

我们的构造函数在 TasksList 的状态中设置一个 dataSource 属性,等于一个硬编码的字符串数组。我们的首要目标仍然是简单地在一个屏幕上渲染一个列表。

接下来,我们将利用 TasksList 组件的 render 方法来完成这个任务:

  render () { 
    return ( 
      <ListView 
        dataSource={ this.state.dataSource } 
        renderRow={ (rowData) =>  
          <Text> { rowData } </Text> } 
      /> 
    ); 
  } 

合并起来,代码应该看起来像这样:

// Tasks/app/components/TasksList/index.js 

import React, { Component } from 'react'; 

import { 
  ListView, 
  Text 
} from 'react-native'; 

export default class TasksList extends Component { 
  constructor (props) { 
    super (props); 

    const ds = new ListView.DataSource({ 
      rowHasChanged: (r1, r2) => r1 !== r2 
    }); 

    this.state = { 
      dataSource: ds.cloneWithRows([ 
        'Buy milk', 
        'Walk the dog', 
        'Do laundry', 
        'Write the first chapter of my book' 
      ]) 
    }; 
  } 

  render () { 
    return ( 
      <ListView 
        dataSource={ this.state.dataSource } 
        renderRow={ (rowData) => 
          <Text>{ rowData }</Text> } 
      /> 
    ); 
  } 
} 

太好了!这应该就足够了。然而,我们需要将这个组件链接到我们应用程序的入口点。让我们跳转到 index.ios.js 并做一些更改。

将 TasksList 链接到 index

我们 iOS 应用程序的入口点是 index.ios.js,它渲染的所有内容都从这里开始。现在,如果你使用 react-native run-ios 命令启动 iOS 模拟器,你将看到我们在前言中熟悉的相同的 Hello World 示例应用程序。

我们现在需要做的是将我们刚刚构建的 TasksList 组件链接到 index,并自动移除所有不必要的 JSX。让我们继续清除 Tasks 组件的 render 方法中的几乎所有内容,除了顶层的 View 容器。当你完成时,它应该看起来像这样:

class Tasks extends Component { 
  render () { 
    return ( 
      <View style={styles.container}> 
      </View> 
    ); 
  } 
} 

我们希望在 View 容器中插入 TasksList。然而,在我们这样做之前,我们必须让 index 文件能够访问该组件。让我们使用一个 import 语句来完成:

import TasksList from './app/components/TasksList'; 

虽然这个 import 语句只是指向我们的 TasksList 组件所在的文件夹,但 React Native 智能地寻找一个名为 index 的文件,并将其分配给我们想要的。

现在TasksList已经可以供我们使用了,让我们将其包含在Tasksrender方法中:

export default class Tasks extends Component { 
  render () { 
    return ( 
      <View style={styles.container}> 
        <TasksList /> 
      </View> 
    ); 
  } 
} 

如果你不再运行 iOS 模拟器,让我们使用之前提到的react-native run-ios命令将其重新启动并运行。一旦加载完成,你应该看到以下内容:

这太棒了!一旦加载完成,让我们通过按键盘上的Command + D来打开 iOS 模拟器开发者菜单,并搜索一个可以帮助我们在创建应用程序时节省时间的选项。

在本节的结尾,你的index.ios.js文件应该看起来像这样:

// Tasks/index.ios.js 

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  StyleSheet, 
  View 
} from 'react-native'; 

import TasksList from './app/TasksList'; 

export default class Tasks extends Component { 
  render() { 
    return ( 
      <View style={styles.container}> 
        <TasksList /> 
      </View> 
    ); 
  } 
} 

以下代码渲染了TasksList组件:

const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: '#F5FCFF', 
  } 
}); 

AppRegistry.registerComponent('Tasks', () => Tasks); 

iOS 模拟器开发者菜单

当你打开开发者菜单时,你会看到以下选项:

我想要介绍一下这个菜单中的一些选项,这将帮助你使应用程序的开发过程更加顺畅。一些选项在这里没有涵盖,但你可以在 React Native 文档中阅读有关这些选项的内容。

首先,我们将介绍重载的选项:

  • 重载:这个选项会重新加载你的应用程序代码。类似于在键盘上使用Command + R,重载选项会将你带到应用程序流程的开始。

  • 启用实时重载:开启实时重载会导致你的应用程序在你在项目中保存文件时自动执行重载操作。实时重载很棒,因为你一旦启用它,每次你保存文件时,你的应用程序都会显示其最新的更改。重要的是要知道,重载和启用实时重载都会执行一个完整的重载操作,包括重置你的应用程序状态。

  • 启用热重载:热重载是 React Native 在 2016 年 3 月引入的一个新功能。如果你在 Web 上使用过 React,这个术语可能对你来说很熟悉。热重载的想法是保持你的应用程序运行,并在运行时注入新代码,这样可以防止你像重载(或扩展到启用实时重载)那样丢失应用程序状态。

    • 在开启实时重载的情况下构建功能的一个瓶颈是,当你处理一个多层深度的功能并且依赖于你的应用程序状态来正确地记录对其的更改时。这会给你的应用程序编写和重载的反馈循环增加几秒钟。热重载可以解决这个问题,让你的反馈循环减少到一秒或两秒以下。

    • 在热重载方面需要注意的一点是,在其当前版本中,它并不完美。React Native 文档指出,在某些情况下,你需要使用常规的重载来重置你的应用程序,因为热重载失败了。

同样重要的是要知道,如果你在应用程序中添加新的资产或修改原生 Objective-C/Swift 或 Java/C++代码,你的应用程序在更改生效之前需要完全重新构建。

接下来的一组选项与调试有关:

  • 远程调试 JS:启用此功能将在您的机器上打开 Chrome,并带您到一个 Chrome 标签页,允许您使用 Chrome 开发者工具来调试您的应用程序。

  • 显示检查器:类似于在 Web 上检查元素,您可以使用 React Native 开发中的检查器来检查您的应用程序中的任何元素,并打开影响该元素的部分代码和源代码。您还可以通过这种方式查看每个特定元素的性能。

使用开发者菜单,我们将启用热重载。这将给我们提供关于我们正在编写的代码的最快反馈循环,使我们能够高效地工作。

现在我们已经启用了热重载并有一个基本的任务列表渲染到屏幕上,是时候考虑输入了--我们稍后再回来讨论样式。

TextInput

构建最小可行产品(MVP)的第二个目标如下:

  • 我们的用户应该能够使用文本字段和原生键盘输入他们自己的任务

为了成功创建这个输入,我们必须将问题分解为一些必要的要求:

  • 我们需要一个输入字段,以便弹出键盘进行输入

  • 当我们点击键盘外部时,键盘应该自动隐藏

  • 当我们成功添加一个任务时,它需要添加到 TasksList 中的 dataSource,它存储在其状态中

  • 需要将任务列表存储在应用程序的本地,这样在状态重置时不会删除我们创建的所有任务列表

  • 我们还应该解决几条道路上的分歧:

    • 当用户在键盘上按回车键时会发生什么?它会自动创建一个任务吗?或者,我们实现并支持换行?

    • 是否有一个专门的 添加此任务 按钮?

    • 成功添加任务的动作会导致键盘消失,需要用户再次点击输入字段吗?或者,我们允许用户在点击键盘外部之前继续添加任务?

    • 我们支持多少个字符?任务的长度有多长才算太长?如果用户超过这个限制,我们的软件用户将得到什么样的反馈?

这需要吸收很多信息,所以让我们一步一步来!我将建议我们现在忽略重大决策,先实现屏幕上的输入,然后让这个输入添加到我们的任务列表中。

由于输入应该保存到状态并在 ListView 中渲染,因此输入组件作为 ListView 的兄弟组件是有意义的,这样它们就可以共享相同的状态。

从架构上讲,TasksList 组件将看起来是这样的:

|TasksList 
|__TextInput 
|__ListView 
|____RowData 
|____RowData 
|____... 
|____RowData 

React Native 的 API 中有一个 TextInput 组件,它满足了我们对键盘输入的需求。其代码是可定制的,并允许我们将输入添加到我们的任务列表中。

这个 TextInput 组件可以接受多种属性。我在这里列出了我们将使用的属性,但 React Native 的文档将提供更多深度:

  • autoCorrect: 这是一个布尔值,用于开启和关闭自动更正。默认设置为true

  • onChangeText: 这是一个回调,当输入字段的文本发生变化时触发。组件的值作为参数传递给回调

  • onSubmitEditing: 这是一个回调,当单行输入的提交按钮被按下时触发

  • returnKeyType: 这设置返回键的标题为许多不同的字符串之一;donegonextsearchsend是两个平台都支持的五个选项

我们可以将当前任务分解为几个小步骤:

  • index.ios.js中更新容器样式,使其内容占据整个屏幕而不是只是中央

  • 将一个TextInput组件添加到TasksList组件的render方法中

  • TextInput组件创建一个提交处理程序,该处理程序将文本字段的值添加到ListView

  • 提交后清除TextInput的内容,为下一个要添加的任务留下一个空白字段

抽点时间尝试将这个第一个功能添加到我们的应用中!在下一节中,我将分享一些我的结果截图,并分解我为它编写的代码。

这里有一个屏幕来展示我这一阶段的输入:

它符合前面章节中列出的四个基本要求:内容不在屏幕中央,顶部渲染了一个TextInput组件,提交处理程序将TextInput组件的值添加到ListView,并且一旦发生,TextInput的内容就会被清空。

让我们看看代码,看看我是如何处理的——你的可能不同!:

// Tasks/index.ios.js 

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View 
} from 'react-native'; 

import TasksList from './app/components/TasksList'; 

export default class Tasks extends Component { 
  render() { 
    return ( 
      <View> 
        <TasksList /> 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent('Tasks', () => Tasks);

这是TasksList的更新样式:

// Tasks/app/components/TasksList/styles.js

import { StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  container: { 
    flex: 1 
  } 
}); 

export default styles; 

我在这里所做的就是移除了容器的justifyContentalignItems属性,这样项目就不会仅限于显示的中央。

接下来是TasksList组件,我进行了一些重大更改:

// Tasks/app/components/TasksList/index.js 

import React, { Component } from 'react'; 

import { 
  ListView, 
  Text, 
  TextInput, 
  View 
} from 'react-native'; 

import styles from './styles'; 

export default class TasksList extends Component { 
  constructor (props) { 
    super (props); 

    const ds = new ListView.DataSource({ 
      rowHasChanged: (r1, r2) => r1 !== r2 
    }); 

    this.state = { 
      ds: new ListView.DataSource({ 
        rowHasChanged: (r1, r2) => r1 !== r2 
      }), 
      listOfTasks: [], 
      text: '' 
    }; 
  } 

构造函数现在将三件事保存到状态中:我们的本地ListView.DataSource实例、一个空字符串以跟踪TextInput的值,以及一个用于存储任务列表的数组。

render函数创建了一个dataSource的引用,我们将使用它来为ListView组件,克隆状态中存储的listOfTasks数组。再次强调,ListView只呈现纯文本:

  render () { 
    const dataSource = 
    this.state.ds.cloneWithRows(this.state.listOfTasks); 

TextInput组件有几个选项。它将其输入字段的value绑定到我们的状态中的text值,随着字段的编辑而重复更改。通过在键盘上按下完成键提交它时,它触发一个名为_addTask的回调:

    return ( 
      <View style={ styles.container }> 
        <TextInput 
          autoCorrect={ false } 
          onChangeText={ (text) => this._changeTextInputValue(text) } 
          onSubmitEditing={ () => this._addTask() } 
          returnKeyType={ 'done' } 
          style={ styles.textInput } 
          value={ this.state.text } 
        /> 

它渲染一个ListView组件,其中_renderRowData方法负责返回组件的每一行:

        <ListView 
          dataSource={ dataSource } 
          enableEmptySections={ true } 
          renderRow={ (rowData) => this._renderRowData(rowData) } 
        /> 
      </View> 
    ); 
  } 

我喜欢在我自己创建的 React 组件的方法名前加上下划线,这样我就可以从默认的生命周期方法中视觉上区分它们。

_addTask 方法使用 ES6 中引入的数组扩展运算符来创建一个新的数组,并将现有数组的值复制过来,将最新的任务添加到列表的末尾。然后,我们将它分配给状态中的 listOfTasks 属性。记住,我们必须将组件状态视为不可变对象,直接向其推送将是一个反模式:

_addTask () { 
    const listOfTasks = [...this.state.listOfTasks, this.state.text]; 

    this.setState({ 
      listOfTasks 
    }); 

    this._changeTextInputValue('' 
  } 

最后,我们调用 _changeTextInputValue 以清空 TextInput 框:

  _changeTextInputValue (text) { 
    this.setState({ 
      text 
    }); 
  } 

  _renderRowData (rowData) { 
    return ( 
      <Text>{ rowData }</Text> 
    ) 
  } 
} 

目前,只返回待办事项列表项的名称就足够了。

_addTask 方法中设置 listOfTasks 属性和在 _changeTextInputValue 中设置 text 属性时,我正在使用 ES6 的新特性,称为简写属性名,将值分配给与值同名的键。这相当于我写下以下内容:

this.setState({ 
  listOfTasks: listOfTasks, 
  text: text 
}) 

继续前进,你可能注意到,当你刷新应用程序时,你会丢失你的状态!这对于待办事项列表应用来说是不切实际的,因为我们不应该期望用户在重新打开应用时重新输入相同的列表。我们想要的是将此任务列表本地存储在设备上,以便我们可以在需要时访问它。这就是 AsyncStorage 发挥作用的地方。

AsyncStorage

AsyncStorage 组件是一个简单的键值存储,它对您的 React Native 应用程序全局可用。它是持久的,这意味着 AsyncStorage 中的数据将在退出或重新启动应用程序或手机时继续存在。如果你已经使用过 HTML 的 LocalStorageSessionStorage,那么 AsyncStorage 将看起来很熟悉。它适用于轻量级使用,但 Facebook 建议你在 AsyncStorage 之上使用抽象层,以处理更复杂的情况。

如其名所示,AsyncStorage 是异步的。如果你还没有接触过异步 JavaScript,这意味着这个存储系统的方法可以与你的其他代码并发运行。AsyncStorage 的方法返回一个 Promise——一个表示尚未完成但预期将来会完成的操作的对象。

AsyncStorage 中的每个方法都可以接受一个回调函数作为参数,并在 Promise 履行后触发该回调。这意味着我们可以编写我们的 TasksList 组件来处理这些承诺,在需要时保存和检索我们的任务数组。

关于 AsyncStorage 的最后一件事——它是一个简单的键值存储。它期望其键和值都是字符串,这意味着我们需要使用 JSON.stringify 将发送到存储的数据转换为字符串,并在检索时使用 JSON.parse 将其转换回数组。

玩转 AsyncStorage 并更新你的 TasksList 组件以支持它。以下是你希望使用 AsyncStorage 达成的目标:

  • 一旦 TasksList 加载,我们希望查看是否在本地存储中存在任何任务。如果存在,向用户展示这个列表。如果不存在,从空数组开始存储。数据应该始终在重启后持久化。

  • 当输入任务时,我们应该更新任务列表,将更新后的列表保存到 AsyncStorage 中,然后更新 ListView 组件。

这是最终写出的代码:

// TasksList/app/components/TasksList/index.js

... 
import { 
  AsyncStorage, 
  ... 
} from 'react-native'; 
... 

从 React Native SDK 中导入 AsyncStorage API。

export default class TasksList extends Component { 
  ... 
  componentDidMount () { 
    this._updateList(); 
  } 

componentDidMount 生命周期中调用 _updateList 方法。

  ... 
  async _addTask () { 
    const listOfTasks = [...this.state.listOfTasks, this.state.text]; 

    await AsyncStorage.setItem('listOfTasks', 
    JSON.stringify(listOfTasks)); 

    this._updateList(); 
  } 

_addTask 更新为使用 asyncawait 关键字以及 AsyncStorage。有关使用 asyncawait 的详细信息,请参阅以下内容:

  ... 
  async _updateList () { 
    let response = await AsyncStorage.getItem('listOfTasks'); 
    let listOfTasks = await JSON.parse(response) || []; 

    this.setState({ 
      listOfTasks 
    }); 

    this._changeTextInputValue(''); 
  } 
} 

_updateTask 中,我们使用 AsyncStorage 做的是获取使用 listOfTasks 键本地存储的值。从这里,我们解析结果,将字符串转换回数组。然后,我们检查数组是否存在,如果返回 null,则将其设置为空数组。最后,我们通过更新 listOfTasks 并触发 _changeTextInputValue 来重置 TextInput 值来设置我们组件的状态。

上述示例还使用了 ES7 规范提案中的一部分新 asyncawait 关键字,并且可以与 React Native 一起使用。

使用 Async 和 Await 关键字

通常,为了处理异步函数,我们会将其与一些承诺链式调用,以便获取我们的数据。我们可以这样写 _updateList

_updateList () { 
  AsyncStorage.getItem('listOfTasks'); 
    .then((response) => {fto 
      return JSON.parse(response); 
    }) 
    .then((parsedResponse) => { 
      this.setState({ 
        listOfTasks: parsedResponse 
      }); 
    }); 
} 

然而,这可能会变得相当复杂。相反,我们将使用 asyncawait 关键字来创建一个更简单的解决方案:

async _updateList () { 
  let response = await AsyncStorage.getItem('listOfTasks'); 
  let listOfTasks = await JSON.parse(response) || []; 

  this.setState({ 
    listOfTasks 
  }); 

  this._changeTextInputValue(''); 
} 

_updateList 前面的 async 关键字将其声明为异步函数。它自动为我们返回承诺,并可以利用 await 关键字告诉 JS 解释器暂时退出异步函数,并在异步调用完成后恢复运行。这对我们来说很棒,因为我们可以在单个函数中以顺序表达我们的意图,并且仍然获得与承诺相同的精确结果。

自定义 RenderRow 组件

我们列表中的最后一项,即要有一个可用的最小可行产品,是允许每个任务被标记为完成。这就是我们将创建 TasksListCell 组件并在 ListViewrenderRow 函数中渲染该组件,而不是仅仅显示文本的地方。

我们对这个组件的目标应该是以下内容:

  • 从父组件接受文本作为 prop,并在 TasksListCell 中渲染它

  • listOfTasks 更新为接受对象数组而不是字符串数组,允许每个对象跟踪任务的名称以及它是否已完成

  • 当任务被点击时,提供某种视觉指示器,将任务标记为完成,不仅在视觉上,而且在任务的 data 对象中,这样在应用程序重新加载时也能持久化。

自定义 RenderRow 示例

让我们看看我是如何创建这个组件的:

// Tasks/app/components/TasksList/index.js 

... 
import TasksListCell from '../TasksListCell'; 
... 
export default class TasksList extends Component { 
  ... 
  async _addTask () { 
    const singleTask = { 
      completed: false, 
      text: this.state.text 
    } 

首先,任务现在在数组中以对象的形式表示。这允许我们为每个任务添加属性,例如其完成状态,并为未来的添加留出空间。

    const listOfTasks = [...this.state.listOfTasks, singleTask]; 
    await AsyncStorage.setItem('listOfTasks', 
    JSON.stringify(listOfTasks)); 
    this._updateList(); 
  } 
  ... 
  _renderRowData (rowData, rowID) { 
    return ( 
      <TasksListCell 
        completed={ rowData.completed } 
        id={ rowID } 
        onPress={ (rowID) => this._completeTask(rowID) } 
        text={ rowData.text } 
      /> 
    ) 
  } 
  ... 
} 

_renderRowData 方法也被更新,以渲染新的 TasksListCell 组件。四个 props 被共享到 TasksListCell:任务的完成状态、其行标识符(由 renderRow 提供)、一个用于更改任务完成状态的回调函数,以及该任务本身的详细信息。

这就是 TasksListCell 组件的编写方式:

// Tasks/app/components/TasksListCell/index.js 

import React, { Component, PropTypes } from 'react'; 

import { 
  Text, 
  TouchableHighlight, 
  View 
} from 'react-native'; 

export default class TasksListCell extends Component { 
  static propTypes = { 
    completed: PropTypes.bool.isRequired, 
    id: PropTypes.string.isRequired, 
    onLongPress: PropTypes.func.isRequired, 
    onPress: PropTypes.func.isRequired, 
    text: PropTypes.string.isRequired 
  } 

使用 PropTypes 明确声明组件期望接收的数据。 继续阅读以了解 React 中 prop 验证的解释。

  constructor (props) { 
    super (props); 
  } 

  render () { 
    const isCompleted = this.props.completed ? 'line-through' : 'none'; 
    const textStyle = { 
      fontSize: 20, 
      textDecorationLine: isCompleted 
    }; 

使用三元运算符来计算任务完成时的样式。

    return ( 
      <View> 
        <TouchableHighlight 
          onPress={ () => this.props.onPress(this.props.id) } 
          underlayColor={ '#D5DBDE' } > 
          <Text style={ textStyle }>{ this.props.text }</Text> 
        </TouchableHighlight> 
      </View> 
    ) 
  } 
} 

前面的组件为列表中的每个任务提供了一个 TouchableHighlight,当点击项目时,给我们视觉上的不透明度反馈。它还会触发 TasksListCell_completeTask 方法,随后调用传递给它的 onPress prop,并对单元格的样式进行视觉更改,通过在任务的水平中心画一条线来标记它为完成

React 中的 prop 验证

通过为组件声明一个 propTypes 对象,我可以指定给定组件期望的 props 和它们的类型。这对我们代码的未来维护者很有帮助,并在 props 错误输入或缺失时提供有用的警告。

要利用 prop 验证,首先从 React 导入 PropTypes 模块:

import { PropTypes } from 'react'; 

然后,在我们的组件中,我们给它一个静态属性 propTypes

class Example extends Component { 
  static propTypes = { 
    foo: PropTypes.string.isRequired, 
    bar: PropTypes.func, 
    baz: PropTypes.number.isRequired 
  } 
} 

在前面的示例中,foobazExample 组件的必需 propsfoo 预期是一个字符串,而 baz 预期是一个数字。另一方面,bar 预期是一个函数,但不是必需的 props

超越 MVP

现在我们已经完成了一个非常基础的 MVP,下一个目标是向应用程序添加一些功能,使其变得完整。

这就是我之前提到的某些有用的功能:

我希望为每个独特的任务设置一个提醒,这样我就可以有序地完成每个任务。理想情况下,列表中的项目可以按类别分组。类别分组可能可以通过类似图标的东西来简化。这样,我也可以通过图标对列表进行排序和筛选。

除了功能之外,我们还应该调整应用程序的样式,使其看起来更好。在我的示例代码中,应用程序的组件与 iOS 的状态栏冲突,并且行格式完全没有设置。我们应该给应用程序一个自己的身份。

下一章将更深入地探讨我们的 MVP,并将其转变为一个功能齐全且样式丰富的应用程序。我们还将探讨如果应用程序是为 Android 编写的,我们会做哪些不同的事情。

摘要

在本章中,你通过规划一个最小可行产品版本的待办事项应用开始了你的学习,其中包括向列表中添加任务并将它们标记为已完成。然后,你学习了 React Native 中的基本样式设计,使用 Flexbox,并熟悉了 ES6 规范的新语法和功能。你还发现了 iOS 模拟器的调试菜单,这是一个编写应用的非常有用的工具。

之后,你创建了一个ListView组件来渲染一系列项目,然后实现了一个TextInput组件来保存用户输入并将它渲染到Listview中。接着,你使用了AsyncStorage来持久化用户添加到应用中的数据,利用新的asyncawait关键字编写了干净的异步函数。最后,你实现了一个TouchableHighlight单元格,用于标记任务为已完成。

第二章:高级功能与待办事项应用程序的样式设计

在为 Tasks 应用程序构建了 MVP(最小可行产品)之后,现在是时候深入构建高级功能,并对应用程序进行样式设计,使其看起来更美观。本章将探讨以下主题:

  • 利用 NavigatorIOS 组件构建一个编辑屏幕,以便添加任务的详细信息

  • 使用 DatePickerIOS 捕获任务截止日期和时间

  • 为我们的应用程序创建一个自定义可折叠组件,并利用 LayoutAnimation 来实现流畅的过渡

  • 为我们的 UI 构建一个 Button 组件,以清除待办事项的截止日期

  • 保存已编辑任务的资料,如果适用,则渲染截止日期

  • 将应用程序移植到 Android,用 DatePickerAndroidTimePickerAndroid 替换 DatePickerIOS,用 Navigator 替换 NavigatorIOS,并探索在决定使用哪个组件时的控制流程

导航器和 NavigatorIOS

在移动应用程序中实现导航有助于我们控制用户如何与我们的应用程序互动和体验。它让我们为那些原本没有任何上下文的情况赋予上下文——例如,在 Tasks 中,向用户展示一个尚未选择的任务的编辑视图是没有意义的;只有当用户选择编辑任务时,才向用户展示此视图,这样可以构建情境上下文和意识。

React Native 的 Navigator 组件负责处理应用程序中不同视图之间的转换。浏览文档时,您可能会注意到存在一个 NavigatorNavigatorIOS 组件。Navigator 在 iOS 和 Android 上都可用,并使用 JavaScript 实现。另一方面,NavigatorIOS 仅适用于 iOS,并且是 iOS 原生 UINavigationController 的包装器,它动画化并按照您从任何 iOS 应用程序中期望的方式表现。

在本章的后面部分,我们将更详细地探讨 Navigator。

关于 NavigatorIOS 的重要说明

虽然 NavigatorIOS 支持 UIKit 动画,并且是构建 Tasks iOS 版本的绝佳选择,但需要记住的是,NavigatorIOS 事实上是 React Native SDK 的一个社区驱动组件。Facebook 从一开始就公开表示,它在自己的应用程序中大量使用了 Navigator,但 NavigatorIOS 组件未来改进和添加的所有支持都将直接来自开源贡献。

查看 NavigatorIOS

NavigatorIOS 组件在您的 React Native 应用程序的最顶层设置。我们将提供至少一个对象,标识为 routes,以便识别我们应用程序中的每个视图。此外,NavigatorIOS 会查找一个 renderScene 方法,该方法负责渲染我们应用程序中的每个场景。以下是一个使用 NavigatorIOS 渲染基本场景的示例:

import React, { Component } from 'react'; 
import { 
  NavigatorIOS, 
  Text 
} from 'react-native'; 

export default class ExampleNavigation extends Component { 
  render () { 
    return ( 
      <NavigatorIOS 
        initialRoute={{ 
          component: TasksList, 
          title: 'Tasks' 
        }} 
        style={ styles.container } 
      /> 
 ); 
  } 
} 

这只是一个基本的示例。我们正在初始化 NavigatorIOS 组件,并以一个简单的 text 组件作为基本路由进行渲染。我们真正感兴趣的是在 routes 之间切换以编辑任务。让我们将这个目标分解成一系列更容易处理的子任务:

  • 创建一个新的 EditTask 组件。它可以从一个带有一些填充信息的简单屏幕开始。

  • 设置 NavigatorIOS 以在任务长按时路由到 EditTask

  • EditTask 构建逻辑,使其能够接受作为组件属性的精确任务以渲染特定于任务的数据。添加适当的输入字段,以便此组件可以从编辑屏幕标记为完成,以及具有设置截止日期和标签的能力。

  • 当编辑保存时,添加逻辑将编辑后的数据保存到 AsyncStorage

我们将花一些时间来完成每个步骤,并在必要时进行回顾。花几分钟时间构建一个简单的 EditTask 组件,然后参考我是如何构建的。

一个简单的 EditTasks 组件

在我的应用程序文件夹结构中,我的 EditTasks 组件嵌套如下:

|Tasks 
|__android 
|__app 
|____components 
|______EditTask 
|______TasksList 
|______TasksListCell 
|__ios 
|__node_modules 
|__... 

这是一个基本的组件,只是为了在屏幕上显示一些内容:

// Tasks/app/components/EditTask/index.js 

import React, { Component } from 'react'; 

import { 
  Text, 
  View 
} from 'react-native'; 

import styles from './styles'; 

export default class EditTask extends Component { 
  render () { 
    return ( 
      <View style={ styles.editTaskContainer }> 
        <Text style={ styles.editTaskText }>Editing Task</Text> 
      </View> 
    ); 
  } 
} 

之前的代码现在返回要渲染到屏幕上的文本。

现在是时候设置 NavigatorIOS 以与 TasksList 顺利协作了:

// Tasks/app/components/EditTask/styles.js 

import { Navigator, StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  editTaskContainer: { 
    flex: 1, 
    paddingTop: Navigator.NavigationBar.Styles.General.TotalNavHeight 
  }, 
  editTaskText: { 
    fontSize: 36 
  } 
}) 

export default styles; 

首先,我们应该修改 TasksList 以使其:

  • 添加一个名为 _editTask 的函数,将 EditTask 组件推送到 Navigator

  • _editTask 函数作为名为 onLongPress 的属性传递给 TasksListCell

然后,我们应该修改 EditTask 以确保其 render 方法中的 TouchableHighlight 组件在其自己的 onLongPress 回调期间调用此属性:

// Tasks/app/components/TasksList/index.js 

... 
import EditTask from '../EditTask'; 
... 
export default class TasksList extends Component { 
  ... 
  render () { 
    ... 
    return ( 
      <View style={ styles.container }> 
        ... 
        <ListView 
          ... 
          automaticallyAdjustContentInsets={ false } 
          style={ styles.listView } 
        /> 
      </View> 
    ); 
  } 

我们添加了一个布尔值,用于禁用内容内边距的自动调整。默认设置为 true,我们在 InputListView 组件之间看到了 ~55px 的内边距。在我们的组件和 EditTask 的样式设置中,我们开始导入 Navigator 组件。

这样我们就可以设置容器 paddingTop 属性,考虑到导航栏的高度,以便内容不会被留在导航栏后面。这种情况发生的原因是导航栏在组件加载完成后被渲染。

调用 NavigatorIOSpush 方法,渲染我们刚刚导入的 EditTask 组件:

  ... 
  _editTask (rowData) { 
    this.props.navigator.push({ 
      component: EditTask, 
      title: 'Edit' 
    }); 
  } 

TasksListCell 分配一个名为 onLongPress 的回调,执行我们刚刚定义的 _editTask 方法:

  _renderRowData (rowData, rowID) { 
    return ( 
      <TasksListCell 
        ... 
        onLongPress={ () => this._editTask() } 
      /> 
    ) 
  } 
  ... 
} 

paddingTop 属性设置为 Navigator 的高度,解决了我们的导航栏隐藏其后面的应用内容的问题:

// Tasks/app/components/TasksList/styles.js 

import { Navigator, StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  container: { 
    ... 
    paddingTop: Navigator.NavigationBar.Styles.General.TotalNavHeight 
... 
}); 

export default styles; 

使用 DatePickerIOS

Tasks中,一个关键特性是能够为任务到期时设置提醒。理想情况下,我们的用户可以为任务完成设定日期和时间,以便他们能够被提醒到期日期。为了实现这一点,我们将使用一个名为DatePickerIOS的 iOS 组件。这是一个可以用于我们应用程序中的日期和时间选择器组件。

这里列出了我们将与DatePickerIOS组件一起使用的两个属性。如果你感兴趣,React Native 文档中还有其他属性:

  • date: 这是两个必需属性之一,用于跟踪当前选定的日期。理想情况下,此信息存储在渲染DatePickerIOS组件的状态中。date应该是 JavaScript 中的Date对象实例。

  • onDateChange: 这是另一个必需的属性,当用户在组件中更改日期时间时触发。它接受一个参数,即表示新日期和时间的Date对象。

下面是一个简单的DatePicker组件的示例:

// Tasks/app/components/EditTask/index.js 

... 
import { 
  DatePickerIOS, 
  ... 
} from 'react-native';  
...  
export default class EditTask extends Component { 
  constructor (props) { 
    super (props); 

    this.state = { 
      date: new Date() 
    } 
  } 

它创建一个新的 JavaScript Date对象实例并将其保存到状态中。

  render () { 
    return ( 
      <View style={ styles.editTaskContainer }> 
        <DatePickerIOS 
          date={ this.state.date } 
          onDateChange={ (date) => this._onDateChange(date) } 
          style={ styles.datePicker } 
        /> 
      </View> 
    ); 
  } 

这会导致使用组件状态中的date值作为同名的属性来渲染DatePickerIOS组件。

当用户与DatePickerIOS组件交互时,在组件状态中更改date的回调:

  _onDateChange (date) { 
    this.setState({ 
      date 
    }); 
  } 
} 

这就是渲染后的DatePicker的外观:

这还有许多不足之处。首先,DatePickerIOS组件始终可见!通常,当我们在 iOS 应用程序中与这类选择器交互时,它是折叠的,只有当点击时才会展开。我们想要复制的正是这种确切的经验,即渲染一个可触摸的行,显示当前设置的到期日期或类似未设置到期日期的内容,当行被点击时,动画展开DatePickerIOS

编写可折叠组件

我们的可折叠组件应实现以下目标:

  • 当点击时,它应显示和隐藏传递给它的其他组件

  • 这个组件将伴随动画,增强我们应用程序的用户体验

  • 组件不应对其显示和隐藏的数据类型做出任何假设;它不应严格特定于DatePickerIOS,以防我们将来想要将组件用于其他目的

我们需要利用 React Native 的出色LayoutAnimation API,该 API 旨在让我们创建流畅且富有意义的动画。

首先,我在项目的components文件夹中创建了一个名为ExpandableCell的组件,如下所示:

|Tasks 
|__android 
|__app 
|____EditTask 
|____ExpandableCell 
|____TasksList 
|____TasksListCell 
|__ios 
|__... 

布局动画 API

我们的目标是在EditTask中点击date/time组件,然后使其向下展开以显示隐藏的DatePickerIOS组件。React Native 有一个名为LayoutAnimation的 API,允许我们创建自动动画布局。

LayoutAnimation 包含三个表示默认动画曲线的方法:easeInEaseOutlinearspring。这些决定了动画在其过渡过程中的行为。你可以在 componentWillUpdate 生命周期方法下简单地调用这三个方法之一,如果组件状态的变化触发了重新渲染,LayoutAnimation 将将其动画添加到你的更改中。

要隐藏和显示传递给 ExpandableCell 的子组件,我可以根据组件是否应该显示或隐藏来操作其 maxHeight 样式。此外,我可以通过将 overflow 属性设置为 hidden 来在不需要时隐藏组件。

花些时间隐藏传递给 ExpandableCell 的子组件,并设置一些逻辑来根据需要显示和隐藏此内容。准备好后,查看我的实现。

基本 ExpandableCell 实现

这是我们开始构建 ExpandableCell 的方法:

// Tasks/app/components/ExpandableCell/index.js 

import React, { Component, PropTypes } from 'react'; 

import { 
  LayoutAnimation, 
  Text, 
  TouchableHighlight, 
  View 
} from 'react-native'; 

import styles from './styles'; 

export default class ExpandableCell extends Component {

这将 title 设置为组件期望的 PropTypes 字符串:

  static propTypes = { 
    title: PropTypes.string.isRequired 
  } 

现在我们跟踪组件 state 中的布尔值 expanded。默认情况下,我们的子组件不应可见:

  constructor (props) { 
    super (props); 

    this.state = { 
      expanded: false 
    } 
  } 

设置此组件更改时的 LayoutAnimation 样式:

  componentWillUpdate () { 
    LayoutAnimation.linear(); 
  } 

TouchableHighlight 组件包裹在 ExpandableCellText 组件周围。当按下时,它会调用 _onExpand 方法:

  render () { 
    return ( 
      <View style={ styles.expandableCellContainer }> 
        <View> 
          <TouchableHighlight 
            onPress={ () => this._expandCell() } 
            underlayColor={ '#D3D3D3' } 
          > 

在组件未展开的情况下,向此 View 的样式添加一个 maxHeight 属性,使用三元运算符:

            <Text style={ styles.visibleContent }>
            { this.props.title}</Text> 
          </TouchableHighlight> 
        </View> 
        <View style={ [styles.hiddenContent, 
        this.state.expanded ? {} : {maxHeight: 0}]}> 

这将渲染组件本身嵌套的任何子组件:

          { this.props.children } 
        </View> 
      </View> 
    ) 
  }

以下是一个回调,用于在组件状态中切换 expanded 布尔值:

  _expandCell () { 
    this.setState({ 
      expanded: !this.state.expanded 
    }); 
  } 
} 

这是 ExpandableCell 的样式:

// Tasks/app/components/ExpandableCell/styles.js 

import { StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  expandableCellContainer: { 
    flex: 1, 
    padding: 10, 
    paddingTop: 0 
  }, 
  hiddenContent: { 
    overflow: 'hidden' 
  }, 
  visibleContent: { 
    fontSize: 24 
  } 
}) 

EditTask 中的基本实现如下所示:

// Tasks/app/components/EditTask/index.js 

... 
import ExpandableCell from '../ExpandableCell'; 

export default class EditTask extends Component { 
  ... 

渲染一个带有标题的 ExpandableCell 组件:

render () { 
    return ( 
      <View style={ styles.editTaskContainer }> 
        <ExpandableCell title={ 'Due On' }> 

ExpandableCell 内嵌套 DatePickerIOS 以使其最初保持隐藏:

          <DatePickerIOS 
            ... 
          /> 

        </ExpandableCell> 
      </View> 
    ); 
  } 
  ... 
} 

理想情况下,此组件将显示以下之一:

  • 如果存在,则选择任务的截止日期

  • 如果不存在截止日期,则选择日期的空白占位符

我们将在稍后处理诸如清除截止日期等问题,但现在,我们应该修改 EditTask,使其传递给 ExpandableCelltitle 属性取决于任务是否分配了截止日期。组件当前应该看起来是这样的:

这是解决这个问题的方法。自上一个示例以来,唯一更改的文件是 EditTask 组件:

// Tasks/app/components/EditTask/index.js 

... 
import moment from 'moment'; 
... 
export default class EditTask extends Component { 
  ...  
  render () { 
 const noDueDateTitle = 'Set Reminder'; 
    const dueDateSetTitle = 'Due On ' + this.state.formattedDate; 

设置两个字符串以显示 ExpandableCelltitle 属性。

return ( 
      <View style={ styles.editTaskContainer }> 
        <ExpandableCell 
          title={ this.state.dateSelected ? 
          dueDateSetTitle : noDueDateTitle }> 

使用三元运算符来决定传递给 ExpandableCell 的字符串。

          ... 
        </ExpandableCell> 
      </View> 
    ); 
  } 

  _formatDate (date) { 
    return moment(date).format('lll'); 
  } 

我还从 npm 导入了 moment 以使用其强大的日期格式化功能。Moment 是一个非常流行、广泛使用的库,它允许我们使用 JavaScript 操作日期。安装它就像打开项目根目录的终端并输入以下内容一样简单:

npm install --save moment      

MomentJS 库有很好的文档,其主页位于 momentjs.com,将展示你如何使用它的所有方法。对于这个文件,我使用了 Moment 的格式化方法,并设置为显示缩写月份名称,后跟数字日期和年份,以及时间。

使用 'lll' 标志格式化的 Moment 日期示例如下:

Dec 25, 2016 12:01 AM 

使用 Moment 格式化日期有不同的方式,我鼓励你玩一玩这个库,找到最适合你的日期格式。

dateSelected 设置为 true,并将日期的 Moment 格式版本添加到状态中,这将反过来触发此组件的 render 方法再次更新传递给 ExpandableCelltitle 字符串:

  _onDateChange (date) { 
    this.setState({ 
      ... 
      dateSelected: true, 
      formattedDate: this._formatDate(date) 
    }); 
  } 
} 

到本节结束时,你的应用应该看起来像以下截图:

使用 onLayout

在我们前面的例子中,我们不需要指定 DatePickerIOS 组件在展开时的高度。然而,可能存在需要手动获取组件尺寸的场景。

为了计算组件的高度,我们可以利用其 onLayout 属性来触发一个回调,然后使用该回调保存传递给回调的属性。onLayout 属性是一个在挂载和布局更改时被调用的事件,它给事件对象一个 nativeEvent 对象,该对象嵌套了组件的布局属性。以 DatePickerIOS 为例,你可以像这样将其 onLayout 属性传递一个回调:

<DatePickerIOS 
  date={ this.state.date } 
  onDateChange={ (date) => this._onDateChange(date) } 
  onLayout={ (event) => this._getComponentDimensions(event) } 
  style={ styles.datePicker }  
/> 

onLayout 事件提供了以下属性:

event: { 
  nativeEvent: { 
    layout: { 
      x: //some number 
      y: //some number 
      width: //some number 
      height: //some number 
    } 
  } 
} 

按钮

让我们为 EditTask 组件构建一个 清晰的截止日期 按钮,并且只有当待办事项已选择截止日期时才选择性地启用它。React Native 中的 Button 组件应该能帮助我们快速渲染。

Button 组件接受一些属性;以下四个将在我们的应用程序中使用:

  • color:这是一个字符串(或字符串化的十六进制值),用于设置 iOS 上的文本颜色或 Android 上的背景颜色

  • disabled:这是一个布尔值,如果设置为 true,则禁用按钮;默认为 false

  • onPress:这是一个在按钮被按下时触发的回调

  • title:这是要在按钮内显示的文本

一个示例 Button 组件可以渲染如下:

<Button 
  color={ 'blue' } 
  disabled={ this.state.buttonDisabled } 
  onPress={ () => alert('Submit button pressed') } 
  title={ 'Submit' }  
/> 

修改 EditTask 以使其具有以下功能:

  • 它在其状态中包含一个布尔值,标题为 expanded,用于控制 ExpandableCell 的打开/关闭状态。

  • 它修改了 ExpandableCell 的渲染,以接受 expandedonPress 属性。expanded 属性应指向 EditTask 状态中的 expanded 布尔值,而 onPress 属性应触发一个翻转 expanded 布尔值的方法。

  • onLayout 回调添加到 DatePickerIOS 以计算其高度,并将其保存到状态中。

  • 包含一个具有title属性的Button组件,提示用户清除截止日期。给它一个onPress属性,当按下时会清除状态中的dateSelected布尔值。如果dateSelected布尔值设置为false,则选择性地禁用它。

清除截止日期示例

下面是我为了使按钮能够清除选定的日期并展开/折叠我们的单元格以良好地工作所做的事情:

// Tasks/app/components/EditTask/index.js 

... 
import { 
  Button, 
  ... 
} from 'react-native'; 
... 
export default class EditTask extends Component { 
  constructor (props) { 
    ... 
    this.state = { 
      ... 
      expanded: false 
    } 
  } 

  render () { 
    ... 
    return ( 
      <View style={ styles.editTaskContainer }> 
        <View style={ [styles.expandableCellContainer,
        { maxHeight: this.state.expanded ? 
        this.state.datePickerHeight : 40 }]}> 

我在ExpandableCell周围包裹了一个新的View。其样式根据EditTask状态中的展开Boolean进行修改。如果组件被展开,则其maxHeight属性设置为子组件的高度。否则,它被设置为40像素。

然后,将expandedonPress属性传递给此组件:

          <ExpandableCell 
            ... 
            expanded={ this.state.expanded } 
            onPress={ () => this._onExpand() } 
          > 

onLayout事件期间调用_getDatePickerHeight

            <DatePickerIOS 
              ... 
              onLayout={ (event) => this._getDatePickerHeight(event) } 
            /> 
          </ExpandableCell> 
        </View>

Button组件也被封装在其自己的View中。这样做是为了使ButtonExpandableCell堆叠在一起:

        <View style={ styles.clearDateButtonContainer }> 
          <Button 
            color={ '#B44743' } 
            disabled={ this.state.dateSelected ? false : true } 
            onPress={ () => this._clearDate() } 
            title={ 'Clear Date' } 
          /> 
        </View> 
      </View> 
    ); 
  } 

将状态中的dateSelected布尔值设置为false,更改ExpandableCell传递的title

  _clearDate () { 
    this.setState({ 
      dateSelected: false 
    }); 
  } 

这将DatePickerIOS组件的宽度保存到状态中:

  _getDatePickerHeight (event) { 
    this.setState({ 
      datePickerHeight: event.nativeEvent.layout.width 
    }); 
  } 

  _onExpand () { 
    this.setState({ 
      expanded: !this.state.expanded 
    }); 
  } 
} 

我向此组件的StyleSheet添加了clearDateButtonContainer样式:

// Tasks/app/components/EditTask/styles.js 

import { Navigator, StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  ... 
  clearDateButtonContainer: { 
    flex: 1 
  } 
}) 

export default styles; 

让我们继续工作并在这个屏幕上添加一些更多功能。接下来,我们应该有一个字段来编辑任务名称,紧随其后的是一个用于切换任务完成或不完成状态的Switch组件。

开关

Switch是一个渲染布尔输入并允许用户切换的组件。

使用Switch,我们将使用以下属性:

  • onValueChange: 这是一个回调,当开关的值改变时,会使用新的开关值被调用

  • value: 这是一个布尔值,用于确定开关是否设置为'开启'位置;默认为false

一个简单的Switch组件可能看起来像这样:

<Switch 
  onValueChange={ (value) =? this.setState({ toggled: value })} 
  value={ this.state.toggled } 
/> 

如前所述,Switch有两个必需的属性:其value和一个当切换时更改其值的回调。

使用这些知识,让我们对TasksList组件进行修改,使其将每行的completeddueformattedDatetext属性传递给EditTask组件以供使用。

然后,向EditTask组件添加一些修改,使其:

  • 期望其propTypes声明中包含completeddueformattedDatetext属性。

  • 包含一个预加载待办事项列表项名称的TextInput字段,并允许用户编辑名称。

  • 添加一个预加载待办事项列表项完成状态的Switch组件。当切换时,其完成状态应改变。

这是我想出的解决方案:

// Tasks/app/components/TasksList/index.js 

...  
export default class TasksList extends Component { 
  ... 
  _editTask (rowData) { 
    this.props.navigator.push({ 
      ... 
      passProps: { 
        completed: rowData.completed, 
        due: rowData.due, 
        formattedDate: rowData.formattedDate, 
        text: rowData.text 
      }, 
      ... 
    }); 
  } 
  ... 
} 

EditTask所需的四个字段传递进去,以便视图可以访问渲染待办事项列表项的现有详细信息。如果行不包含这些字段之一或多个,它将传递undefined

声明此组件期望的四个propTypes。由于当应用程序创建待办事项列表项时,只有completedtext是设置的,因此它们被标记为必需属性。

// Tasks/app/components/EditTask/index.js 

import React, { Component, PropTypes } from 'react'; 
... 
import { 
  ... 
  Switch, 
  TextInput, 
  ... 
} from 'react-native'; 
... 
export default class EditTask extends Component { 
  static propTypes = { 
    completed: PropTypes.bool.isRequired, 
    due: PropTypes.string, 
    formattedDate: PropTypes.string, 
    text: PropTypes.string.isRequired 
  } 

  constructor (props) { 
    super (props); 

    this.state = { 
      completed: this.props.completed, 
      date: new Date(this.props.due), 
      expanded: false, 
      text: this.props.text 
    } 
  } 

在状态中使用props被认为是一种反模式,但在这里我们有很好的理由,因为我们将会作为组件的一部分修改这些属性。

在下一节中,我们还将创建一个保存按钮,以便我们可以保存待办事项的更新详情,因此我们需要在状态中有一个本地可用的数据副本来反映EditTask组件的更改。

渲染一个TextInput组件来处理更改待办事项列表项的名称:

  render () { 
    ... 
    return ( 
      <View style={ styles.editTaskContainer }> 
        <View> 
          <TextInput 
            autoCorrect={ false } 
            onChangeText={ (text) => this._changeTextInputValue(text) } 
            returnKeyType={ 'done' } 
            style={ styles.textInput } 
            value={ this.state.text } 
          /> 
        </View> 

ExpandableCell下方但在清除截止日期Button上方渲染Switch

        ... 
        <View style={ styles.switchContainer } > 
          <Text style={ styles.switchText } > 
            Completed 
          </Text> 
          <Switch 
            onValueChange={ (value) => this._onSwitchToggle(value) } 
            value={ this.state.completed } 
          /> 
        </View> 
        ... 
      </View> 
    ); 
  } 

以下回调方法更改TextInputSwitch的值:

  _changeTextInputValue (text) { 
    this.setState({ 
      text 
    }); 
  } 
  ...  
  _onSwitchToggle (completed) { 
    this.setState({ 
      completed 
    }); 
  } 
} 

为新组件添加一些样式改进:

// Tasks/app/components/EditTask/styles.js 

import { Navigator, StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  ... 
  switchContainer: { 
    flex: 1, 
    flexDirection: 'row', 
    justifyContent: 'space-between', 
    maxHeight: 50, 
    padding: 10 
  }, 
  switchText: { 
    fontSize: 16 
  }, 
  textInput: { 
    borderColor: 'gray', 
    borderWidth: 1, 
    height: 40, 
    margin: 10, 
    padding: 10 
  } 
}) 

export default styles; 

保存按钮

在本节中,我们将在导航栏的右上角创建一个标签为Save的按钮。当它被点击时,必须发生以下两件事:

  • 用户对待办事项所做的更改(如名称、完成状态和截止日期)必须保存到AsyncStorage,覆盖其以前的详细信息

  • TasksList必须更新,以便用户能够立即看到他们所做的更改

使用 React Native 渲染Save按钮很容易。将被推送到NavigatorIOS的对象需要接收以下两个键/值对:

  • rightButtonTitle:这是一个字符串,用于显示该区域的文本

  • onRightButtonPress:这是一个在按下该按钮时触发的回调

从表面上看,这似乎很简单。然而,我们不能从渲染的子组件传递任何信息到NavigatorIOSonRightButtonPress方法。相反,我们必须在我们的TasksList组件内部保留我们做出的更改的副本,并在DatePickerIOSTextInputEditTask中的Switch组件更新时更新它们。

// Tasks/app/components/TasksList/index.js 

... 
export default class TasksList extends Component { 
  constructor (props) { 
    ... 
    this.state = { 
      currentEditedTaskObject: undefined, 
      ... 
    }; 
  } 
  ... 
  _completeTask (rowID) { 
    const singleUpdatedTask = { 
      ...this.state.listOfTasks[rowID], 
      completed: !this.state.listOfTasks[rowID].completed 
    }; 

    this._saveAndUpdateSelectedTask(singleUpdatedTask, rowID); 
  } 

这不再是一个异步函数。利用async/await的部分被拆分为_saveAndUpdateSelectedTask

将当前编辑的任务对象设置为状态:

  _editTask (rowData, rowID) { 
    this.setState({ 
      currentEditedTaskObject: rowData 
    }); 

为右按钮添加一个onRightButtonPress回调和字符串:

    this.props.navigator.push({ 
      ... 
      onRightButtonPress: () => this._saveCurrentEditedTask(rowID), 
      rightButtonTitle: 'Save', 

EditTask传递四个新函数来处理项目的详细信息:

      passProps: { 
        changeTaskCompletionStatus: (status) =>
        this._updateCurrentEditedTaskObject('completed', status), 
        changeTaskDueDate: (date, formattedDate) => 
        this._updateCurrentEditedTaskDueDate
        (date, formattedDate), 
        changeTaskName: (name) => 
        this._updateCurrentEditedTaskObject('text', name), 
        clearTaskDueDate: () => 
        this._updateCurrentEditedTaskDueDate(undefined, undefined), 
      } 
    }); 
  } 

_editTask添加参数以接受:

  _renderRowData (rowData, rowID) { 
    return ( 
      <TasksListCell 
        ... 
        onLongPress={ () => this._editTask(rowData, rowID) } 
        ... 
      /> 
    ) 
  } 

这是之前在componentDidMount中找到的逻辑。由于_saveCurrentEditedTask需要调用它,因此将其拆分为自己的函数:

  async _saveAndUpdateSelectedTask (newTaskObject, rowID) { 
    const listOfTasks = this.state.listOfTasks.slice(); 
    listOfTasks[rowID] = newTaskObject; 

    await AsyncStorage.setItem('listOfTasks', 
    JSON.stringify(listOfTasks)); 

    this._updateList(); 
  } 

要保存当前编辑的任务,我们将对象和rowID传递给_saveAndUpdateSelectedtask,然后对导航器调用pop

_saveCurrentEditedTask (rowID) { 
this._saveAndUpdateSelectedTask(this.state.currentEditedTaskObject,
rowID); 
  this.props.navigator.pop(); 
} 

此函数更新当前编辑的任务对象的dateformattedDate

  _updateCurrentEditedTaskDueDate (date, formattedDate) { 
    this._updateCurrentEditedTaskObject ('due', date); 
    this._updateCurrentEditedTaskObject ('formattedDate', 
    formattedDate); 
  } 

以下函数接受一个键和一个值,创建一个带有新值的currentEditedTaskObject的克隆,并将其设置在状态中:

  _updateCurrentEditedTaskObject (key, value) { 
    let newTaskObject = Object.assign({}, 
    this.state.currentEditedTaskObject); 

    newTaskObject[key] = value; 

    this.setState({ 
      currentEditedTaskObject: newTaskObject 
    }); 
  } 
  ... 
} 

最后两个函数的目的是更新正在编辑的对象的 TasksList 本地状态副本。这是出于两个原因:

  • 我们对 EditTask 所做的任何更新,例如更改名称、完成状态和截止日期,目前都不会传播到其父组件

  • 此外,我们不能仅仅将 EditTask 中的值指向作为 props 传递的内容,因为 EditTask 组件不会在传递给它的 props 发生变化时重新渲染。

EditTask 获得了几个更改,包括组件预期的新 propTypes

// Tasks/app/components/EditTask/index.js 

... 
export default class EditTask extends Component { 
  static propTypes = { 
    changeTaskCompletionStatus: PropTypes.func.isRequired, 
    changeTaskDueDate: PropTypes.func.isRequired, 
    changeTaskName: PropTypes.func.isRequired, 
    clearTaskDueDate: PropTypes.func.isRequired, 
    ... 
  } 

EditTask 收到的更改涉及调用作为 props 传递给它的函数来更新父组件的数据以保存:

  ... 
  render () { 
    ... 
        const dueDateSetTitle = 'Due On ' + 
        this.state.formattedDate || this.props.formattedDate;
    ... 
  } 

  _changeTextInputValue (text) { 
    ...  
    this.props.changeTaskName(text); 
  } 

  _clearDate () { 
    ...  
    this.props.clearTaskDueDate(); 
  } 
  ... 
  _onDateChange (date) { 
    ...  
    this.props.changeTaskDueDate(date, formattedDate); 
  } 
  ... 
  _onSwitchToggle (completed) { 
    ...  
    this.props.changeTaskCompletionStatus(completed); 
  } 
} 

TasksListCell 修改

最后,我们希望编辑由我们的 ListView 渲染的每一行,以显示截止日期(如果存在)。

为了做到这一点,我们将不得不编写一些条件逻辑来显示格式化的日期,如果分配给我们要渲染的任务项,这将也是一个创建自定义 styles 文件夹的好时机,因为我们将需要它。

花些时间创建您版本的此功能。我的解决方案如下:

// Tasks/app/components/TasksListCell/index.js 

... 
import styles from './styles'; 

您可能会注意到上面的导入语句中,TasksListCell 现在导入了它的 StyleSheet

formattedDate 添加到 propTypes 中作为可选字符串:

export default class TasksListCell extends Component { 
  static propTypes = { 
    ... 
    formattedDate: PropTypes.string, 
  } 

... 
  render () { 
    ... 
    return ( 
      <View style={ styles.tasksListCellContainer }> 
        <TouchableHighlight 
          ... 
        > 
          <View style={ styles.tasksListCellTextRow }> 
            <Text style={ [styles.taskNameText, 
            { textDecorationLine: isCompleted }] }> 
              { this.props.text } 
            </Text>

调用 _getDueDate 来渲染截止日期的字符串,如果存在:

            <Text style={ styles.dueDateText }> 
              { this._getDueDate() } 
            </Text> 
          </View> 
        </TouchableHighlight> 
      </View> 
    ) 
  } 

_getDueDate () { 
    if (this.props.formattedDate && !this.props.completed) { 
      return 'Due ' + this.props.formattedDate; 
    } 

    return ''; 
  } 
} 

此组件已被修改以支持显示截止日期的第二行文本,但前提是它存在。

逻辑设置为仅在任务未标记为完成时显示截止日期,这样用户就不会在看到他们已经完成的任务的截止日期时感到困惑。

此外,还添加了样式以使两行显示在同一行:

// Tasks/app/components/TasksListCell/styles.js 

import { StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  dueDateText: { 
    color: 'red', 
    flex: 1, 
    fontSize: 12, 
    paddingTop: 0, 
    textAlign: 'right' 
  }, 
  taskNameText: { 
    fontSize: 20 
  }, 
  tasksListCellContainer: { 
    flex: 1 
  }, 
  tasksListCellTextRow: { 
    flex: 1 
  } 
}); 

export default styles;  

这就是它的样子:

到目前为止,这是一个相当不错的应用程序,您将能够使用我们在下一个项目中获得的技能对其进行更多改进。随着我们结束这个项目,我想将您的注意力转向我经常收到的问题:

我们如何在 Android 上做这件事?

这是一个很好的问题,我们将在本书每个项目的末尾进行探索。我将假设您已经设置了您的开发环境,以便在 React Native 中开发 Android 应用。如果没有,请在继续之前先做这件事。如果您对开发 Android 没有兴趣,请随意跳过这部分内容,继续下一章!

修改 Android 任务

首先,我们需要在我们的应用的 Android 文件夹下创建一个新的 local.properties 文件,并指向 Android SDK 目录。添加以下行,其中 USERNAME 是您的机器用户名:

// Tasks/android/local.properties 

sdk.dir = /Users/USERNAME/Library/Android/sdk 

如果您的 Android SDK 安装在与前一个示例不同的位置,您需要修改此文件以指向正确的位置。

然后,启动一个Android 虚拟设备AVD),在项目根目录下执行react-native run-android命令。您将看到以下屏幕,这与我们最初为 iOS 构建Tasks时的默认模板看起来几乎一样:

图片

在 Android 上工作时,按RR重新加载应用,并使用Command + M进入开发者菜单。

您可能会发现,当远程 JS 调试开启时,从简单事物(如TouchableHighlight阴影和导航)的动画可能会非常缓慢。在撰写本文时,一些技术解决方案正在被提出以解决这个问题,但在此期间,强烈建议您根据需要启用和禁用远程 JS 调试。

导航器

Navigator组件的工作方式与其原生 iOS 组件略有不同,但它仍然非常强大。使用Navigator的一个变化是,您的路由应该明确定义。我们可以通过设置路由数组并根据我们访问的路由渲染特定的场景来实现这一点。以下是一个示例:

export default class Tasks extends Component { 
  render () { 
    const routes = [ 
      { title: 'First Component', index: 0 }, 
      { title: 'Second Component', index: 1 } 
    ]; 

创建一个routes数组,如前述代码所示。

您可能会注意到,我们从一开始就明确定义了我们的路由,设置了一个初始路由,然后在这里将属性传递给每个路由的组件:

    return ( 
      <Navigator 
        initialRoute={{ index: 0 }} 
        renderScene={ (routes, navigator) => 
        this._renderScene(routes, navigator) } /> 
    ) 
  } 

传递给_renderScene的路由对象包含一个passProps对象,我们可以在推送导航时设置它。

在将组件推送到Navigator时,我们不传递组件,而是传递一个index;这是Navigator_renderScene方法确定要向用户显示哪个场景的地方。以下是推送Navigator的方式:

    _renderScene (route, navigator) { 
      if (route.index === 0) { 
        return ( 
          <FirstComponent 
            title={ route.title } 
            navigator={ navigator } /> 
        ) 
    } 

    if (route.index === 1) { 
      return ( 
        <SecondComponent 
         navigator={ navigator } 
         details={ route.passProps.details } /> 
      ) 
    } 
  } 
} 

这是我们使用导航器组件推送不同路由的方式。请注意,与NavigatorIOS中传递组件的方式不同,我们传递的是路由的索引:

  _renderAndroidNavigatorView () { 
    this.props.navigator.push({ 
      index: 1, 
      passProps: { 
        greeting: 'Hello World' 
      } 
    }); 
  } 

如果您将此与我们在 iOS 中渲染EditTask的方式进行比较,您会注意到我们根本就没有设置导航栏。Android 应用通常通过DrawerToolbarAndroid组件的组合来处理导航,我们将在稍后的项目中解决这些问题。这将帮助我们的应用看起来和感觉就像任何 Android 应用一样。

导航器示例

以下代码是导航器的示例:

// index.android.js

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  Navigator, 
} from 'react-native'; 

import TasksList from './app/components/TasksList'; 
import EditTask from './app/components/EditTask'; 

class Tasks extends Component { 

  render () { 
    const routes = [ 
      { title: 'Tasks', index: 0 }, 
      { title: 'Edit Task', index: 1 } 
    ]; 

再次,为我们的应用建立路由。

    return ( 
      <Navigator 
        initialRoute={{ index: 0}} 
        renderScene={ (routes, navigator) =>
        this._renderScene(routes, navigator) }/> 
    ); 
  } 

导入Navigator组件并为用户渲染它。它从index:``0开始,返回TasksList组件。

如果索引是0,则返回TasksList。这是默认的route

  _renderScene (route, navigator) { 
    if (route.index === 0) { 
      return ( 
        <TasksList 
          title={ route.title } 
          navigator={ navigator } /> 
      ) 
    } 

如果路由索引是 1,则返回EditTask。它将通过passProps方法接收上述属性:

    if (route.index === 1) { 
      return ( 
        <EditTask 
          navigator={ navigator } 
          route={ route } 
          changeTaskCompletionStatus={ 
          route.passProps.changeTaskCompletionStatus } 
          changeTaskDueDate={ route.passProps.changeTaskDueDate } 
          changeTaskName={ route.passProps.changeTaskName } 
          completed={ route.passProps.completed } 
          due={ route.passProps.due } 
          formattedDate={ route.passProps.formattedDate } 
          text={ route.passProps.text } 
        /> 
      ) 
    } 
  } 
} 

AppRegistry.registerComponent('Tasks', () => Tasks); 

在这个阶段,无需进行进一步修改,我们就可以创建新的待办事项并将它们标记为已完成。然而,由于Navigator组件的推送方法接受的参数与 iOS 的推送方法不同,我们将在TasksList文件中创建一些条件逻辑来适应它。

平台

当你的文件在 iOS 和 Android 功能之间的差异很小,使用相同的文件是可以的。利用Platform API,我们可以识别用户所使用的移动设备类型,并条件性地将他们引导到特定的路径。

与你的其他 React Native 组件一起导入Platform API:

import { Platform } from 'react-native';  

然后在组件中调用其OS属性:

  _platformConditional () { 
    if (Platform.OS === 'ios') { 
      doSomething(); 
    } 

    if (Platform.OS === 'android') { 
      doSomethingElse(); 
    } 
  } 

这使我们能够控制应用所走的路径,并允许进行一些代码复用。

Android 特定文件如果需要创建一个仅在 Android 设备上运行的文件,只需将其命名为<FILENAME>.android.js,就像两个索引文件一样。React Native 将确切知道要构建哪个文件,这让我们能够在需要添加大量逻辑而一个通用的index.js文件无法处理时创建特定平台的组件。将文件命名为<FILENAME>.ios.js以设置 iOS 特定文件。

使用Platform API,我们可以创建条件逻辑来决定Navigator应根据用户的平台如何推送下一个组件。导入Platform API:

// Tasks/app/components/TasksList/index.js 

... 
import { 
  ... 
  Platform, 
  ... 
} from 'react-native'; 

根据用户的平台修改TextInput的样式,使其具有与其平台相呼应的设计语言。在 Android 上,它通常显示为没有边框的单条下划线;因此,我们在该组件的 Android 特定样式中消除了边框:

... 
export default class TasksList extends Component { 
  ... 
  render () { 
  ... 
    return ( 
      <View style={ styles.container }> 
        <TextInput 
          ... 
          style={ Platform.os === 'IOS' ? styles.textInput :
          styles.androidTextInput } 
          ... 
        /> 
        ... 
      </View> 
    ); 
  } 

我将_editTask函数改为运行条件逻辑。如果我们的平台是 iOS,我们调用_renderIOSEditTaskComponent;否则,我们的平台必须是 Android,我们调用_renderAndroidEditTaskComponent代替:

  _editTask (rowData, rowID) { 
    ...  
    if (Platform.OS === 'ios') { 
      return this._renderIOSEditTaskComponent(rowID); 
    } 

    return this._renderAndroidEditTaskComponent(rowID); 
  } 

  _renderAndroidEditTaskComponent (rowID) { 
    this.props.navigator.push({ 
      index: 1, 
      passProps: { 
        changeTaskCompletionStatus: (status) => 
        this._updateCurrentEditedTaskObject('completed', status), 
        changeTaskDueDate: (date, formattedDate) =>
        this._updateCurrentEditedTaskDueDate(date, formattedDate), 
        changeTaskName: (name) => 
        this._updateCurrentEditedTaskObject('text', name), 
        clearTaskDueDate: () => 
        this._updateCurrentEditedTaskDueDate(undefined, undefined), 
        completed: this.state.currentEditedTaskObject.completed, 
        due: this.state.currentEditedTaskObject.due, 
        formattedDate: 
        this.state.currentEditedTaskObject.formattedDate, 
        text: this.state.currentEditedTaskObject.text 
      } 
    }) 
  } 

上述代码将EditTaskindex推送到导航器。它传递了 iOS 版本的应用之前传递的相同属性。

_renderIOSEditTaskComponent的内容与_editTask之前包含的内容相同:

  _renderIOSEditTaskComponent (rowID) { 
    this.props.navigator.push({ 
      ... 
    }); 
  } 
  ... 
} 

在以下代码中,我们为TextInput添加了一个自定义的 Android 样式,省略了边框:

// Tasks/app/components/EditTask/styles.js 

... 
const styles = StyleSheet.create({ 
  androidTextInput: { 
    height: 40, 
    margin: 10, 
    padding: 10 
  }, 
  ... 
}); 

DatePickerAndroid 和 TimePickerAndroid

在 Android 上设置时间和日期与 iOS 大不相同。在 iOS 上,你有一个包含日期和时间的DatePickerIOS组件。在 Android 上,这被分为两个原生模态,DatePickerAndroid用于日期,TimePickerAndroid用于时间。它不是一个用于渲染的组件,而是一个异步函数,它打开模态并等待自然结束,然后再应用逻辑。

要打开其中一个,将其包裹在一个异步函数中:

async renderDatePicker () { 
  const { action, year, month, day } = await DatePickerAndroid.open({ 
    date: new Date() 
  }); 

  if (action === DatePickerAndroid.dismissedAction) { 
    return; 
  } 

  // do something with the year, month, and day here 
} 

DatePickerAndroidTimePickerAndroid组件都返回一个对象,我们可以通过使用 ES6 解构赋值来获取每个对象的属性,如前一个片段所示。

由于这些组件默认将渲染为模态,所以我们也没有必要使用为 iOS 版本的应用构建的ExpandableCell组件。为了实现 Android 特定的日期和时间选择器,我们应该创建一个处理此功能的 Android 特定EditTask组件。

而不是扩展单元格,我们应该创建另一个 Button 组件来打开和关闭对话框。

在下一节给出的示例中,我克隆了 EditTask 的 iOS index.js 文件,并将其重命名为 index.android.js,然后再对其进行修改。省略了未从 iOS 版本更改的任何代码。已删除的内容也已注明。

DatePickerAndroid 和 TimePickerAndroid 示例

从导入语句中移除 DatePickerIOSExpandableCell

// Tasks/app/components/EditTask/index.android.js 

... 
import { 
  ... 
  DatePickerAndroid, 
  TimePickerAndroid, 
} from 'react-native'; 
... 

我已从该组件的 constructor 函数中移除了状态中的 expanded 布尔值:

export default class EditTask extends Component { 
  ... 

这个新的 DatePicker 按钮在按下时会调用 _showAndroidDatePicker。它放置在 TextInput 下方并替换了 ExpandableCell

  render () { 
    ... 
    return ( 
      <View style={ styles.editTaskContainer }> 
        ... 
        <View style={ styles.androidButtonContainer }> 
          <Button 
            color={ '#80B546' } 
            title={ this.state.dateSelected ? dueDateSetTitle : 
            noDueDateTitle } 
            onPress={ () => this._showAndroidDatePicker() } 
          /> 
        </View> 

清除截止日期的 Button 没有发生变化,但其样式已更改:

        <View style={ styles.androidButtonContainer }> 

        </View> 
      </View> 
    ); 
  }

一个异步函数在 DatePickerAndroid 上调用 open,提取 actionyearmonthday,将它们设置为状态,然后调用 _showAndroidTimePicker

  async _showAndroidDatePicker () { 
    const options = { 
      date: this.state.date 
    }; 

    const { action, year, month, day } = await 
    DatePickerAndroid.open(options); 

    if (action === DatePickerAndroid.dismissedAction) { 
      return; 
    } 

    this.setState({ 
      day, 
      month, 
      year 
    }); 

    this._showAndroidTimePicker(); 
  } 

以下是我们之前用于 _showAndroidDatePicker 的相同策略,但在最后调用 _onDateChange

  async _showAndroidTimePicker () { 
    const { action, minute, hour } = await TimePickerAndroid.open(); 

    if (action === TimePickerAndroid.dismissedAction) { 
      return; 
    } 

    this.setState({ 
      hour, 
      minute 
    }); 

    this._onDateChange(); 
  } 

使用 DatePickerAndroidTimePickerAndroid 返回的五个组合值创建一个新的 Date 对象:

  ... 
  _onDateChange () { 
    const date = new Date(this.state.year, this.state.month, 
    this.state.day, this.state.hour, this.state.minute); 
    ... 
  }
  ... 
} 

我已移除 _getDatePickerHeight_onExpand,因为它们与 EditTask 的部分相关,这些部分在 Android 版本的 app 中不可用。我还为此组件添加了一些样式更改:

// Tasks/app/components/EditTask/styles.js 

... 
const styles = StyleSheet.create({ 
  androidButtonContainer: { 
    flex: 1, 
    maxHeight: 60, 
    margin: 10 
  }, 
  ... 
  textInput: { 
    height: 40, 
    margin: 10, 
    padding: 10 
  } 
}); 

保存更新

由于我们不在 Android 版本的 app 中使用导航栏,我们应该创建一个处理相同保存逻辑的保存按钮。

首先,我们应该修改 index.android.js 以将 saveCurrentEditedTask 属性从 TasksList 组件传递给 EditTask

// index.android.js

... 
class Tasks extends Component { 
  ... 
  _renderScene (route, navigator) { 
    ... 
    if (route.index === 1) { 
      return ( 
        <EditTask 
          ... 
          saveCurrentEditedTask={ route.passProps
          .saveCurrentEditedTask } 
          ... 
        /> 
      ) 
    } 
  } 
} 

然后,修改 TasksList 以在 _renderAndroidEditTaskComponent 中将 _saveCurrentEditedTask 方法传递给 EditTask

// Tasks/app/components/TasksList/index.js 

... 
export default class TasksList extends Component { 
  ... 
  _renderAndroidEditTaskComponent (rowID) { 
    this.props.navigator.push({ 
      ... 
      passProps: { 
        ... 
        saveCurrentEditedTask: () => 
        this._saveCurrentEditedTask(rowID), 
        ... 
      } 
    }) 
  } 
  ... 
} 

在此之后,修改 EditTask 的 Android 版本以包含一个新按钮,当按下时会调用其 saveCurrentEditedTask 方法:

// Tasks/app/components/EditTask/index.android.js 

... 
export default class EditTask extends Component { 
  static propTypes = { 
    ... 
    saveCurrentEditedTask: PropTypes.func.isRequired, 
    ... 
  } 

  render () { 
    ... 
    return ( 
      <View style={ styles.editTaskContainer }> 
        ... 
        <View style={ styles.saveButton }> 
          <Button 
            color={ '#4E92B5' } 
            onPress={ () => this.props.saveCurrentEditedTask() } 
            title={ 'Save Task' } 
          /> 
        </View> 
      </View> 
    ); 
  } 
  ... 
} 

最后,使用新的 saveButton 属性添加一些样式:

// Tasks/app/components/EditTask/styles.js 

import { Navigator, StyleSheet } from 'react-native'; 

const styles = StyleSheet.create({ 
  ... 
  saveButton: { 
    flex: 1, 
    marginTop: 20, 
    maxHeight: 70, 
  }, 
  ... 
}); 

BackAndroid

我们需要处理的最后一件事是返回按钮。每个 Android 设备上都有一个通用的返回按钮,无论是硬件还是软件实现。我们需要使用 BackAndroid API 来检测返回按钮的按下并设置我们自己的自定义功能。如果我们不这样做,每次按下返回按钮时,应用程序都会自动关闭。

要使用它,我们可以在 componentWillMount 生命周期事件中添加一个事件监听器,当检测到返回按钮按下时会弹出导航器。我们还可以在组件卸载时移除监听器。

componentWillMount 期间,向 BackAndroid API 添加一个 hardwareButtonPress 事件的事件监听器,当触发时调用 _backButtonPress

// Tasks/app/components/EditTask/index.android.js 

... 
import { 
  BackAndroid, 
  ... 
} from 'react-native'; 
... 
export default class EditTask extends Component { 
  ... 
  componentWillMount () { 
    BackAndroid.addEventListener('hardwareButtonPress', () => 
    this._backButtonPress()); 
  } 

如果组件被卸载,则移除相同的监听器:

  componentWillUnmount () { 
    BackAndroid.removeEventListener('hardwareButtonPress', () => 
    this._backButtonPress()) 
  } 

使用 _backButtonPress 在导航器上调用 pop

  ... 
  _backButtonPress () { 
    this.props.navigator.pop(); 
    return true; 
  } 
  ... 
} 

摘要

这是一章很长的内容!我们完成了很多事情。首先,我们使用NavigatorIOS来建立自定义路由,并创建了一个组件来编辑待办事项的详细信息,包括将其标记为已完成和添加截止日期。

然后,我们构建了一个具有流畅动画的自定义、可重用组件,用于展开和折叠子组件,使得DatePickerIOS可以根据需要展开和折叠。之后,我们实现了逻辑,以便使用导航栏保存我们对任务所做的更改。

我们还将我们的应用程序移植到支持 Android 操作系统!我们首先将NavigatorIOS替换为Navigator,使用Platform API 根据用户所使用的移动设备类型触发条件逻辑,并通过在每个索引文件后附加.android.ios来创建 iOS 和 Android 特定的组件。

我们通过在 Android 上渲染日期和时间选择器完成了对 Android 的移植,这两个选择器是两个独立的弹出窗口,并在我们特定的EditTask组件内创建了一个保存按钮,以便我们的用户可以保存他们所做的更改。最后,通过使用BackAndroid API 监听返回按钮的点击,允许我们的用户从编辑待办事项返回到待办事项列表屏幕,而不是完全离开应用程序。