Skip to content
React Native 示例:列表与导航
概述
FlatList 是 React Native 中用来展示长列表的组件。与 ScrollView 将所有子元素一次性挂载到视图树不同,FlatList 基于 VirtualizedList 实现了视图回收:只渲染当前可见区域附近的元素,当用户滚动时,移出屏幕的视图会被回收,新出现的视图才会被创建。
这一差异直接决定了二者的适用场景——ScrollView 适合内容固定且数量不多的可滚动区域,FlatList 适合数据量大、长度不可预知的列表。React Native 官方文档明确指出,ScrollView 会一次性渲染所有子视图,而 FlatList 仅渲染可视区域。如果错误地在长列表场景使用 ScrollView,性能会随数据量线性下降,滚动掉帧几乎不可避免。
FlatList 核心属性
FlatList 通过三个核心属性定义列表的基本行为:data、renderItem、keyExtractor。
data:一个数组,每一项表示列表中的一行数据。可以是简单的字符串数组,也可以是对象数组。renderItem:函数({ item, index, separators }) => ReactElement,根据数组中的每一项返回一个 React 元素。这个函数只在元素进入渲染窗口时才会被调用。keyExtractor:函数(item, index) => string,为每一项生成唯一的 key。如果不提供,FlatList 会尝试用item.key或item.id作为 key,找不到则会警告;对于纯字符串数组会自动使用字符串本身作为 key。
一个最基本的 FlatList 使用示例如下:
tsx
import React from 'react';
import { FlatList, Text, View } from 'react-native';
type ItemData = {
id: string;
name: string;
};
const data: ItemData[] = [];
const MyList = () => {
return (
<FlatList
data={data}
renderItem={({ item }) => <Text>{item.name}</Text>}
keyExtractor={item => item.id}
/>
);
};加载后,列表从 data 中取出每一项,调用 renderItem 生成对应的 <Text>。当数据变化且 data 引用发生改变时,FlatList 会重新计算渲染窗口中的内容。
性能优化属性
FlatList 提供了一组属性,用于减少不必要的渲染和布局计算。
windowSize
windowSize 是一个数值,控制渲染窗口相对于可视区域的大小。默认值为 21(10 个屏幕高度在可视区域上方,10 个下方,1 个为可视区域)。数字越大,预渲染的元素越多,能减少滚动时出现空白区域的概率,但会占用更多内存;数字越小,内存占用低,但快速滚动时容易出现白屏。
tsx
<FlatList
data={data}
renderItem={...}
keyExtractor={...}
windowSize={5} // 仅渲染 2 个屏幕高度上下的内容
/>getItemLayout
当列表每一项的高度固定时,可以通过 getItemLayout 直接指定每一行的偏移量和尺寸,从而跳过运行时动态测量,避免布局抖动。
tsx
<FlatList
data={data}
renderItem={...}
keyExtractor={...}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>提供该属性后,FlatList 可以直接计算出任意 index 的位置,而不需要等待视图实例化和测量。
removeClippedSubviews
设置为 true 时,会将被其他视图完全遮挡的子视图从原生视图层级中移除,减少 GPU 的绘制压力。该属性在 Android 上默认启用,iOS 上需要手动开启。开启后可能影响一些需要保持视图存在的场景(如保持内部状态),但一般情况下对列表场景是安全的。
tsx
<FlatList
data={data}
renderItem={...}
keyExtractor={...}
removeClippedSubviews={true}
/>下拉刷新与加载更多
FlatList 内置了下拉刷新和加载更多的支持。
下拉刷新
通过 refreshing 和 onRefresh 两个属性配合实现。refreshing 为布尔值,表示是否正在刷新;onRefresh 是刷新回调,当用户下拉时触发。
tsx
const [refreshing, setRefreshing] = React.useState(false);
const onRefresh = React.useCallback(() => {
setRefreshing(true);
// 重新请求第一页数据
fetchData().then(() => setRefreshing(false));
}, []);
<FlatList
data={data}
renderItem={...}
refreshing={refreshing}
onRefresh={onRefresh}
/>refreshing 状态需要由开发者自己管理,通常与网络请求的生命周期绑定。
加载更多(无限滚动)
onEndReached 在列表滚动到距底部一定距离时触发,onEndReachedThreshold 用于设置触发距离,取值范围 0~1,表示距离底部的屏幕百分比。例如 0.5 表示当滚动到距底部半个屏幕高度时触发。
tsx
const [page, setPage] = React.useState(1);
const onEndReached = React.useCallback(() => {
// 加载下一页
fetchNextPage(page + 1).then(newData => {
setData(prev => [...prev, ...newData]);
setPage(prev => prev + 1);
});
}, [page]);
<FlatList
data={data}
renderItem={...}
onEndReached={onEndReached}
onEndReachedThreshold={0.3}
/>注意不要在 onEndReached 中直接使用组件内的过期状态,通常需要借助 page 这样的状态变量来控制页码。
React Navigation
React Navigation 是 React Native 生态中广泛使用的导航库,提供了页面栈、底部 Tab、抽屉等导航模式。对于基本的页面间跳转,通常会使用原生的栈导航器(createNativeStackNavigator)。
安装时会引入几个包:
bash
npm install @react-navigation/native @react-navigation/native-stack还需要依赖 react-native-screens 和 react-native-safe-area-context,这两个包在创建项目时通常就已经存在。
NavigationContainer 与 Stack Navigator
NavigationContainer 是导航树的根组件,用于管理导航状态和页面栈。应用的最外层用它包裹整个导航结构:
tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}initialRouteName 指定应用启动时显示的页面。每个 Stack.Screen 定义一个页面,name 是页面的唯一标识,component 是该页面对应的 React 组件。
页面组件会自动收到 navigation 和 route 这两个 props。navigation 用于执行跳转操作;route 用于获取当前页面的参数。
页面跳转与参数传递
跳转
从 Home 跳转到 Detail:
tsx
function HomeScreen({ navigation }) {
return (
<Button
title="Go to Detail"
onPress={() => navigation.navigate('Detail')}
/>
);
}navigation.navigate 会从当前的栈中推入一个新页面。如果目标页面已经在栈顶(即当前页面),再次调用不会产生新页面,这是 React Navigation 的默认行为。如果确实需要推入一个新实例,可以使用 navigation.push。
返回上一页使用 navigation.goBack(),或者直接使用导航栏上自动生成的返回按钮。
传递参数
跳转时可以携带参数:
tsx
navigation.navigate('Detail', { itemId: 86, from: 'Home' });Detail 页面通过 route.params 获取:
tsx
function DetailScreen({ route }) {
const { itemId, from } = route.params;
return <Text>Item ID: {itemId}</Text>;
}参数会保存在 route.params 对象中,如果未传递则默认为 undefined。参数应当保持可序列化——因为导航库可能在应用重启时保留和恢复这些参数。
示例:列表页到详情页
以下示例组合了 FlatList 的数据渲染和 React Navigation 的页面跳转与参数传递,完成从列表加载数据到点击跳转详情展示的完整流程。
列表页
tsx
import React, { useState, useEffect } from 'react';
import { FlatList, Text, TouchableOpacity, View } from 'react-native';
type Item = { id: string; title: string; description: string };
function ListScreen({ navigation }) {
const [data, setData] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 模拟请求数据
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(json => {
setData(json.slice(0, 20)); // 只取前 20 条
setLoading(false);
});
}, []);
const renderItem = ({ item }: { item: Item }) => (
<TouchableOpacity
onPress={() => navigation.navigate('Detail', { itemId: item.id })}
>
<View style={{ padding: 16, borderBottomWidth: 1 }}>
<Text>{item.title}</Text>
</View>
</TouchableOpacity>
);
if (loading) {
return <Text>正在加载...</Text>;
}
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id.toString()}
/>
);
}fetch 获取数据并写入 data 状态,FlatList 根据 data 进行渲染。每一项都被包裹在 TouchableOpacity 中,点击时传递 item.id 作为参数跳转至 Detail 页面。
详情页
tsx
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
type Item = { id: string; title: string; body: string };
function DetailScreen({ route }) {
const { itemId } = route.params;
const [detail, setDetail] = useState<Item | null>(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts/${itemId}`)
.then(res => res.json())
.then(json => setDetail(json));
}, [itemId]);
if (!detail) {
return <Text>加载详情...</Text>;
}
return (
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 20 }}>{detail.title}</Text>
<Text style={{ marginTop: 16 }}>{detail.body}</Text>
</View>
);
}itemId 从 route.params 中解构出来,再用它发起网络请求,获取该帖子的详细信息并展示。
导航配置
将两个页面组装进 Stack.Navigator:
tsx
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="List">
<Stack.Screen name="List" component={ListScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}运行后,应用首先展示列表,点击每一项会跳转到详情,并在详情页中加载该条目的正文。
注意点
FlatList必须提供稳定的 key,否则每次数据变化可能导致整个列表重新创建视图,性能损失很大。如果数据项没有唯一的id字段,务必使用keyExtractor手动指定。- 列表项的高度不固定时,
getItemLayout不能使用,否则会导致布局计算错误。 onEndReached可能被多次触发,需要防抖或者通过状态锁来防止重复请求。- React Navigation 的参数应当是简单的、可序列化的对象。不要将函数、组件实例或其他不可序列化的值放入参数,虽然在开发模式下会有警告。
- 在列表页和详情页之间传递参数时,如果参数来自服务端数据,确保在组件卸载后不要尝试修改状态(例如请求还未完成就返回了上一页)。可以通过
AbortController或清理函数取消请求。
