十六、展示项目清单
在本章中,您将学习如何使用项目列表。在为 Web 构建应用时,使用列表是一种常见的开发活动。使用<ul>
和<li>
元素构建列表也相对简单。在本地移动平台上尝试做类似的事情要复杂得多。
谢天谢地,React Native 提供了一个简单的条目列表界面,隐藏了所有的复杂性。我们将通过浏览一个示例来了解项目列表是如何工作的。然后,我们将介绍一些更改列表中显示的数据的控件。最后,您将看到几个从网络获取项目的示例。
渲染数据采集
让我们从一个基本的例子开始。用于呈现列表的 React 本机组件是ListView
,它在 iOS 和 Android 上的工作方式相同。列表视图采用数据源属性,该属性必须是ListView.DataSource
实例。别担心;在大多数情况下,它实际上只是数组的包装器。ListView
组件需要这种类型的数据源的原因是,它可以执行高效的渲染。列表可能很长,频繁更新会导致性能问题。
那么,让我们现在实施一个基本列表,好吗?下面是呈现基本 100 项列表的代码:
import React from 'react';
import {
AppRegistry,
Text,
View,
ListView,
} from 'react-native';
import styles from './styles';
// You always need a comparator function that's
// used to determine whether or not a row has
// changed. Even in simple cases like this, where
// strict inequality is used.
const rowHasChanged = (r1, r2) => r1 !== r2;
const source = new ListView
// A data source for the list. It's eventually passed
// to the "<ListView>" component, and it requires a
// "rowHasChanged()" comparator.
.DataSource({ rowHasChanged })
// Returns a clone of the data source with new data.
// The comparator function is used by the "<ListView>"
// component to determine what has changed.
.cloneWithRows(
new Array(100)
.fill(null)
.map((v, i) => `Item ${i}`)
);
const RenderingDataCollections = () => (
<View style={styles.container}>
{ /* Renders the list by providing a "dataSource"
property and a "renderRow" function which
renders each item in the list. */ }
<ListView
dataSource={source}
renderRow={
i => (<Text style={styles.item}>{i}</Text>)
}
/>
</View>
);
AppRegistry.registerComponent(
'RenderingDataCollections',
() => RenderingDataCollections
);
让我们从source
常数开始,来看看这里发生了什么。如您所见,这是使用ListView.DataSource()
构造函数创建的。这里,我们给它一个rowHasChanged()
函数。需要告诉数据源如何查找更改,即使是简单的相等性检查。然后,我们将实际数据传递到cloneWithRows()
方法中。这实际上会导致数据源的一个新实例,而且实际上是一个令人困惑的名称,因为实际上没有克隆任何数据。您要克隆的只是您为数据源提供的选项,例如rowHasChanged()
函数。DataSource
实例是不可变的,我们将在下面的示例中看到如何实际更新它们。
接下来,我们呈现<ListView>
组件本身。它位于<View>
容器中,因为列表视图需要一个高度,才能使滚动正常工作。source
和renderRow
属性被传递给<ListView>
,最终确定渲染内容。
乍一看,ListView
组件似乎对我们没有太大帮助。我们得弄清楚这些东西是什么样子的?嗯,是的,ListView
应该是通用的。它应该擅长处理更新,并为我们将滚动功能嵌入到列表中。以下是用于呈现列表的样式:
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
// Flexing from top to bottom gives the
// container a height, which is necessary
// to enable scrollable content.
flex: 1,
flexDirection: 'column',
paddingTop: 20,
},
item: {
margin: 5,
padding: 5,
color: 'slategrey',
backgroundColor: 'ghostwhite',
textAlign: 'center',
},
});
这里,我们为列表中的每个项目提供一个基本样式。否则,每个项目将仅为文本,很难区分其他列表项目。容器样式通过将“弯曲方向”设置为“列”,为列表提供高度。如果没有高度,您将无法正确滚动。
让我们看看这东西现在是什么样子,好吗?
如果您在模拟器中运行此示例,您可以单击并按住屏幕上的任何位置(如手指)的鼠标按钮,然后向上或向下滚动项目。
排序过滤列表
既然您已经掌握了ListView
组件的基本知识,并将它们传递给DataSource
实例,那么让我们向刚刚实现的列表中添加一些控件。ListView
组件本身帮助您为列表控件呈现固定位置的内容。您还将看到如何操作数据源本身,最终驱动屏幕上呈现的内容。
在我们开始实现列表控件组件之前,如果我们检查一下这些组件的高级结构,使代码具有更多的上下文,可能会有所帮助。下面是我们将要实现的组件结构的示例:
以下是每个组件的职责:
ListContainer
:列表的整体容器;它遵循熟悉的 React 容器模式List
:一个无状态组件,将相关的状态片段传递到ListControls
和ListView
组件中ListControls
:包含更改列表状态的各种控件的组件ListFilter
:用于过滤项目列表的控件ListSort
:用于更改列表排序顺序的控件ListView
:呈现项目的实际 ReactNative 组件
在某些情况下,像这样拆分列表的实现是过分的。然而,我认为,如果您的列表首先需要控件,那么您可能正在实现一些东西,这些东西将受益于经过深思熟虑的组件体系结构。
现在,让我们深入了解这个列表的实现,从ListContainer
组件开始:
import React, { Component } from 'react';
import { ListView } from 'react-native';
import List from './List';
// The two comparator functions we need to pass
// to the data source. The "rowHasChanged()" function
// does simple strict inequality. So does
// "sectionHeaderHasChanged()".
const rowHasChanged = (r1, r2) => r1 !== r2;
const sectionHeaderHasChanged = rowHasChanged;
// Performs sorting and filtering on the given "data".
const filterAndSort = (data, text, asc) =>
data.filter(
i =>
// Items that include the filter "text" are returned.
// Unless the "text" argument is an empty string,
// then everything is included.
text.length === 0 ||
i.includes(text)
).sort(
// Sorts either ascending or descending based on "asc".
asc ?
(a, b) => b > a ? -1 : (a === b ? 0 : 1) :
(a, b) => a > b ? -1 : (a === b ? 0 : 1)
);
class ListContainer extends Component {
constructor() {
super();
// The initial state. The "data" is what drives
// the list, and it's initially filtered and sorted
// here.
this.state = {
data: filterAndSort(
new Array(100)
.fill(null)
.map((v, i) => `Item ${i}`)
, '', true),
asc: true,
filter: '',
};
// The "source" is also part of the component state,
// but it's based on "state.data", which is why it's
// set here. This is the data source that's ultimately
// used by the "<ListView>".
this.state.source = new ListView
.DataSource({
rowHasChanged,
sectionHeaderHasChanged,
})
.cloneWithRows(this.state.data);
}
render() {
return (
<List
source={this.state.source}
asc={this.state.asc}
onFilter={(text) => {
// Updates the "filter" state, the actual filter
// text, and the "source" of the list. The "data"
// state is never actually touched -
// "filterAndSort()" doesn't mutate anything.
this.setState({
filter: text,
source: this.state.source.cloneWithRows(
filterAndSort(
this.state.data,
text,
this.state.asc
)
),
});
}}
onSort={() => {
this.setState({
// Updates the "asc" state in order to change
// the order of the list. The same principles as
// used in the "onFilter()" handler are applied
// here, only with different arguments passed to
// "filterAndSort()"
asc: !this.state.asc,
source: this.state.source.cloneWithRows(
filterAndSort(
this.state.data,
this.state.filter,
!this.state.asc
)
),
});
}}
/>
);
}
}
export default ListContainer;
如果这看起来有点过分,那是因为它是。这个容器组件有很多状态需要处理。它也有一些非平凡的行为,需要让它的孩子们可以使用。因此,如果您从封装状态的角度来看,这似乎并不复杂。它的任务是用状态数据填充列表,并提供在此状态下运行的函数。
在理想情况下,这个容器的子组件应该很好而且简单,因为它们不必直接与状态接口。让我们来看看下一个组件:
import React, { PropTypes } from 'react';
import { Text, ListView } from 'react-native';
import styles from './styles';
import ListControls from './ListControls';
// Renders the actual "<ListView>" React Native
// component. The "renderSectionHeader" property
// is where our controls go. The "renderRow"
// property, as always, renders the actual item.
const List = ({
Controls,
source,
onFilter,
onSort,
asc,
}) => (
<ListView
enableEmptySections
dataSource={source}
renderSectionHeader={() => (
<Controls
{...{ onFilter, onSort, asc }}
/>
)}
renderRow={i => (
<Text style={styles.item}>{i}</Text>
)}
/>
);
List.propTypes = {
Controls: PropTypes.func.isRequired,
source: PropTypes.instanceOf(ListView.DataSource).isRequired,
onFilter: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
asc: PropTypes.bool.isRequired,
};
// The "Controls" component is actually our own
// "ListControls" component by default. However,
// this can be overriden by anyone wanting to provide
// their own control components.
List.defaultProps = {
Controls: ListControls,
};
export default List;
该组件将来自ListContainer
组件的状态作为属性,并呈现ListView
组件。与前面的示例相比,这里的主要区别在于renderSectionHeader
属性。此函数用于呈现列表的控件。此属性特别有用的是,它在可滚动列表内容之外呈现控件,确保控件始终可见。
注
还有一个renderHeader
属性,它的作用与renderSectionHeader
基本相同;然而,这个位置并不是固定的。
另外,请注意,我们将自己的ListControls
组件指定为controls
属性的默认值。这使得其他人很容易传入自己的列表控件。让我们看看下面的 To.T2U.组件:
import React, { PropTypes } from 'react';
import { View } from 'react-native';
import styles from './styles';
import ListFilter from './ListFilter';
import ListSort from './ListSort';
// Renders the "<ListFilter>" and "<ListSort>"
// components within a "<View>". The
// "styles.controls" style lays out the controls
// horizontally.
const ListControls = ({
onFilter,
onSort,
asc,
}) => (
<View style={styles.controls}>
<ListFilter onFilter={onFilter} />
<ListSort onSort={onSort} asc={asc} />
</View>
);
ListControls.propTypes = {
onFilter: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
asc: PropTypes.bool.isRequired,
};
export default ListControls;
这可能是示例中迄今为止最简单的组件。它将ListFilter
和ListSort
控件结合在一起。因此,如果您要添加另一个列表控件,您可以将其添加到此处。现在让我们来看一下 Type T2 实现:
import React, { PropTypes } from 'react';
import { View, TextInput } from 'react-native';
import styles from './styles';
// Renders a "<TextInput>" component which allows the
// user to type in their filter text. This causes
// the "onFilter()" event handler to be called.
// This handler comes from "ListContainer" and changes
// the state of the list data source.
const ListFilter = ({ onFilter }) => (
<View>
<TextInput
autoFocus
placeholder="Search"
style={styles.filter}
onChangeText={onFilter}
/>
</View>
);
ListFilter.propTypes = {
onFilter: PropTypes.func.isRequired,
};
export default ListFilter;
filter 控件是一个简单的文本输入,它将项列表过滤为用户类型。处理这个问题的 onChange
函数来自ListContainer
组件。关于他的组件,最需要注意的是它是多么简单和明显。关于它的作用没有任何混淆;当用户在输入框中键入时,它调用一些函数。
ListSort
组件具有与之类似的简单性:
import React, { PropTypes } from 'react';
import { Text } from 'react-native';
// The arrows to render based on the state of
// the "asc" property. Using a Map let's us
// stay declarative, rather than introducing
// logic into the JSX.
const arrows = new Map([
[true, '▼'],
[false, '▲'],
]);
// Renders the arrow text. When clicked, the
// "onSort()" function that's passed down from
// the container.
const ListSort = ({ onSort, asc }) => (
<Text onPress={onSort}>{arrows.get(asc)}</Text>
);
ListSort.propTypes = {
onSort: PropTypes.func.isRequired,
asc: PropTypes.bool.isRequired,
};
export default ListSort;
下面是结果列表:
默认情况下,整个列表按升序呈现。当用户尚未提供任何内容时,您可以看到占位符文本搜索。让我们看看当我们输入一个过滤器并更改排序顺序时的情况:
此搜索包括包含 1 的项目,并按降序对结果进行排序。请注意,您可以先更改顺序,也可以先输入过滤器。过滤器和排序顺序都是ListContainer
状态的一部分。
取列表数据
通常,您会从某个 API 端点获取列表数据。在本节中,您将了解如何从 React 本机组件发出 API 请求。好消息是fetch()
API 由 React Native 填充,因此移动应用中的网络代码看起来和感觉应该与 web 应用中的代码非常相似。
首先,让我们使用 fetch mock 为列表项构建一个模拟 API:
import fetchMock from 'fetch-mock';
import querystring from 'querystring';
// A mock item list...
const items = new Array(100)
.fill(null)
.map((v, i) => `Item ${i}`);
// The same filter and sort functionality
// as the previous example, only it's part of the
// API now, instead of part of the React component.
const filterAndSort = (data, text, asc) =>
data.filter(
i =>
text.length === 0 ||
i.includes(text)
).sort(
asc ?
(a, b) => b > a ? -1 : (a === b ? 0 : 1) :
(a, b) => a > b ? -1 : (a === b ? 0 : 1)
);
// Defines the mock handler for the "/items" URL.
fetchMock.mock(/\/items.*/, (url) => {
// Gets the "filter" and "asc" parameters.
const params = querystring.parse(url.split('?')[1]);
// Performs the sorting and filtering before
// responding.
return ({
items: filterAndSort(
items,
params.filter ? params.filter : '',
!!+params.asc
),
});
});
在 mockapi 端点就绪后,让我们对列表容器组件进行一些更改。您现在可以使用fetch()
函数从 API 模拟加载数据,而不是使用本地数据源:
import React, { Component } from 'react';
import { ListView } from 'react-native';
// Note that we're importing mock here to enable the API.
import './mock';
import List from './List';
const rowHasChanged = (r1, r2) => r1 !== r2;
const sectionHeaderHasChanged = rowHasChanged;
// Fetches items from the API using
// the given "filter" and "asc" arguments. The
// returned promise resolves a JavaScript object.
const fetchItems = (filter, asc) =>
fetch(`/items?filter=${filter}&asc=${+asc}`)
.then(resp => resp.json());
class ListContainer extends Component {
constructor() {
super();
// The "source" state is empty because we need
// to fetch the data from the API.
this.state = {
// data: [],
asc: true,
filter: '',
source: new ListView
.DataSource({
rowHasChanged,
sectionHeaderHasChanged,
})
.cloneWithRows([]),
};
}
// When the component is first mounted, fetch the initial
// items from the API, then
componentDidMount() {
fetchItems(this.state.filter, this.state.asc)
.then(({ items }) => {
this.setState({
source: this.state.source.cloneWithRows(items),
});
});
}
render() {
return (
<List
source={this.state.source}
asc={this.state.asc}
onFilter={text => {
// Makes an API call when the filter changes...
fetchItems(text, this.state.asc)
.then(({ items }) =>
this.setState({
filter: text,
source: this.state.source.cloneWithRows(items),
}));
}}
onSort={() => {
// Makes an API call when the sort order
// changes...
fetchItems(this.state.filter, !this.state.asc)
.then(({ items }) =>
this.setState({
asc: !this.state.asc,
source: this.state.source.cloneWithRows(items),
}));
}}
/>
);
}
}
export default ListContainer;
我认为这看起来简单多了,尽管它需要接触网络才能工作。任何修改列表状态的操作只需调用fetchItems()
,并在承诺解决后设置适当的状态。
惰性列表加载
在本节中,我们将实现一种不同的列表,一种无限滚动的列表。有时候,用户实际上并不知道他们在寻找什么,所以过滤或排序是没有帮助的。想想你登录账户时看到的 Facebook 新闻提要;这是应用的主要功能,您很少寻找特定的功能。您需要通过滚动列表来查看发生了什么。
要使用ListView
组件实现这一点,您需要能够在用户滚动到列表末尾时获取更多 API 数据。为了了解它是如何工作的,我们需要使用大量的 API 数据。发电机在这方面很棒!因此,让我们修改我们在上一个示例中创建的模拟,以便它只使用新数据继续响应:
import fetchMock from 'fetch-mock';
// Items...keep'em coming!
function* genItems() {
let cnt = 0;
while (true) {
yield `Item ${cnt++}`;
}
}
const items = genItems();
// Grabs the next 20 items from the "items"
// generator, and responds with the result.
fetchMock.mock(/\/items.*/, () => {
const result = [];
for (let i = 0; i < 20; i++) {
result.push(items.next().value);
}
return ({
items: result,
});
});
有了它,您现在可以在每次到达列表末尾时对新数据发出 API 请求。好吧,最终这将失败,但我只是试图向您展示在 React Native 中实现无限滚动的一般方法。以下是ListContainer
组件的外观:
import React, { Component } from 'react';
import { ListView } from 'react-native';
import './mock';
import List from './List';
const rowHasChanged = (r1, r2) => r1 !== r2;
const sectionHeaderHasChanged = rowHasChanged;
class ListContainer extends Component {
constructor() {
super();
this.state = {
data: [],
asc: true,
filter: '',
source: new ListView
.DataSource({
rowHasChanged,
sectionHeaderHasChanged,
})
.cloneWithRows([]),
};
// This function is passed to the "onEndReached"
// property of the React Native "ListView" component.
// Instead of replacing the "source", it concatenates
// the new items with those that have already loaded.
this.fetchItems = () =>
fetch('/items')
.then(resp => resp.json())
.then(({ items }) =>
this.setState({
data: this.state.data.concat(items),
source: this.state.source.cloneWithRows(
this.state.data
),
})
);
}
// Fetches the first batch of items once the
// component is mounted.
componentDidMount() {
this.fetchItems();
}
render() {
return (
<List
source={this.state.source}
fetchItems={this.fetchItems}
/>
);
}
}
export default ListContainer;
每次调用fetchItems()
时,响应都与数据数组连接。这将成为新的列表数据源,而不是像前面的示例中那样替换它。现在让我们看一看 Oracle T1 组件,看看你如何回应列表的结尾:
import React, { PropTypes } from 'react';
import { Text, ListView } from 'react-native';
import styles from './styles';
// Renders a "<ListView>" component, and
// calls "fetchItems()" and the user scrolls
// to the end of the list.
const List = ({
source,
fetchItems,
}) => (
<ListView
enableEmptySections
dataSource={source}
renderRow={i => (
<Text style={styles.item}>{i}</Text>
)}
onEndReached={fetchItems}
/>
);
List.propTypes = {
source: PropTypes.instanceOf(ListView.DataSource).isRequired,
fetchItems: PropTypes.func.isRequired,
};
export default List;
如果您运行这个示例,您将看到,当您在滚动时接近屏幕底部时,列表会不断增长。
总结
在本章中,您了解了 React Native 中的ListView
组件。该组件是通用的,因为它不会对渲染的项目施加任何特定的外观。相反,列表的外观取决于您,而ListView
组件有助于高效地呈现数据源。ListView
组件还为其呈现的项目提供了一个可滚动区域。
您实现了一个示例,该示例利用了列表视图中的节标题。这是渲染静态内容(如列表控件)的好地方。然后,您学习了如何在 React Native 中进行网络调用;这就像在任何其他 web 应用中使用fetch()
。最后,您实现了无限滚动的惰性列表,只在滚动到已渲染内容的底部后加载新项目。
在下一章中,您将学习如何显示网络呼叫等事项的进度。
版权属于:月萌API www.moonapi.com,转载请注明出处