React-native_FlatList思考

Ymc 2019-08-22 01:26:13 11 Page View 21.1'm Read Time ReactNative
在工作中使用到了reactNative,封装了几个通用的组件,下面介绍FlatList组件在使用的时候一些思考吧

FlatList 是 reactNative 老版本中的 ViewList 的一个升级版本,近期项目中使用到了,遇到了一些兼容差异,并简单封装一下,下面简单介绍下

兼容性问题

onEndReachedThreshold

API 中赘述的比较多简单说下就是 当容器拖动距离底部某个值的时候 触发onRefresh上拉刷新方法,注意 这个值是容器高德比例值,比如 .5则为一半容器高度,建议直接使用默认.1 不然在触发机制上需要做些处理,并交互体验上不友好

容器高度问题

在渲染容器高度的时候,使用flex:1是无效的,FlatList所有的无数据组件/底部容器组件渲染的高度都是根据容器撑开,so 我们需要撑满整个屏幕的时候需要主动设置高度,方法有几个:主动Dimensionsapi可以获取,优先如下方法

 <FlatList
        {...this.props}
        onLayout={e => {
          if (!this.state.FLATMIHEIGHT && e.nativeEvent.layout.height) {
            this.setState({
              FLATMIHEIGHT: e.nativeEvent.layout.height
            });
          }
        }}
/>

注意需要放置到state中,否则容易出现闪屏的情况(渲染时机问题)

介于上面的一些问题,以及无数据、错误页、等状态统一性一致性,遂简单封装如下

封装

state

//state
export const State = {
  NORMAL: 0, //正常状态
  REFRESHING: 1, //刷新中
  LOADING: 2, //正在加载
  LOAD_END: 3, //上拉加载完成
  ERROR: 4, //加载发生错误(上拉或者刷新根据不同情况显示不同)
  NO_DATA: 5 //无数据情况
};

用于 api 请求的时候变更转态,以便控制上拉加载、下拉刷新的显示等

render

/ PropTypes
  static propTypes = {
    loadingText: PropTypes.string,
    loadErrorText: PropTypes.string,
    loadEndText: PropTypes.string,
    loadEmptyText: PropTypes.string,
    requestState: PropTypes.number,
    footerContainer: ViewPropTypes.style,
    footerText: ViewPropTypes.style,
    data: PropTypes.array,
    id: PropTypes.string,
    onRequest: PropTypes.func
    // renderItem: PropTypes.func,
    // separator: PropTypes.func //分隔符
  };
  //props+state
  static defaultProps = {
    loadingText: '数据加载中...',
    loadErrorText: '点击重新加载...',
    loadEndText: '已加载全部数据',
    loadEmptyText: '暂时没有相关数据',
    requestState: State.NORMAL,
    footerContainer: {},
    footerText: {},
    data: [],
    id: 'flat_list',
    onRequest: isRefresh => {}
  };
/**
   * 渲染
   * @returns {*}
   */
  render() {
    let { renderItem = () => {} } = this.props;
    return (
      <FlatList
        {...this.props}
        onLayout={e => {
          if (!this.state.FLATMIHEIGHT && e.nativeEvent.layout.height) {
            this.setState({
              FLATMIHEIGHT: e.nativeEvent.layout.height
            });
          }
        }}
        onEndReached={this._onEndReached}
        onRefresh={this.state.enableRefresh ? this._onRefresh : null}
        ListFooterComponent={this.state.enableLoadMore ? this._renderFooter : null}
        refreshing={this.state.enableRefresh && this.props.requestState === State.REFRESHING}
        // ListEmptyComponent={this.props.requestState === State.NO_DATA ? this._renderEmpty : null}
        onEndReachedThreshold={0.1}
        renderItem={renderItem}
        ItemSeparatorComponent={this._separator}
        keyExtractor={this._keyExtractor}
      />
    );
  }

注意FlatList中我们用两个转态控制是否可以加载和刷新enableLoadMore,enableRefresh,存入state是以便外部动态改变的时候,下面我们我们向外部暴露方法

