第 11 天|列表容器:List、Grid、Tabs、Swiper

1 阅读3分钟
本篇看点
  • 阅读目标:把列表、网格、Tab、轮播和分页放到同一套数据身份与状态模型里。
  • 核心问题:容器组件需要稳定数据身份、稳定尺寸和稳定状态。
  • 对比迁移:对应 Flutter ListView/GridView/TabBarView/PageView 和 iOS TableView/CollectionView。
前言
做 App 离不开容器组件:列表、网格、Tab、轮播。它们看起来只是展示更多 UI,但复杂度会突然上升,因为它们同时涉及滚动、复用、选中态、分页、刷新和性能。
Flutter 里你会用 ListView.builderGridViewTabBarViewPageView。iOS 里你会用 UITableViewUICollectionViewUIPageViewController。HarmonyOS 里常见组合就是 List/Grid/Tabs/Swiper
一. List 的核心不是 ForEach,而是数据身份
简单列表:
List() {
  ForEach(this.albums, (album: Album) => {
    ListItem() {
      AlbumRow({ album: album })
    }
  }, (album: Album) => album.id)
}
最后一行 key 很重要。它告诉框架每个 item 的身份是什么。对于会插入、删除、分页、刷新、排序的列表,稳定业务 id 通常比 index 更合适。
Flutter 里类似 ValueKey(album.id);iOS DiffableDataSource 也要求 item identity 稳定。没有稳定身份,局部刷新和动画都会变得不可靠。
二. Grid 要先确定卡片比例
音频 App 常见专辑网格,一般需要固定封面比例和文字行数:
Grid() {
  ForEach(this.albums, (album: Album) => {
    GridItem() {
      Column() {
        Image(album.cover).aspectRatio(1)
        Text(album.title)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
    }
  }, (album: Album) => album.id)
}
.columnsTemplate('1fr 1fr 1fr')
如果封面不固定比例,文字不限行,网格会很快变成瀑布流一样高低不齐。除非你本来就想做瀑布流,否则内容卡片应该稳定。
三. Tabs 的选中态要和内容态绑定
Tab 不是几个按钮。它至少包含:
  1. 当前选中 index。
  2. 当前选中的业务 id。
  3. 切换时是否加载数据。
  4. 是否支持滑动切换。
  5. 选中项是否滚动到可视中心。
一个基础写法:
@State selectedIndex: number = 0;

Tabs({ index: this.selectedIndex }) {
  ForEach(this.tabs, (tab: CategoryTab, index: number) => {
    TabContent() {
      CategoryPage({ tabId: tab.id })
    }
    .tabBar(tab.title)
  }, (tab: CategoryTab) => tab.id)
}
.onChange((index: number) => {
  this.selectedIndex = index;
  this.loadTab(this.tabs[index].id);
})
喜马拉雅发现页里,“推荐、小说、音乐、广播”等分类就应该按这个思路处理:每个 tab 对应自己的 mock 数据,点击或滑动切换内容。
四. Swiper 适合轮播,但要关心尺寸
轮播图如果太小,效果会很弱。推荐页的轮播通常应该独占一行,宽度跟随屏幕,保持稳定高度:
Swiper() {
  ForEach(this.banners, (banner: Banner) => {
    Image(banner.image)
      .width('100%')
      .height(150)
      .borderRadius(10)
  }, (banner: Banner) => banner.id)
}
.autoPlay(true)
.interval(3000)
Flutter 中这类能力常用 PageView 加 timer;iOS 里可能用 UICollectionView 横向分页加 timer。HarmonyOS 的 Swiper 能更直接表达轮播,但数据身份、尺寸和空态仍然要自己管。
五. 分页列表一定要防竞态
上拉加载时最常见的 bug 是重复触发,最后 page 错乱:
async loadMore() {
  if (this.loadingMore || !this.hasMore) {
    return;
  }
  this.loadingMore = true;
  const nextPage = this.page + 1;
  const result = await this.repository.loadPage(nextPage);
  this.items = this.items.concat(result.items);
  this.page = nextPage;
  this.hasMore = result.hasMore;
  this.loadingMore = false;
}
真实项目里还要加错误处理和请求版本号,防止旧请求覆盖新请求。后面的网络分页文章会展开讲。
六. 实践经验
第一,列表 key 用 index,刷新后 item 状态串位。
第二,Tab 只改了标题选中态,没有真正切换内容数据。
第三,Swiper 没有固定高度,图片加载前后页面跳动。
第四,上拉加载没有防重复,用户滑快一点就请求多页。
小结
容器组件的重点不是“怎么把数据画出来”,而是稳定身份、稳定尺寸、稳定状态。列表、网格、Tab、轮播写好了,App 的骨架才站得住。
今日练习
  1. 把一个列表的 key 从 index 改成业务 id。
  2. 写一个 Tab 状态,保证点击和滑动都能切换内容数据。
  3. 给 Swiper 设置固定高度和 autoplay,避免图片加载前后布局跳动。