鸿蒙-NestedScrollTabContainer-mvvm-demo
之前学了安卓版的吸顶嵌套可左右翻页上下滑动的rv:
安卓demo-CeilingNestedScrollView-CSDN博客之后,下一个需求就是做鸿蒙版的这个需求,然后鸿蒙也没有这种组件,凭借俺对安卓的理解,也很快promot出了鸿蒙版,哈哈哈哈,真是机智如俺,不过这个没有tabbar吸顶效果哈。之前很少用mvvm,结果现在项目全是mvvm。嗯,用mvvm重构一下吧,再封装成单个组件(人模狗样版)
组件封装
1.背景
NestedScrollTabContainer 是一个可复用的"嵌套滑动 Tab 容器",解决 Scroll + Swiper + List 三层嵌套下的滑动分发问题。 典型场景:顶部 Header 区域 + 多 Tab 内列表区,手指上滑先滚走 Header 再滚列表;手指下滑先把内层列表滚到顶,再让外层 Scroll 把 Header 显示出来。
2.划分原则
共性、稳定的布局进容器;业务差异大的留外面。
封装时问自己三句话:
-
不同页面这段代码会一样吗?一样 → 进容器;不一样 → 留外面。
-
这段代码跟业务数据有关吗?无关 → 进容器;有关系 → 留外面。
-
这段代码要不要被外部主动触发?要(如 scrollTo / changeIndex)→ 不放在容器方法里,改用控制器由外部持有。
3.进容器(共性)
以下内容由容器内部 build() 承担,调用方无需关心:
| 维度 | 内容 |
|---|---|
| 外层滚动 | Scroll(this.parentScroller) { Column() { ... } } |
| Swiper 通用配置 | indicator(false) / loop(false) / width('100%') / height('100%') |
| 内层 Swiper 与控制器绑定 | Swiper(this.swiperController) |
| 滑动分发方向 | nestedScroll({ scrollForward: SELF_ONLY, scrollBackward: SELF_ONLY }) |
| 多层嵌套手势分发 | ScrollForward 先消费外层 → 再内层;ScrollBackward 先内层 → 再外层 |
| 第一Tab右滑退出手势监听 | onGestureSwipe:第一个Tab且向右滑动时触发回调 |
| 防重复退出标记 | private isPopping,节流 onFirstTabSwipeOut |
| 背景样式 | 支持 bgColor 纯色 或 useLinearGradient 渐变(二选一) |
4.滑动冲突解决方式
容器内部存在两层嵌套滚动:外层 Scroll(包裹 Header + Swiper)与内层 List(每个 Tab 的列表)。 关键手法是用 ArkUI 的 nestedScroll 配置 + 外层 Scroll 采用 SELF_ONLY,把滑动事件链路拆解成可预期的优先级:
手指上滑(scrollForward,内容向上滚): 外层 Scroll 先消费 → Header 被滚走 → 然后内层 List 接管继续滚 内层 List 配置:nestedScroll({ scrollForward: PARENT_FIRST, scrollBackward: SELF_FIRST })
手指下滑(scrollBackward,内容向下滚): 内层 List 先消费 → 列表滚到顶 → 然后外层 Scroll 接管 → Header 显示出来
外层 Scroll 用 SELF_ONLY 的用意: 确保外层与内层的 nestedScroll 链路独立可控,避免外层"无脑"拦截事件导致内层 List 在 Header 还没滚走时就已经开始滚动。
Tab 间横向切换: 由 Swiper 自身处理,不参与纵向滑动分发;onGestureSwipe 监听第一个 Tab 的右滑手势,触发 onFirstTabSwipeOut 回调(通常调 viewModel.popPage)。
5.留外面(业务差异大)
以下内容由调用方自行实现,通过 builder / callback 注入:
| 维度 | 注入方式 | 说明 |
|---|---|---|
| Header 布局 | @BuilderParam headerBuilder | 每个页面 Header 结构差异大,不强行参数化,调用方写 @Builder 直接嵌入 |
| Tab 内容(每个Tab的List结构) | @BuilderParam contentBuilder | 每页 Item 渲染逻辑完全不同,调用方根据 currentTabIndex 渲染 |
| Tab 切换的业务回调 | onTabChange?: (index) => void | 例如切换Tab后刷新对应列表、切换tabType 维度等 |
| 当前Tab索引来源 | @Prop currentTabIndex | 由调用方 @State 维护,并通过 onTabChange 同步 |
| 第一Tab右滑退出动作 | onFirstTabSwipeOut?: () => void | 通常调用 viewModel.popPage() |
| 内层 List 的 Scroller 数组 | Page 自己持有 Scroller[] | Page 在 tabContentBuilder 里按 tab.index 选用对应 Scroller 绑定到 List |
6.控制器传递约定
ArkTS 中 @Component 不能被外部 new 实例化,外部无法直接持有容器引用来调方法。 因此改为 "外部持有控制器,传给容器使用" 的反向依赖:
| 控制器 | 声明方 | 用途 |
|---|---|---|
| SwiperController | 外部 Page | 外部点击 Tab 时调 changeIndex(index) 切换;容器内部用同一实例绑定 Swiper |
| Scroller(外层) | 外部 Page | 外部 scrollToTop() 等场景调用 scrollTo(...);容器内部用同一实例绑定 Scroll |
| Scroller(每个Tab) | 外部 Page | 外部维护一个 Scroller[],按当前 tabIndex 选对应实例调用 |
俺写大需求终于是先做架构设计再写代码了,而不是直接一上来就代码的暴发户土狗了。
架构简述
1.分层结构
nestedscroll/
├── pages/
│ └── NestedScrollDemoPage.ets ← View 层(ArkTS struct @Component)
├── viewmodel/
│ ├── INestedScrollViewModel.ets ← ViewModel 接口 + Callback 接口
│ └── NestedScrollViewModelImpl.ets ← ViewModel 实现(@ObservedV2 + @Trace)
├── model/
│ ├── INestedScrollModel.ets ← Model 接口
│ ├── NestedScrollModelImpl.ets ← Model 实现(mock Tab 数据)
│ └── NestedScrollTabConfig.ets ← DTO
└── components/
└── NestedScrollTabContainer.ets ← 业务专用 UI 组件
2.通信方式
单向数据流 + 双向控制流
┌────────────────┐ setCallback(callback) ┌──────────────────┐
│ │ ─────────────────────────────────► │ │
│ View (Page) │ │ ViewModel │
│ │ ◄───────────────────────────────── │ (@ObservedV2) │
└────────────────┘ @Trace 字段变化触发 UI 重渲染 └──────────────────┘
│ ▲ │
│ 用户事件 │ Promise resolve/reject │ 调用
│ onTabClick/onSwiper │ ▼
│ TabChange │ ┌──────────────────┐
└────────────────────────┼──────────────────────►│ Model │
│ │ (mock 数据) │
│ └──────────────────┘
│
callback.popPage / showLoading / ...
3.三个方向的通信
1. View → ViewModel(控制流,下行)
Page 通过持有 viewModel 实例直接调接口方法:
this.viewModel.pageOnReady(); // 页面就绪时拉数据
this.viewModel.onTabClick(tabIndex); // 点击 Tab
this.viewModel.onSwiperTabChange(idx); // Swiper 滑动
2. ViewModel → View(状态流,上行)
ViewModel 用 @ObservedV2 + @Trace 暴露状态,Page 在 build() 里直接读这些字段。字段变化时 ArkUI 自动触发重渲染:
// ViewModelImpl
@Trace tabs: NestedScrollTabConfig[] = [];
@Trace currentTabIndex: number = 0;
@Trace pageLoading: boolean = false;
// Page 的 build()
Tabs({ index: this.viewModel.currentTabIndex }) { ... }
3. ViewModel → View(事件流,异步回调)
Page 在 aboutToAppear 注入 callback、aboutToDisappear 反注册。ViewModel 通过 callback 通知 Page 执行"非状态型"操作(弹 Toast、跳转页面等):
// Page 注入
private nestedScrollViewModelCallback: NestedScrollViewModelCallback = {
popPage: (): void => { this.pathStack.pop(); }
};
aboutToAppear() {
this.viewModel.setCallback(this.nestedScrollViewModelCallback);
}
aboutToDisappear() {
this.viewModel.setCallback(undefined); // 反注册,防内存泄漏
}
// ViewModel 触发
private callback?.popPage?.();
4. ViewModel → Model(数据流)
ViewModel 内部 new ModelImpl,调 Model 的方法拿 Promise:
this.nestedScrollModel.queryTabs().then(tabs => {
this.tabs = tabs; // @Trace 字段赋值,自动触发 UI 刷新
}).catch(...);
4.关键约束
| 约束 | 做法 |
|---|---|
| ViewModel 不持有 View | 通过 callback 接口反向调用,不 import Page |
| Model 不持有 ViewModel | 只返回 Promise,由 ViewModel .then 处理 |
| 状态可观察 | @ObservedV2 + @Trace,UI 自动响应 |
| 生命周期对称 | aboutToAppear 注入 / aboutToDisappear 反注册 |
| 闭包不泄漏 | callback 是 Page 成员,反注册后 ViewModel 断引用 |
整体是标准的单向数据流 MVVM,《数据下行、事件上行、状态自动同步》。
安卓vs鸿蒙差异
安卓端结构(NestedScrollLayout.java)
NestedScrollLayout (NestedScrollingParent)
└── 外层 RecyclerView
├── HeaderVH (header)
└── FooterVH
└── ViewPager (左右切换Tab)
└── Fragment → 内层 RecyclerView (列表)
安卓冲突解决方式(onNestedPreScroll):
* isPinnedMode():当 Footer 完全可见(Header 滚走了),进入 pinned 模式
* pinned 模式:滚动优先给内层 child RV
* 非 pinned 模式:滚动给外层 parent RV(滚走 Header)
鸿蒙对应结构:
外层 Scroll (parentScroller) ← 对应安卓外层 RecyclerView
├── Header (headerBuilder) ← 对应安卓 HeaderVH
├── Tab (tabBarBuilder) ← 对应安卓 HeaderVH 中的 Tab
└── Swiper (swiperController) ← 对应安卓 FooterVH 中的 ViewPager
└── List (nestedScroll) ← 对应安卓内层 RecyclerView
鸿蒙冲突解决方式(nestedScroll):
* 内层 List:
* scrollForward: PARENT_FIRST — 手指上滑时,外层 Scroll 先消费(滚走 Header),然后内层 List 再滚
* scrollBackward: SELF_FIRST — 手指下滑时,内层 List 先消费(滚到顶),然后外层 Scroll 再滚(显示 Header)
* 外层 Scroll:SELF_ONLY — 自己处理自己的滚动
效果视频
无,参考安卓的效果吧。效果图可以来一张

