HarmonyOS V2状态管理深度解析:列表数据与分页架构

一、列表数据管理的复杂性

1.1 列表场景的核心挑战

列表数据管理是移动应用开发中最常见也是最复杂的场景之一。主要挑战包括:

挑战 描述 技术难点
大数据量 列表可能包含大量数据 性能优化、内存管理
分页加载 需要管理多页数据 状态同步、边界处理
搜索过滤 动态筛选数据 状态联动、缓存策略
状态保持 滚动位置、选中状态 状态持久化
性能优化 流畅的滚动体验 虚拟列表、懒加载

1.2 列表状态的维度

一个完整的列表状态需要管理多个维度:

typescript 复制代码
interface ListState<T> {
  // 数据维度
  items: T[];           // 当前页数据
  allItems: T[];        // 全部数据
  filteredItems: T[];   // 过滤后数据
  
  // 分页维度
  currentPage: number;  // 当前页码
  pageSize: number;     // 每页大小
  totalCount: number;   // 总记录数
  totalPages: number;   // 总页数
  
  // 筛选维度
  searchKeyword: string;    // 搜索关键词
  selectedCategory: string; // 选中分类
  
  // 状态维度
  loading: boolean;     // 加载状态
  refreshing: boolean;  // 刷新状态
  error: string;        // 错误信息
}

1.3 列表状态管理的架构层次

复制代码
┌─────────────────────────────────────────────────────┐
│                    UI Layer                         │
│  (List组件、ListItem、搜索框、分页器)                │
├─────────────────────────────────────────────────────┤
│                   Store Layer                       │
│  (ListStore、状态管理、业务逻辑)                     │
├─────────────────────────────────────────────────────┤
│                  Data Layer                         │
│  (API调用、数据转换、缓存管理)                        │
└─────────────────────────────────────────────────────┘

二、列表状态管理的核心模式

2.1 分页状态管理

typescript 复制代码
@ObservedV2
class ListDataStore {
  @Trace items: ListItem[] = [];
  @Trace filteredItems: ListItem[] = [];
  @Trace loading: boolean = false;
  
  @Trace pagination: PaginationState = {
    currentPage: 1,
    pageSize: 10,
    totalCount: 0,
    totalPages: 0
  };
  
  @Trace selectedCategory: string = 'all';
  @Trace searchKeyword: string = '';
  
  private allItems: ListItem[] = [];

  async loadItems(): Promise<void> {
    this.loading = true;
    await this.delay(500);

    let filtered = this.allItems;

    // 分类过滤
    if (this.selectedCategory !== 'all') {
      filtered = filtered.filter(item => item.category === this.selectedCategory);
    }

    // 搜索过滤
    if (this.searchKeyword.trim()) {
      const keyword = this.searchKeyword.toLowerCase();
      filtered = filtered.filter(item =>
        item.title.toLowerCase().includes(keyword) ||
        item.description.toLowerCase().includes(keyword)
      );
    }

    this.filteredItems = filtered;
    this.pagination.totalCount = filtered.length;
    this.pagination.totalPages = Math.ceil(filtered.length / this.pagination.pageSize);

    // 分页截取
    const start = (this.pagination.currentPage - 1) * this.pagination.pageSize;
    const end = start + this.pagination.pageSize;
    this.items = filtered.slice(start, end);

    this.loading = false;
  }

  async loadNextPage(): Promise<void> {
    if (this.hasNextPage) {
      this.pagination.currentPage++;
      await this.loadItems();
    }
  }

  async loadPreviousPage(): Promise<void> {
    if (this.hasPreviousPage) {
      this.pagination.currentPage--;
      await this.loadItems();
    }
  }

  get hasNextPage(): boolean {
    return this.pagination.currentPage < this.pagination.totalPages;
  }

  get hasPreviousPage(): boolean {
    return this.pagination.currentPage > 1;
  }
}

2.2 状态联动机制

当搜索关键词或分类变化时,需要联动更新分页状态:

typescript 复制代码
setCategory(category: string): void {
  this.selectedCategory = category;
  this.pagination.currentPage = 1;  // 重置到第一页
  this.loadItems();
}

setSearchKeyword(keyword: string): void {
  this.searchKeyword = keyword;
  this.pagination.currentPage = 1;  // 重置到第一页
  this.loadItems();
}

2.3 数据初始化策略

typescript 复制代码
private constructor() {
  this.initializeData();
}

private initializeData(): void {
  const categories = ['技术', '设计', '产品', '运营', '市场'];
  const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];

  for (let i = 1; i <= 50; i++) {
    this.allItems.push({
      id: i,
      title: `项目 ${i}`,
      description: `这是第 ${i} 个项目的详细描述`,
      category: categories[Math.floor(Math.random() * categories.length)],
      status: statuses[Math.floor(Math.random() * statuses.length)],
      createdAt: this.generateRandomDate()
    });
  }
}

