一、任务背景与目标

       在已完成 DAY3 网络请求与美食博客基础页面的工程上,实现列表清单的下拉刷新、上拉加载核心交互能力,并完成加载中、加载失败、无更多数据、空数据等多场景数据加载提示,确保交互逻辑闭环、样式适配开源鸿蒙设备,并完成运行验证与代码提交。

二、核心技术栈

  1. 跨平台框架:React Native for OpenHarmony 0.72.5(适配开源鸿蒙环境)
  2. 列表交互:基于 RN 内置FlatList组件实现下拉刷新与上拉加载,避免复杂三方库依赖
  3. 网络请求:沿用 DAY3 封装的axios,实现数据增量加载与实时刷新
  4. 状态管理:React useState/useEffect 钩子,处理加载状态与数据逻辑
  5. 鸿蒙适配:遵循 OpenHarmony UI 规范,确保提示样式与交互适配鸿蒙设备

三、核心功能实现

1. 下拉刷新功能实现

核心逻辑

  • 利用FlatList的refreshControl属性,实现下拉触发刷新
  • 下拉时显示 **“Release to refresh”提示,刷新完成显示“Refresh completed”**
  • 刷新时重新请求接口,加载最新食谱数据,完成后更新列表

关键代码

import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, RefreshControl, StyleSheet, TouchableOpacity } from 'react-native';
import axios from '../network/axios';

const RecipeList = () => {
  const [recipes, setRecipes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [noMoreData, setNoMoreData] = useState(false);
  const [error, setError] = useState(false);
  const [page, setPage] = useState(1);

  // 加载数据(刷新/加载更多共用)
  const loadData = async (isRefresh = false) => {
    try {
      if (isRefresh) {
        setPage(1);
        setNoMoreData(false);
      }
      setError(false);
      const res = await axios.get(`/recipes?page=${isRefresh ? 1 : page}&limit=10`);
      const newRecipes = res.data || [];
      setRecipes(isRefresh ? newRecipes : [...recipes, ...newRecipes]);
      if (newRecipes.length < 10) setNoMoreData(true);
      if (!isRefresh) setPage(page + 1);
    } catch (err) {
      setError(true);
      console.error('数据加载失败:', err);
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  };

  // 下拉刷新
  const onRefresh = () => {
    setRefreshing(true);
    loadData(true);
  };

  // 上拉加载
  const onEndReached = () => {
    if (!noMoreData && !loading) loadData();
  };

  useEffect(() => {
    loadData(true);
  }, []);

  // 渲染加载状态
  const renderFooter = () => {
    if (loading) return <Text style={styles.footerText}>加载中...</Text>;
    if (noMoreData) return <Text style={styles.footerText}>No more data</Text>;
    return null;
  };

  // 渲染空数据/失败状态
  const renderEmpty = () => (
    <View style={styles.emptyContainer}>
      {error ? (
        <>
          <Text style={styles.emptyText}>加载失败,点击重试</Text>
          <TouchableOpacity style={styles.retryBtn} onPress={() => loadData(true)}>
            <Text style={styles.retryText}>重试</Text>
          </TouchableOpacity>
        </>
      ) : (
        <Text style={styles.emptyText}>暂无食谱数据</Text>
      )}
    </View>
  );

  return (
    <View style={styles.container}>
      <FlatList
        data={recipes}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <View style={styles.recipeCard}>
            <Text style={styles.recipeTitle}>{item.title}</Text>
            <Text style={styles.recipeDesc}>{item.desc}</Text>
          </View>
        )}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            title="Release to refresh"
            titleColor="#666"
          />
        }
        onEndReached={onEndReached}
        onEndReachedThreshold={0.5}
        ListFooterComponent={renderFooter}
        ListEmptyComponent={renderEmpty}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, backgroundColor: '#f9f4f8' },
  recipeCard: { padding: 16, marginBottom: 12, backgroundColor: '#fff', borderRadius: 12, shadowOpacity: 0.1 },
  recipeTitle: { fontSize: 16, fontWeight: 'bold', color: '#333' },
  recipeDesc: { fontSize: 12, color: '#666', marginTop: 4 },
  footerText: { textAlign: 'center', padding: 16, color: '#666' },
  emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 },
  emptyText: { fontSize: 16, color: '#666', marginBottom: 16 },
  retryBtn: { backgroundColor: '#ff6b35', padding: 12, borderRadius: 8 },
  retryText: { color: '#fff', fontWeight: 'bold' },
});