源码纯享版

核心组件
负责当组件核心容器,响应滑动事件,tab改变
/**
* 嵌套滑动Tab容器 (Demo)
*
* 结构:外层 Scroll 包裹 Header + Swiper,Swiper 内每个 Tab 是 List。
*
* 滑动分发:
* - 手指上滑(scrollForward):外层 Scroll 先消费(滚走 Header),然后内层 List 再滚 => PARENT_FIRST
* - 手指下滑(scrollBackward):内层 List 先消费(滚到顶),然后外层 Scroll 再滚(显示 Header) => SELF_FIRST
*
* 注意:外层 Scroll 使用 SELF_ONLY,确保外层与内层 nestedScroll 链路独立可控。
*/
@Component
export struct NestedScrollTabContainer {
/** 当前 Tab 索引 */
@Prop currentTabIndex: number = 0;
@Prop bgColor: string = '#FFFFFF';
@Prop useLinearGradient: boolean = false;
@Prop gradientAngle: number = 180;
@Prop gradientColors: Array<[string, number]> = [];
/** 内层 Swiper 控制器(由外部传入,便于外部主动 changeIndex) */
swiperController: SwiperController = new SwiperController();
/** 外层 Scroll 控制器(由外部传入,便于外部 scrollTo) */
parentScroller: Scroller = new Scroller();
/** 防止重复退出 */
private isPopping: boolean = false;
/** Tab 切换回调 */
onTabChange?: (index: number) => void;
/** 第一个 Tab 右滑退出页面回调 */
onFirstTabSwipeOut?: () => void;
/** Header 区域内容 */
@BuilderParam headerBuilder: () => void = this.defaultHeader;
/** Swiper 中的每个 Tab 的内容(由外部按 currentTabIndex 渲染) */
@BuilderParam contentBuilder: () => void = this.defaultContent;
@Builder
private defaultHeader() {
}
@Builder
private defaultContent() {
}
build() {
Stack() {
Scroll(this.parentScroller) {
Column() {
this.headerBuilder()
Swiper(this.swiperController) {
this.contentBuilder()
}
.index(this.currentTabIndex)
.indicator(false)
.loop(false)
.width('100%')
.height('100%')
.onChange((index: number) => {
if (this.currentTabIndex !== index) {
this.onTabChange?.(index);
}
})
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
// 第一个 Tab 且向右滑出时,触发外部退出逻辑
if (index === 0 && extraInfo.currentOffset > 0 && !this.isPopping) {
this.isPopping = true;
this.onFirstTabSwipeOut?.();
}
})
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.None)
.linearGradient(this.useLinearGradient ? {
angle: this.gradientAngle,
colors: this.gradientColors
} : {
angle: 0,
colors: []
})
.nestedScroll({
scrollForward: NestedScrollMode.SELF_ONLY,
scrollBackward: NestedScrollMode.SELF_ONLY
})
}
.width('100%')
.height('100%')
.backgroundColor(this.useLinearGradient ? Color.Transparent : this.bgColor)
}
}
model抽象层,代码少不用这一个抽象层,只要model就好了,代码太少model可能都不用,这里只是意思一下,因为俺在学mvvm。标准开发一般没这么少接口
/**
* 嵌套滑动 Demo Model 接口
*/
import { NestedScrollTabConfig } from "./NestedScrollTabConfig";
export interface INestedScrollModel {
/**
* 查询 Tab 列表数据
*/
queryTabs(): Promise<NestedScrollTabConfig[]>;
}
model数据源
/**
* 嵌套滑动 Demo Model 实现
*/
import { INestedScrollModel } from './INestedScrollModel';
import { NestedScrollTabConfig, NestedScrollListItem } from './NestedScrollTabConfig';
const TAB_COUNT = 4;
const ITEM_COUNT_PER_TAB = 30;
const TAB_NAMES: string[] = ['推荐', '关注', '热门', '最新'];
const TAB_THEME_COLORS: string[] = ['#33B62E', '#1E88E5', '#E53935', '#8E24AA'];
const ITEM_COLORS: string[] = ['#FFFFFF', '#F5F5F5', '#FFF8E1', '#E8F5E9'];
export class NestedScrollModelImpl implements INestedScrollModel {
queryTabs(): Promise<NestedScrollTabConfig[]> {
return new Promise<NestedScrollTabConfig[]>((resolve, reject) => {
try {
const tabs = this.buildMockTabs();
resolve(tabs);
} catch (e) {
reject(e);
}
});
}
private buildMockTabs(): NestedScrollTabConfig[] {
const tabs: NestedScrollTabConfig[] = [];
for (let i = 0; i < TAB_COUNT; i++) {
tabs.push(new NestedScrollTabConfig(i, TAB_NAMES[i], TAB_THEME_COLORS[i], this.buildMockItems(i)));
}
return tabs;
}
private buildMockItems(tabIndex: number): NestedScrollListItem[] {
const items: NestedScrollListItem[] = [];
for (let i = 0; i < ITEM_COUNT_PER_TAB; i++) {
const id = tabIndex * ITEM_COUNT_PER_TAB + i + 1;
items.push(new NestedScrollListItem(
id,
`${TAB_NAMES[tabIndex]} - 第${i + 1}条`,
`这是 ${TAB_NAMES[tabIndex]} Tab 下的第 ${i + 1} 条 mock 数据`,
ITEM_COLORS[i % ITEM_COLORS.length]
));
}
return items;
}
}
model实体类
/**
* 嵌套滑动 Demo 的 Tab 配置数据模型
*/
/** 列表项数据 */
export class NestedScrollListItem {
id: number = 0;
title: string = '';
subtitle: string = '';
color: string = '#FFFFFF';
constructor(id: number, title: string, subtitle: string, color: string) {
this.id = id;
this.title = title;
this.subtitle = subtitle;
this.color = color;
}
}
/** Tab 配置 */
export class NestedScrollTabConfig {
index: number = 0;
name: string = '';
themeColor: string = '';
listData: NestedScrollListItem[] = [];
constructor(index: number, name: string, themeColor: string, listData: NestedScrollListItem[]) {
this.index = index;
this.name = name;
this.themeColor = themeColor;
this.listData = listData;
}
}
viewmodel 交互抽象层,事件响应
/**
* 嵌套滑动 Demo ViewModel 接口
*/
export interface INestedScrollViewModel {
/** 页面就绪:拉取 Tab 数据 */
pageOnReady(): void;
/** 点击 Tab;返回是否真的发生了切换 */
onTabClick(tabIndex: number): boolean;
/** Swiper 滑动切换 Tab */
onSwiperTabChange(index: number): void;
/** 第一个 Tab 继续向右滑(退出页面) */
onFirstTabSwipeOut(): void;
}
/**
* View 回调能力(由 Page 在 aboutToAppear 时注入)
*/
export interface NestedScrollViewModelCallback {
popPage?: () => void;
}
viewmodel 交互实现层。处理具体事件,数据状态管理
import { NestedScrollModelImpl } from '../model/NestedScrollModelImpl';
import { NestedScrollTabConfig } from '../model/NestedScrollTabConfig';
import { INestedScrollViewModel, NestedScrollViewModelCallback } from './INestedScrollViewModel';
const VM_TAG = 'NestedScrollVM';
/**
* 嵌套滑动 Demo ViewModel 实现
* 通过 @ObservedV2 + @Trace 让 View 层感知状态变化
*/
@ObservedV2
export class NestedScrollViewModelImpl implements INestedScrollViewModel {
private nestedScrollModel: NestedScrollModelImpl;
private callback?: NestedScrollViewModelCallback;
@Trace tabs: NestedScrollTabConfig[] = [];
@Trace currentTabIndex: number = 0;
@Trace pageLoading: boolean = false;
constructor() {
this.nestedScrollModel = new NestedScrollModelImpl();
}
aboutToAppear(): void {
}
aboutToDisappear(): void {
}
setCallback(callback: NestedScrollViewModelCallback | undefined): void {
this.callback = callback;
}
pageOnReady(): void {
this.showPageLoading();
this.queryTabs();
}
private queryTabs(): void {
this.nestedScrollModel.queryTabs()
.then((tabs: NestedScrollTabConfig[]) => {
this.tabs = tabs;
this.hidePageLoading();
})
.catch((e: Error) => {
console.error(`${VM_TAG} queryTabs error: ${e?.message ?? e}`);
this.hidePageLoading();
//TODO::异常处理
});
}
onTabClick(tabIndex: number): boolean {
if (this.currentTabIndex === tabIndex) {
return false;
}
this.currentTabIndex = tabIndex;
return true;
}
onSwiperTabChange(index: number): void {
if (this.currentTabIndex !== index) {
this.currentTabIndex = index;
}
}
onFirstTabSwipeOut(): void {
this.callback?.popPage?.();
}
private showPageLoading(): void {
this.pageLoading = true;
}
private hidePageLoading(): void {
this.pageLoading = false;
}
}
view page层。view组件组装/展示、数据渲染
/**
* View 层:嵌套滑动 Demo 页
*/
import { NestedScrollTabContainer } from '../components/NestedScrollTabContainer';
import { NestedScrollViewModelImpl } from '../viewmodel/NestedScrollViewModelImpl';
import { NestedScrollViewModelCallback } from '../viewmodel/INestedScrollViewModel';
import { NestedScrollTabConfig, NestedScrollListItem } from '../model/NestedScrollTabConfig';
@Builder
export function NestedScrollDemoPageBuilder(name: string, param: Object) {
NestedScrollDemoPage()
}
@Entry
@Component
struct NestedScrollDemoPage {
private viewModel: NestedScrollViewModelImpl = new NestedScrollViewModelImpl();
private parentScroller: Scroller = new Scroller();
private swiperController: SwiperController = new SwiperController();
private childScrollers: Scroller[] = [
new Scroller(), new Scroller(), new Scroller(), new Scroller()
];
private pathStack: NavPathStack = new NavPathStack();
private nestedScrollViewModelCallback: NestedScrollViewModelCallback = {
popPage: (): void => {
this.pathStack.pop();
}
};
build() {
NavDestination() {
Stack() {
NestedScrollTabContainer({
currentTabIndex: this.viewModel.currentTabIndex,
useLinearGradient: true,
gradientAngle: 180,
gradientColors: [
['#33B62E', 0.0],
['#00B365', 1.0]
],
swiperController: this.swiperController,
parentScroller: this.parentScroller,
onTabChange: (index: number) => {
this.viewModel.onSwiperTabChange(index);
},
onFirstTabSwipeOut: () => {
this.viewModel.onFirstTabSwipeOut();
},
headerBuilder: () => {
this.headerBuilder()
},
contentBuilder: () => {
this.swiperContentBuilder()
}
})
}
}
.width('100%')
.height('100%')
.backgroundColor('#00B365')
.hideTitleBar(true)
.hideBackButton(true)
.hideToolBar(true)
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack;
this.viewModel.pageOnReady();
})
}
aboutToAppear() {
this.viewModel.setCallback(this.nestedScrollViewModelCallback);
this.viewModel.aboutToAppear();
}
aboutToDisappear() {
this.viewModel.setCallback(undefined);
this.viewModel.aboutToDisappear();
}
@Builder
headerBuilder() {
Column() {
Column() {
Text('嵌套滑动 Demo')
.fontSize(24)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.margin({ top: 48, bottom: 8 })
Text('下滑内层列表到顶后,外层 Header 才会下移出现')
.fontSize(12)
.fontColor('#E0FFFFFF')
.margin({ bottom: 16 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
Row() {
ForEach(this.viewModel.tabs, (tab: NestedScrollTabConfig) => {
Text(tab.name)
.fontSize(this.viewModel.currentTabIndex === tab.index ? 16 : 14)
.fontColor(this.viewModel.currentTabIndex === tab.index ? '#FFFFFF' : '#B3FFFFFF')
.fontWeight(this.viewModel.currentTabIndex === tab.index ? FontWeight.Bold : FontWeight.Normal)
.padding({
left: 12,
right: 12,
top: 8,
bottom: 8
})
.borderRadius(16)
.backgroundColor(this.viewModel.currentTabIndex === tab.index ? '#33FFFFFF' : Color.Transparent)
.onClick(() => {
const changed = this.viewModel.onTabClick(tab.index);
if (changed) {
this.swiperController.changeIndex(tab.index);
}
})
})
}
.width('100%')
.height(48)
.justifyContent(FlexAlign.Center)
.margin({ bottom: 8 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
@Builder
swiperContentBuilder() {
ForEach(this.viewModel.tabs, (tab: NestedScrollTabConfig) => {
this.tabContentBuilder(tab)
})
}
@Builder
tabContentBuilder(tab: NestedScrollTabConfig) {
List({ scroller: this.childScrollers[tab.index], space: 8 }) {
ForEach(tab.listData, (item: NestedScrollListItem) => {
ListItem() {
Column() {
Text(item.title)
.fontSize(16)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
Text(item.subtitle)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
Text(`#${item.id}`)
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
}
.width('100%')
.padding(16)
.backgroundColor(item.color)
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
}
.padding({ left: 12, right: 12 })
})
ListItem() {
Text('— 已经到底啦 —')
.width('100%')
.textAlign(TextAlign.Center)
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ top: 16, bottom: 32 })
}
}
.width('100%')
.height('100%')
.listDirection(Axis.Vertical)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.None)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
}
}
撒花撒花~
后续优化
这个是基础组件,如果项目使用,还需要根据项目进行拓展。比如:
1.增加footer,footer需要放在list里面。
2.懒加载+分页加载,懒加载是为了不让大列表卡顿,懒加载是lazyforeach,分页加载是因为服务端数据太大一次性下发不完
3.错误页/空白页处理,这个放在tabContentBuilder里面比较好。一般进页面是查第一个tab数据,如果失败该tab展示异常或者空白,切到其他tab的时候才懒加载其他tab,其他tab仍然可以继续请求数据正常展示,然后切回原来异常tab的时候继续请求数据,直到数据请求成功
4.tab动态化。这个根据业务规则来,有的强诉求tab动态化,即端上代码不消费具体的tab类型,只做容器和兼容处理。
更多推荐



所有评论(0)