背景

下拉刷新上拉加载功能在移动端无论是App还是小程序都是非常高频使用的组件,最近在开发RN的时候,本想着这个轮子应该很成熟了,于是跑遍了github都找不到一个自我感觉很完美的刷新组件,而且star数高的组件很多都是几年前维护的,说到这个,不得不吐槽一下,RN的生态在国内真的是太窄了,遇到稍微小众一点的问题,国内某搜索引擎能找到答案的概率为50%,某歌的概率为80%,并且重复率非常高,一直以为RN会因为react的光环,社区应该会非常完善,结果令我失望。
挣扎了一番之后,决定还是用RN自带的组件–flatList,稍微封装了一层,在Tab路由页使用没有问题,但是在详情页使用的时候,遇到了第一次加载和切换tab时,flatList刷新时候的indicator没有显示的bug,找遍了全网,只有国外友人遇到过,rn的issue也看到过,但是是18年左右的,被人关了,并且没有解决方案,唯一一个遇到跟我一模一样的人,他的解决方案是曲线救国,每次刷新的时候让列表滚回到顶部(因为indicator没有显示,其实是列表滚到底部把那个菊花给覆盖了),不能接受这种解决方案。

开发环境

RN版本:0.64.0
UI组件库:react native elements
组件类型:函数式(hooks)
导航版本(react native navigation):5.X

尝试方案

不管怎么设置list的高度也好,外层高度也好,写死高度和flex设置为1全都试过,全都没有用。

预期结果

image-20210523231632386

实际结果

image-20210523231658236

解决方案

在无数遍的调试下,我发现把react native elements组件库的Header组件移除后,第一次进入页面的菊花正常显示了,但是切换页面还是没有显示。由于我想让list组件在安全区域显示,所以我的flatList包裹了一层SafeAreaView,样式设置了flex:1,当我尝试把他移除后,使用View代替了它,现在结果如我预期显示。

父组件:

1
2
3
4
5
6
7
8
9
10
<View style={common.container}>
<ButtonGroup
onPress={updateIndex}
selectedIndex={selectedIndex}
buttons={['文件', '流程中心']}
containerStyle={styles.buttonGroup}
textStyle={styles.buttonGroupText}
/>
{selectedIndex === 0 ? <ProjectFileList /> : <ProjectWorkflow />}
</View>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<View style={styles.container}>
<SearchBar
containerStyle={styles.searchBarContainer}
inputStyle={styles.searchInput}
inputContainerStyle={styles.searchInputContainer}
lightTheme={true}
placeholder="搜索"
/>
<RefreshableList
loadMore={loadMore}
data={dataList}
renderItem={renderItem}
refreshing={refreshing}
onRefresh={handleRefresh}
onEndReached={handleLoadMore}
keyExtractor={item => item.resId}
/>
</View>

自定义Header

启用了react native elements的Header后,开始寻找替代的Header方案,最后还是决定用react native navigation提供的api完成。

screenOptions配置页面导航的默认参数

配置导航的全局统一样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<Stack.Navigator
initialRouteName="Login"
screenOptions={({ navigation }) => {
return {
headerStyle: {
backgroundColor: colors.primary
},
headerTitleStyle: {
color: '#FFFF'
},
headerBackTitleStyle: {
color: '#FFFF'
},
headerBackTitleVisible: false,
headerBackImage: () => (
<TouchableOpacity onPress={navigation.goBack}>
<Ionicons name="arrow-back" size={24} color={'#fff'} />
</TouchableOpacity>
),
headerLeftContainerStyle: {
paddingLeft: 10
},
headerRightContainerStyle: {
paddingRight: 10
}
};
}}
>
{// 页面的若干配置...}
</Stack.Navigator>

NavigationContainer可以接受一个theme参数,接受主题样式,在路由里面就能使用useTheme拿到全局样式。

1
2
3
<NavigationContainer theme={MyTheme}>
{// 若干配置...}
</NavigationContainer>

MyTheme.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { DefaultTheme } from '@react-navigation/native';

const MyTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: '#0784ff',
bgGray: '#efeff3',
text: '#262626',
infoText: '#909399'
}
};

export default MyTheme;

那么如果有一些导航需要一些自定义的按钮事件需要跟页面联动,怎么处理呢?

其实,navigation中有setOptions方法,就是跟你在配置页面路由时配置Header Title、backTitle等等一样的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 设置自定义header
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => drawerRef.current.toggleSideMenu()}>
<Ionicons name="menu" size={24} color={'#fff'} />
</TouchableOpacity>
),
headerTitle: () => (
<Text style={common.headerTitleText}>{route.params.name}</Text>
)
});
}, [navigation]);

flatList组件

我把通用的方法封装了,需要在页面实现的方法通过props传递给组件 ,并且修复了flatLIst组件上拉加载可能遇到的bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { useEffect, useState } from 'react';
import { View, FlatList, ActivityIndicator, Text } from 'react-native';
import styles from './styles';
import Empty from '../Empty';

const RefreshableList = props => {
const { setEndReachedCalled, loadMore } = props;
const renderFooter = () => {
return loadMore ? (
<View style={styles.footer}>
<ActivityIndicator />
<Text>正在加载更多数据...</Text>
</View>
) : (
<></>
);
};
return (
<FlatList
{...props}
contentContainerStyle={props.data.length ? null : { flexGrow: 1 }}
onEndReachedThreshold={0.2}
onMomentumScrollBegin={() => {
// 有些页面遇到第一次加载就触发loadMore的情况,如首页项目中心,遇到此情况需要传递setEndReachedCalled函数控制触发条件
setEndReachedCalled ? setEndReachedCalled(false) : null;
}}
ListEmptyComponent={() => <Empty />}
ItemSeparatorComponent={
// eslint-disable-next-line no-undef
Platform.OS !== 'android' &&
(({ highlighted }) => (
<View style={[styles.separator, highlighted && { marginLeft: 0 }]} />
))
}
ListFooterComponent={renderFooter}
/>
);
};

export default RefreshableList;

总结

RN踩坑一步一个脚印,社区给不了的,自己想办法解决。