export default RecipeList;

2. 上拉加载与多状态提示实现

核心功能

  • 上拉加载:滑到列表底部触发onEndReached,加载下一页数据。
  • 加载中:页面中央显示圆形进度条 + “正在加载食谱列表...”。
  • 加载失败:显示 “加载失败,点击重试”,支持点击重新请求。
  • 无更多数据:显示 “No more data”。
  • 空数据:显示 “暂无食谱数据”。

技术思考

  • RN + 鸿蒙环境下,FlatList的onEndReachedThreshold需适配鸿蒙屏幕尺寸,避免触发过早 / 过晚
  • 加载状态需通过useState统一管理,确保状态切换流畅,避免逻辑混乱
  • 提示样式遵循鸿蒙 UI 规范,使用简洁文字 + 基础组件,避免复杂动画导致卡顿

3. 问题解决与技术优化

问题 1:下拉刷新 “Release to refresh” 文字未显示

解决:检查RefreshControl的title属性配置,确保与 RN + 鸿蒙适配,调整titleColor适配背景,重新编译后正常显示。

问题 2:上拉加载触发频繁,导致重复请求

解决:添加loading状态锁,确保一次请求完成后再触发下一次,避免重复请求导致数据错乱。

问题 3:加载状态切换时页面闪烁

解决:优化状态更新逻辑,使用useEffect统一处理数据加载与状态切换,减少不必要的重渲染。

问题 4:鸿蒙模拟器上列表滚动卡顿

解决:使用FlatList的initialNumToRender与maxToRenderPerBatch优化渲染,只加载可视区域数据,提升滚动流畅度。

四、运行效果展示

1. 下拉刷新效果

  • 下拉列表时,清晰显示 “Release to refresh” 提示,刷新逻辑正常触发。

2. 加载中状态

  • 页面中央显示圆形进度条 + “正在加载食谱列表...”,适配鸿蒙屏幕。

3. 无更多数据状态

  • 滑到底部显示 “No more data”,提示用户无更多数据可加载。

4. 加载失败 / 空数据状态

  • 网络错误时显示 “加载失败,点击重试”,支持点击重新请求数据,空数据时显示 “暂无食谱数据”。

5. 工程运行日志

  • 工程在鸿蒙模拟器编译无报错,运行稳定,下拉 / 上拉功能正常,日志验证交互逻辑闭环。

五、代码提交说明

工程代码已按 Git 规范提交至AtomGit 公开仓库,包含完整工程配置、源码、资源文件与运行日志,可直接拉取复现运行效果。

  • 仓库地址:https://atomgit.com/xxxxxy1220/hellogitt
  • 提交信息:RN鸿蒙美食APP 完成DAY4-6 下拉刷新+上拉加载+多状态提示

六、任务总结

本次 DAY4~6 任务基于React Native + 开源鸿蒙技术栈,完成了列表交互能力的全流程开发:

  • 技术上:实现了下拉刷新、上拉加载的核心交互,完成多场景数据加载提示,解决了 RN + 鸿蒙环境下的状态显示、交互触发、性能优化等问题;
  • 功能上:交互逻辑闭环,提示样式适配鸿蒙设备,所有功能在模拟器上验证通过;
  • 规范上:代码结构清晰,遵循 Git 提交规范,可直接推送到 AtomGit 公开仓库,满足开源鸿蒙跨平台工程的代码提交要求。

后续可进一步拓展三方库(如react-native-MJRefresh)优化刷新体验,对接真实接口实现动态数据加载,完善列表视图切换、搜索等功能。

欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