/**
   * 是否显示底部可以刷新的UI
   * @param enableLoadMore
   */
  setenableLoadMore(enableLoadMore) {
    this.setState({
      enableLoadMore: enableLoadMore
    });
  }
  /**
   * 设置显示顶部显示底部刷新的UI的
   * @param enableRefresh
   */
  setEnableRefresh(enableRefresh) {
    this.setState({
      enableRefresh: enableRefresh
    });
  }

onRefresh

上拉加载方法的封装

/**
   * 上拉加载
   * @private
   */
  _onRefresh = () => {
    if (this._enableRefresh()) {
      this.props.onRequest && this.props.onRequest(true);
    }
  };
  /**
   * 是否可以顶部刷新
   * 当前需要 非刷新,而且也不是正在加载
   * @returns {boolean}
   */
  _enableRefresh = () => {
    return !(this.props.requestState === State.REFRESHING || this.props.requestState === State.LOADING);
  };

判断是是否触发加载更多,通过加载状态不在加载中也不在沙墟中,错误或者无数据是可以触发的

onEndReached

下拉刷新方法

  /**
   * @description 下拉刷新
   * @memberof PullLoadRefresh
   */
  _onEndReached = () => {
    if (this._enableLoad()) {
      this.props.onRequest && this.props.onRequest(false);
    }
  };
  /**
   * 是否可以加载,当前数据不能是0(因为进入的时候必须是refresh获取的数据显示)
   * @returns {boolean}
   */
  _enableLoad = () => {
    let { requestState, data } = this.props;
    if (data.length === 0) {
      //防止初次加载的时候重复触发下拉刷新
      return false;
    }
    return requestState === State.NORMAL;
  };

渲染底部dom

/**
   * 主动触发 刷新或加载
   * @param isRefresh
   * @private
   */
  _reRequest = isRefresh => {
    //回调外部方法
    this.props.onRequest && this.props.onRequest(isRefresh);
  };

  /**
   * @description diff 唯一 keys
   */
  _keyExtractor = (item, index) => {
    const { keyExtractor } = this.props;
    if (keyExtractor) {
      return keyExtractor(item, index);
    }
    return index.toString();
  };

  _separator = () => {
    const { separator } = this.props;
    if (separator) {
      return separator();
    }
    return (
      <View
        style={{
          height: 1,
          backgroundColor: '#ededed',
          marginLeft: 10,
          marginRight: 10
        }}
      />
    );
  };

  /**
   * 渲染底部
   * @returns {*}
   */
  _renderFooter = () => {
    let footer = null;
    let footerContainerStyle = [styles.footerContainer, this.props.footerContainer];
    let footerTextStyle = [styles.footerText, this.props.footerText];
    let { loadingText, loadErrorText, loadEndText, loadEmptyText } = this.props;
    const hasData = this.props.data && this.props.data.length > 0;
    switch (this.props.requestState) {
      case State.NORMAL:
        footer = <View style={footerContainerStyle} />;
        break;
      case State.ERROR: {
        //是否有数据
        console.log('请求出错情况是否有数据', hasData);
        footer = hasData ? (
          <TouchableOpacity activeOpacity={0.8} style={footerContainerStyle} onPress={this._reRequest}>
            <Text style={footerTextStyle}>{loadErrorText}</Text>
          </TouchableOpacity>
        ) : (
          <EmptyData
            onPress={() => {
              this._reRequest(true);
            }}
            tips={loadErrorText}
            FLATMIHEIGHT={this.state.FLATMIHEIGHT}
          />
        );
        break;
      }
      case State.NO_DATA: {
        footer = (
          <EmptyData
            onPress={() => {
              this._reRequest(true);
            }}
            tips={loadEmptyText}
            FLATMIHEIGHT={this.state.FLATMIHEIGHT}
          />
        );
        break;
      }
      case State.LOADING: {
        footer = (
          <View style={footerContainerStyle}>
            <ActivityIndicator size="small" color="#888888" />
            <Text style={[footerTextStyle, { marginLeft: 7 }]}>{loadingText}</Text>
          </View>
        );
        break;
      }
      case State.LOAD_END: {
        footer = (
          <View style={footerContainerStyle}>
            <Text style={footerTextStyle}>{loadEndText}</Text>
          </View>
        );
        break;
      }
    }
    return footer;
  };

