react-native的列表使用

在官网中使用列表组件是FlatList,具体的api使用可以自行去官网查看,本人是从android端转来的,熟悉android开发的在列表加载初期(网络请求开发时候),会出现loading页面,当数据加载成功的时候,有数据的时候会显示数据,数据为空时候显示NotDataView页面,当Error时候会出现请求出差页面,列表的下拉刷新使用的时候官网自带的RefreshControl(也可以自行去写动画)。

基础组件设计

ErrorView(数据加载出错),LoadingView(正在请求中),NotDataView(无数据),ListView(带有上述功能的组件),前三者可以根据自己项目的效果修改,在此我使用最简单的方式实现。

代码实现

ErrorView(非常简单的显示加载出差和重试功能)

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
import React from "react";
import { TouchableOpacity, Text, StyleSheet } from "react-native";
interface ErrorViewProps {
errorMsg?: string;
onPress: () => void;
}
const ErrorView: React.SFC<ErrorViewProps> = ({ errorMsg }) => (
<TouchableOpacity style={styles.container}>
<Text>{errorMsg}</Text>
</TouchableOpacity>
);
ErrorView.defaultProps = {
errorMsg: "服务器出差,请稍后再试..."
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export default ErrorView;

LoadingView(正在加载中)

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
import React from "react";
import { View, Text, StyleSheet, ActivityIndicator } from "react-native";
interface LoadingViewProps {
backgroundColor?: string;
indicatorColor?: string;
}
const LoadingView: React.SFC<LoadingViewProps> = ({
backgroundColor,
indicatorColor
}) => (
<View style={[styles.loading, { backgroundColor: backgroundColor }]}>
<ActivityIndicator size="large" color={indicatorColor} />
<Text style={styles.loadingText}>数据加载中...</Text>
</View>
);
LoadingView.defaultProps = {
backgroundColor: "#ffffff",
indicatorColor: "4C7FEF"
};
const styles = StyleSheet.create({
loading: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
loadingText: {
marginTop: 10,
textAlign: "center"
}
});
export default LoadingView;

NotDataView(简单的不能再简单)

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
import React from "react";
import { View, Text, StyleSheet } from "react-native";
interface NotDataViewProps {
msg: string;
}
const NotDataView: React.SFC<NotDataViewProps> = ({ msg }) => (
<View style={styles.container}>
<Text>{msg}</Text>
</View>
);
NotDataView.defaultProps = {
msg: "暂无数据"
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export default NotDataView;

重头戏(ListView)

先上代码

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
interface ListViewProps {
dataSource: ReadonlyArray<any>;
renderItem: ListRenderItem<any>;
onFetchList: (
loading: boolean,
isRefreshing: boolean,
loadMore: boolean,
pn: number
) => void;
isRefreshing: boolean;
loading: boolean;
loadMore: boolean;
hasMore: boolean;
error: boolean;
refreshColor?: string;
backgroundColor?: string;
defaultPage?: number;
}
const DefaultPage = 0;
class ListView extends React.PureComponent<ListViewProps> {
static defaultProps = {
refreshColor: "#4C7FEF",
backgroundColor: "#ffffff",
defaultPage: DefaultPage
};
page: number;
constructor(props: ListViewProps) {
super(props);
const { defaultPage } = props;
this.page = defaultPage || DefaultPage;
}
componentDidMount() {
const { onFetchList } = this.props;
onFetchList(true, false, false, this.page);
}
onEndReached = () => {
const { onFetchList, loadMore, hasMore, error } = this.props;
if (loadMore || !hasMore || error) {
return;
}
this.page = this.page + 1;
onFetchList(false, false, true, this.page);
};
renderFooter = () => {
const { loadMore, error } = this.props;
if (loadMore) {
return (
<View style={styles.footer}>
<ActivityIndicator />
<Text style={{ marginLeft: 5 }}>加载中...</Text>
</View>
);
}
if (error) {
return (
<TouchableOpacity
style={styles.errorFooter}
onPress={() => {
const { onFetchList } = this.props;
onFetchList(false, false, true, this.page);
}}
>
<Text>加载出错,点击重试</Text>
</TouchableOpacity>
);
}
return <View />;
};
handleRefresh = () => {
const { onFetchList, defaultPage } = this.props;
this.page = defaultPage || DefaultPage;
onFetchList(false, true, false, this.page);
};
render() {
const {
dataSource,
renderItem,
isRefreshing,
loading,
error,
refreshColor,
backgroundColor,
defaultPage
} = this.props;
const refreshColors: string[] = [refreshColor || "#4C7FEF"];
if (loading && this.page === defaultPage) {
return (
<LoadingView
indicatorColor={refreshColor}
backgroundColor={backgroundColor}
/>
);
}
if (error && this.page === defaultPage) {
return (
<ErrorView
onPress={() => {
const { onFetchList } = this.props;
onFetchList(true, false, false, this.page);
}}
/>
);
}
return (
<FlatList
data={dataSource}
onEndReached={this.onEndReached}
onEndReachedThreshold={0.2}
ListEmptyComponent={NotDataView}
keyExtractor={(item, index) => item.id || index.toString()}
renderItem={renderItem}
ListFooterComponent={this.renderFooter}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={this.handleRefresh}
colors={refreshColors}
progressBackgroundColor={backgroundColor}
/>
}
/>
);
}
}
const styles = StyleSheet.create({
footer: {
height: 40,
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
},
errorFooter: {
height: 40,
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
}
});
export default ListView;

从Props可以很明显的看出该组件的功能,dataSource(数据源),renderItem(渲染的Item),onFetchList(请求数据),isRefreshing(是否正在下拉刷新),loading(是否显示正在加载),loadMore(是否显示footer正在加载),hasMore(是否还有数据),error(是否加载出错),refreshColor(下拉刷新的颜色),backgroundColor(背景颜色),defaultPage(默认开始页码)。

接口设计

1
2
3
4
5
6
export interface PageEntity<TMODLE> {
//代表是否还有数据
hasMore: boolean;
list: TMODLE[];
totalCount: number;
}

redux设计

项目中使用的是redux,我们中间件采用的是redux-promise-middleware(简单的来说就是把Promise的三种状态采用拼接的type的形式)。一般来说一个接口有请求开始,成功,失败。

handleReducer设计(对应Promise的三种状态)

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
export interface BaseAction {
type: string;
}
export interface Action<Payload> extends BaseAction {
payload: Payload;
}
export interface ActionMeta<Payload, Meta> extends Action<Payload> {
meta: Meta;
}
export interface Reducer<State, Payload, Meta> {
pending: (state: State, action: ActionMeta<Payload, Meta>) => State;
fulfilled: (state: State, action: ActionMeta<Payload, Meta>) => State;
rejected: (state: State, action: ActionMeta<Payload, Meta>) => State;
}
export default function handleReducer<State, Payload, Meta = any>(
typeName: string,
initialState: State,
reducer: Reducer<State, Payload, Meta>
) {
return function(state = initialState, action: ActionMeta<Payload, Meta>) {
const type = action.type;
const { pending, fulfilled, rejected } = reducer;
if (pending && typeName + "_PENDING" === type) {
return pending(state, action);
}
if (fulfilled && typeName + "_FULFILLED" === type) {
return fulfilled(state, action);
}
if (rejected && typeName + "_REJECTED" === type) {
return rejected(state, action);
}
return state;
};
}

pageReducer的设计(基本看代码都能看出来和ListView相对应)

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import handleReducer from "./handleReducer";
export interface PageState<TMODLE> {
dataSource: TMODLE[];
isRefreshing: boolean;
loading: boolean;
loadMore: boolean;
hasMore: boolean;
error: boolean;
}
export interface PageMeta {
loading: boolean;
isRefreshing: boolean;
loadMore: boolean;
}
export interface PageEntity<TMODLE> {
hasMore: boolean;
list: TMODLE[];
totalCount: number;
}
export function pageAction<TMODLE, PARAMS>(
typeName: string,
api: (param: PARAMS) => Promise<PageEntity<TMODLE>>,
paramCallback?: (param: PARAMS) => PARAMS
) {
return (
loading: boolean,
isRefreshing: boolean,
loadMore: boolean,
param: PARAMS
) => ({
type: typeName,
payload: paramCallback ? api(paramCallback(param)) : api(param),
meta: {
loading,
isRefreshing,
loadMore
}
});
}
const initialState: PageState<any> = {
dataSource: [],
isRefreshing: false,
loading: false,
loadMore: false,
hasMore: false,
error: false
};
export function pageReducer<TMODEL>(typeName: string) {
return handleReducer<PageState<TMODEL>, PageEntity<TMODEL>, PageMeta>(
typeName,
initialState,
{
pending: (state, { meta }) => ({
...state,
...meta
}),
fulfilled: (state, { payload }) => ({
...state,
isRefreshing: false,
loading: false,
loadMore: false,
error: false,
hasMore: payload.hasMore,
dataSource: state.loadMore
? state.dataSource.concat(payload.list)
: payload.list
}),
rejected: state => ({
...state,
isRefreshing: false,
loading: false,
loadMore: false,
hasMore: false,
error: true
})
}
);
}

使用

好比有个UserList页面

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
41
42
43
44
45
46
47
48
49
50
51
52
interface UserListProps {
fetchUserList: (
loading: boolean,
isRefreshing: boolean,
loadMore: boolean,
param: CommomListParams
) => void;
userList: PageState<UserData>;
}
class UserList extends React.Component<UserListProps> {
onFetchList = (
loading: boolean,
isRefreshing: boolean,
loadMore: boolean,
page: number
) => {
const { fetchUserList } = this.props;
fetchUserList(loading, isRefreshing, loadMore, { start: page });
};
renderUserItem = ({ item }: ListRenderItemInfo<UserData>) => (
<View>
<Text>{item.id}</Text>
<Text>{item.name}</Text>
<Text>{item.age}</Text>
<Text>{item.sex}</Text>
</View>
);
render() {
const { userList } = this.props;
return (
<ListView
{...userList}
onFetchList={this.onFetchList}
renderItem={this.renderUserItem}
/>
);
}
}
function mapStateToProps(state: ReducerType) {
return {
userList: state.userList
};
}
export default connect(
mapStateToProps,
{
fetchUserList: fetchUserListAction
}
)(UserList);

action很简单

1
2
3
4
export const fetchUserListAction = pageAction<UserData, CommomListParams>(
User.FETCH_USER_LSIT,
fetchUserList
);

reducers也很简单,不需要写任何其它纯函数

1
2
3
4
5
6
7
8
9
const reducer = combineReducers({
userList: pageReducer<UserData>(User.FETCH_USER_LSIT)
});
export default reducer;
export interface ReducerType {
userList: PageState<UserData>;
}

总结

通过这次的封装使我对redux, Typescript有更加深入的了解。代码地址