三、列表组件实现

3.1 完整的列表页面

typescript 复制代码
@Entry
@ComponentV2
struct ListDataPage {
  @Local searchText: string = '';
  private store = listDataStore;
  private categories: string[] = ['all', '技术', '设计', '产品', '运营', '市场'];

  aboutToAppear(): void {
    this.store.loadItems();
  }

  build() {
    Column({ space: 16 }) {
      // 搜索框
      this.buildSearchBar();
      
      // 分类筛选
      this.buildCategoryFilter();
      
      // 列表内容
      this.buildList();
      
      // 分页器
      this.buildPagination();
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#F5F5F7')
  }

  @Builder
  buildSearchBar(): void {
    Row({ space: 12 }) {
      Image($r('app.media.search'))
        .width(20)
        .height(20)
        .fillColor('#999999')
      
      TextInput({ placeholder: '搜索项目...', text: this.searchText })
        .flexGrow(1)
        .height(44)
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .onChange((value: string) => {
          this.searchText = value;
          this.store.setSearchKeyword(value);
        })
    }
    .padding({ left: 12, right: 12 })
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }

  @Builder
  buildCategoryFilter(): void {
    Scroll({ direction: Axis.Horizontal }) {
      Row({ space: 8 }) {
        ForEach(this.categories, (category: string) => {
          Button(category === 'all' ? '全部' : category)
            .width(60)
            .height(32)
            .backgroundColor(this.store.selectedCategory === category ? '#007DFF' : '#F0F0F2')
            .fontColor(this.store.selectedCategory === category ? '#FFFFFF' : '#666666')
            .fontSize(12)
            .borderRadius(16)
            .onClick(() => this.store.setCategory(category))
        })
      }
      .padding({ right: 16 })
    }
    .backgroundColor('#FFFFFF')
    .padding(12)
    .borderRadius(8)
  }

  @Builder
  buildList(): void {
    if (this.store.loading) {
      this.buildLoadingState();
    } else if (this.store.items.length === 0) {
      this.buildEmptyState();
    } else {
      this.buildDataList();
    }
  }

  @Builder
  buildLoadingState(): void {
    Column({ space: 12 }) {
      LoadingProgress()
        .width(48)
        .height(48)
        .color('#007DFF')
      Text('加载中...')
        .fontSize(14)
        .fontColor('#666666')
    }
    .width('100%')
    .height(300)
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  buildEmptyState(): void {
    Column({ space: 12 }) {
      Text('📋')
        .fontSize(48)
      Text('暂无匹配的数据')
        .fontSize(14)
        .fontColor('#999999')
    }
    .width('100%')
    .height(300)
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  buildDataList(): void {
    List({ space: 12 }) {
      ForEach(this.store.items, (item: ListItem) => {
        ListItem() {
          this.buildListItem(item);
        }
      })
    }
    .width('100%')
    .layoutWeight(1)
    .scrollBar(BarState.Off)
  }

  @Builder
  buildListItem(item: ListItem): void {
    Column({ space: 8 }) {
      Row({ space: 12 }) {
        Column({ space: 4 }) {
          Text(item.category)
            .fontSize(10)
            .fontColor('#007DFF')
            .backgroundColor('#E8F4FF')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .borderRadius(4)
          Text(item.createdAt)
            .fontSize(10)
            .fontColor('#999999')
        }
        
        Column({ space: 4 }) {
          Text(item.title)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
          Text(item.description)
            .fontSize(12)
            .fontColor('#666666')
            .maxLines(2)
        }
        .flexGrow(1)
        
        this.buildStatusBadge(item.status)
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }

  @Builder
  buildStatusBadge(status: string): void {
    let color: string;
    let text: string;
    
    switch (status) {
      case 'active':
        color = '#2ED573';
        text = '进行中';
        break;
      case 'inactive':
        color = '#FF4757';
        text = '已停止';
        break;
      case 'pending':
        color = '#FFA502';
        text = '待处理';
        break;
      default:
        color = '#999999';
        text = status;
    }
    
    Text(text)
      .fontSize(10)
      .fontColor('#FFFFFF')
      .backgroundColor(color)
      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
      .borderRadius(4)
  }

  @Builder
  buildPagination(): void {
    Row({ space: 8 }) {
      // 上一页
      Button('‹')
        .width(40)
        .height(40)
        .backgroundColor(this.store.hasPreviousPage ? '#007DFF' : '#E0E0E0')
        .enabled(this.store.hasPreviousPage)
        .onClick(() => this.store.loadPreviousPage())
      
      // 页码显示
      Text(`${this.store.pagination.currentPage} / ${this.store.pagination.totalPages}`)
        .fontSize(14)
        .fontColor('#666666')
      
      // 下一页
      Button('›')
        .width(40)
        .height(40)
        .backgroundColor(this.store.hasNextPage ? '#007DFF' : '#E0E0E0')
        .enabled(this.store.hasNextPage)
        .onClick(() => this.store.loadNextPage())
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }
}

3.2 列表项组件化

typescript 复制代码
@ComponentV2
struct ListItemCard {
  @Param item: ListItem;

  build() {
    Column({ space: 8 }) {
      Row({ space: 12 }) {
        Column({ space: 4 }) {
          Text(this.item.category)
            .fontSize(10)
            .fontColor('#007DFF')
            .backgroundColor('#E8F4FF')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .borderRadius(4)
          Text(this.item.createdAt)
            .fontSize(10)
            .fontColor('#999999')
        }
        
        Column({ space: 4 }) {
          Text(this.item.title)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
          Text(this.item.description)
            .fontSize(12)
            .fontColor('#666666')
            .maxLines(2)
        }
        .flexGrow(1)
        
        this.buildStatusBadge(this.item.status)
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }

  @Builder
  buildStatusBadge(status: string): void {
    let color: string;
    let text: string;
    
    switch (status) {
      case 'active':
        color = '#2ED573';
        text = '进行中';
        break;
      case 'inactive':
        color = '#FF4757';
        text = '已停止';
        break;
      case 'pending':
        color = '#FFA502';
        text = '待处理';
        break;
      default:
        color = '#999999';
        text = status;
    }
    
    Text(text)
      .fontSize(10)
      .fontColor('#FFFFFF')
      .backgroundColor(color)
      .padding({ left: 8, right: 8, top: 2, bottom: 2 })
      .borderRadius(4)
  }
}

四、高级分页策略

4.1 无限滚动加载

typescript 复制代码
@Entry
@ComponentV2
struct InfiniteScrollPage {
  private store = infiniteScrollStore;

  build() {
    Column({ space: 12 }) {
      List({ space: 12 }) {
        ForEach(this.store.items, (item: ListItem) => {
          ListItem() {
            ListItemCard({ item })
          }
          .onAppear(() => {
            // 当最后一项出现时加载下一页
            const index = this.store.items.indexOf(item);
            if (index === this.store.items.length - 1 && this.store.hasNextPage) {
              this.store.loadNextPage();
            }
          })
        })
        
        // 加载更多提示
        if (this.store.loading && this.store.items.length > 0) {
          ListItem() {
            Row({ space: 8 }) {
              LoadingProgress()
                .width(20)
                .height(20)
              Text('加载更多...')
                .fontSize(14)
                .fontColor('#666666')
            }
            .width('100%')
            .height(60)
            .justifyContent(FlexAlign.Center)
          }
        }
      }
      .width('100%')
      .layoutWeight(1)
    }
  }
}

4.2 虚拟列表优化

typescript 复制代码
@Entry
@ComponentV2
struct VirtualListPage {
  @Local items: ListItem[] = [];
  
  aboutToAppear(): void {
    // 生成大量数据
    for (let i = 1; i <= 1000; i++) {
      this.items.push({
        id: i,
        title: `项目 ${i}`,
        description: `描述 ${i}`,
        category: '技术',
        status: 'active',
        createdAt: '2024-01-01'
      });
    }
  }

  build() {
    Column() {
      List({ space: 12, scroller: this.scroller }) {
        LazyForEach(this.items, (item: ListItem) => {
          ListItem() {
            ListItemCard({ item })
          }
          .height(100)
        })
      }
      .width('100%')
      .layoutWeight(1)
      .cachedCount(30)  // 缓存30个列表项
    }
  }
}

4.3 分页缓存策略

typescript 复制代码
@ObservedV2
class CachedListStore {
  private pageCache: Map<number, ListItem[]> = new Map();

  async loadPage(page: number): Promise<ListItem[]> {
    // 检查缓存
    if (this.pageCache.has(page)) {
      return this.pageCache.get(page)!;
    }

    // 实际加载
    const items = await this.fetchPage(page);
    
    // 缓存结果
    this.pageCache.set(page, items);
    
    return items;
  }

  clearCache(): void {
    this.pageCache.clear();
  }
}

五、状态同步与更新策略

5.1 不可变更新模式

typescript 复制代码
// 错误:直接修改数组
this.items.push(newItem);

// 正确:创建新数组
this.items = [...this.items, newItem];

// 对象更新
this.pagination = { ...this.pagination, currentPage: 2 };

5.2 批量更新优化

typescript 复制代码
async loadItems(): Promise<void> {
  this.loading = true;
  
  const newItems = await this.fetchData();
  const newTotal = await this.fetchTotalCount();
  
  // 批量更新状态
  this.items = newItems;
  this.pagination = {
    ...this.pagination,
    totalCount: newTotal,
    totalPages: Math.ceil(newTotal / this.pagination.pageSize)
  };
  
  this.loading = false;
}

5.3 状态变更通知

typescript 复制代码
class ListStore {
  private subscribers: Set<() => void> = new Set();

  protected notify(): void {
    this.subscribers.forEach(callback => callback());
  }

  subscribe(callback: () => void): () => void {
    this.subscribers.add(callback);
    return () => this.subscribers.delete(callback);
  }
}

六、性能优化技术

6.1 列表项复用

typescript 复制代码
List({ space: 12 }) {
  ForEach(this.store.items, (item: ListItem, index: number) => {
    ListItem() {
      ItemComponent({ 
        item, 
        index,
        key: item.id.toString()
      })
    }
    .sticky(StickyStyle.Header)
    .editable(true)
  }, (item: ListItem) => item.id.toString())
}

6.2 减少重渲染

typescript 复制代码
@Builder
buildListItem(item: ListItem): void {
  Column({ space: 8 }) {
    // 静态内容
    Text(item.title)
      .fontSize(16)
    
    // 动态内容(可能频繁变化)
    if (item.status === 'active') {
      Text('进行中')
        .fontColor('#2ED573')
    }
  }
}

6.3 滚动性能优化

typescript 复制代码
List({ space: 12 }) {
  ForEach(this.items, (item) => {
    ListItem() {
      ItemCard({ item })
    }
    .decoration({
      shadow: { radius: 4, color: '#00000010', offsetX: 0, offsetY: 2 }
    })
  })
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.cachedCount(20)

七、测试与调试

7.1 单元测试

typescript 复制代码
describe('ListDataStore', () => {
  it('should load items with pagination', async () => {
    const store = new ListDataStore();
    
    await store.loadItems();
    
    expect(store.items.length).toBe(10);
    expect(store.pagination.currentPage).toBe(1);
    expect(store.pagination.totalPages).toBe(5);
  });

  it('should filter items by category', async () => {
    const store = new ListDataStore();
    
    store.setCategory('技术');
    
    expect(store.selectedCategory).toBe('技术');
    expect(store.pagination.currentPage).toBe(1);
  });

  it('should search items', async () => {
    const store = new ListDataStore();
    
    store.setSearchKeyword('项目1');
    
    expect(store.searchKeyword).toBe('项目1');
  });
});

7.2 集成测试

typescript 复制代码
it('should display correct number of items', async () => {
  const page = new ListDataPage();
  page.build();
  
  await waitFor(() => {
    return page.store.items.length === 10;
  });
});

it('should navigate to next page', async () => {
  const page = new ListDataPage();
  page.build();
  
  await page.store.loadNextPage();
  
  expect(page.store.pagination.currentPage).toBe(2);
});

八、最佳实践总结

8.1 状态管理原则

  1. 状态集中:将列表相关状态集中在一个Store中
  2. 状态分离:区分数据状态、分页状态、筛选状态
  3. 不可变更新:使用不可变方式更新状态
  4. 状态验证:更新前验证状态合法性

8.2 列表性能原则

  1. 虚拟列表:大数据量使用LazyForEach
  2. 缓存策略:缓存已加载的页面数据
  3. 减少重渲染:拆分静态和动态内容
  4. 滚动优化:使用cachedCount优化滚动性能

8.3 代码组织建议

复制代码
src/
├── stores/
│   ├── ListDataStore.ts
│   └── types.ts
├── pages/
│   └── ListDataPage.ts
├── components/
│   └── ListItemCard.ts
└── utils/
    └── pagination.ts

参考资料

相关推荐
Ztopcloud极拓云视角2 小时前
ChatGPT超级应用改版技术解析:Codex集成架构与多模型路由实战
人工智能·chatgpt·架构
坚果派·白晓明11 小时前
【鸿蒙PC】SDL3 适配:AtomCode + Skills 快速集成 NAPI 测试工具
c++·华为·ai编程·harmonyos·atomcode
YM52e12 小时前
男孩子在外自我保护指南——用鸿蒙 ArkTS 构建交互式安全教育应用
学习·安全·华为·harmonyos·鸿蒙·鸿蒙系统
逻极12 小时前
Hermes Agent深度探索:一个会自我沉淀经验的终端智能体
架构·llm·agent·rag·多智能体系统·hermes agent·hermes
祭曦念12 小时前
古诗小集开发实战:从零开发一款 HarmonyOS 古诗鉴赏应用
pytorch·深度学习·harmonyos
数智顾问13 小时前
(151页PPT)XX集团信息化整体架构规划及ERP方案建议书(附下载方式)
大数据·架构
caimouse13 小时前
Reactos 第1章 概述
c语言·开发语言·架构
namexingyun13 小时前
拆解Fable 5三重安全护栏:模型路由、蒸馏防护与生物安全分类器的技术原理 - 微元算力(weytoken)
java·人工智能·python·安全·架构·ai编程