这里需要注意,我们没有使用ListEmptyComponent官方提供的空白页渲染节点,我们将所有状态都通过底部的节点进行渲染,并且我们注意到错误请求,我们区分了当前是否有数据不同状态进行渲染,当容器无内容的时候,使用组件方法加载一个错误页,如下

// 无数据/接口异常显示
class EmptyData extends Component {
  static defaultProps = {
    tips: '暂无数据'
  };
  _onPress = () => {
    const { onPress } = this.props;
    if (onPress) {
      onPress(true);
    }
  };
  render() {
    return (
      <View
        style={{
          height: this.props.FLATMIHEIGHT
        }}
      >
        <TouchableOpacity
          style={{
            flex: 1,
            alignItems: 'center',
            justifyContent: 'center'
          }}
          onPress={this._onPress}
          activeOpacity={0.8}
        >
          <Image
            style={{
              marginTop: 10,
              marginBottom: 10,
              width: 120,
              height: 120,
              resizeMode: 'contain'
            }}
            source={require('./../assets/img/weather_bg.jpg')}
            // source={{ uri: 'https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg' }}
          />
          <Text>{this.props.tips}</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

封装到这里基本就结束,如果有什么更好的意见和想法欢迎留言联系

实现

封装完毕实现就比较容易了,主要传入4个props即可,请求方法,请求状态,cell节点item,数据源

 return (
      <View style={{ flex: 1,backgroundColor:'pink' }}>
        <PullLoadComponent
          ref={flat_list => {
            this.flat_list = flat_list;
          }}
          onRequest={this._request}
          data={this.state.data}
          requestState={this.state.requestState}
          renderItem={this._renderItem}
        />
      </View>
    );
  }

_renderItem = ({ item }) => {
    const { FPersonName } = item;
    return (
      <Text
        style={{
          color: 'black',
          textAlign: 'center',
          height:80,
          lineHeight:80
        }}
      >
        {FPersonName}
      </Text>
    );
  };

唯一需要注意和麻烦的就是在api请求的时候需要注意不同请求状态的更新

_request = isRefresh => {
    console.log('api请求+isRefresh', isRefresh);
    this.currentPage = isRefresh ? 0 : this.currentPage + 1;
    this.setState({
      requestState: isRefresh ? State.REFRESHING : State.LOADING
    });
    //发起请求

    fetch(`http://192.168.131.141:8081/src/mock/list.json?t=${new Date().getTime()}&currentPage=${this.currentPage}`) // 返回一个Promise对象
      .then(res => {
        return res.json();
      })
      .then(res => {
        console.log(res);
        if (res.code !== 1) {
          this.setState({ requestState: State.ERROR });
        } else {
          this._setData(res, isRefresh);
        }
      })
      .catch(e => {
        //加载失败
        console.log('加载失败',e);
        this.setState({ requestState: State.ERROR });
      });
  };
 _setData = (res, isRefresh) => {
    let data = res.object,
    pages = res.page;
    if (data && data.length > 0) {
      //判断一下是否还有下一页,因为一页最多20条,不满足则是无法去加载更多了
      // console.log('狗日的🐶', data.length,(this.state.data.length + data.length) < pages.totalRecords);
      this.setState({
        requestState: isRefresh
          ? State.NORMAL
          :  this.state.data.length + data.length < pages.totalRecords
          ? State.NORMAL
          : State.LOAD_END,
        data: isRefresh ? data : this.state.data.concat(data)
      });
    } else {
      //已经没有数据了,需要对footer进行处理
      if (this.currentPage === 0) {
        //第一页没有数据,那么就是当前接口无数据
        this.setState({
          requestState: State.NO_DATA,
          data: []
        });
      } else {
        //不是第一页,新页返回空,就是接下来没有数据了
        this.setState({
          requestState: State.LOAD_END,
          data: this.state.data
        });
      }
    }
  };

思考

以上FlatList 的封装和实现的基本完成,但是任然是感觉在实现层关心请求请求状态不够简洁,在思考,将api请求再包一层Promise,组件内部去处理请求状态,外部只关注接口返回的结果数据源?但是好像还是避免不了要去更新请求转态,这个可以思考下

Bye-bye~

comments