HarmonyOS鸿蒙Next中tabbar切换每次都触发组件的生命周期

HarmonyOS鸿蒙Next中tabbar切换每次都触发组件的生命周期 代码如下:

import { MessageList } from './MessageList'
import { BillList } from './BillList'
import { HMRouter } from '@hadss/hmrouter';
import { common } from '@kit.AbilityKit'

// Tab
interface Tab {
  title: string;
  icon: Resource;
  selectedIcon: Resource;
}


@HMRouter({ pageUrl: 'Home' })
@ComponentV2
export struct Home {
  @Local currentTabIndex: number = 0
  private tabsController: TabsController = new TabsController()
  private tabs: Array<Tab> = [
    { title: "账单", icon: $r('app.media.bill'), selectedIcon: $r('app.media.bill_selected') },
    { title: "消息", icon: $r('app.media.message'), selectedIcon: $r('app.media.message_selected') },
  ]

  @Builder
  TabItemBuilder(title: string, index: number, normalImg: Resource, selectedImg: Resource) {
    Column() {
      Image(this.currentTabIndex === index ? selectedImg : normalImg)
        .size({ width: 28, height: 28 })
      Text(title)
        .fontColor(this.currentTabIndex === index ? '#007AFF' : '#6B6B6B')
    }
    .onClick(() => {
      this.currentTabIndex = index
      this.tabsController.changeIndex(index)
    })
  }

  build() {
    Flex({ direction: FlexDirection.Column }) {
      // 内容区域
      Column() {
        if (this.currentTabIndex === 0) {
          BillList()
        } else if (this.currentTabIndex === 1) {
          MessageList()
        }
      }
      .flexGrow(1)

      // 底部导航
      Tabs({ controller: this.tabsController, barPosition: BarPosition.End, index: this.currentTabIndex }) {
        ForEach(this.tabs, (item: Tab, index) => {
          TabContent() {
          }
          .tabBar(this.TabItemBuilder(
            item.title,
            index,
            item.icon,
            item.selectedIcon
          ))
        })
      }
      .width('100%')
      .height(60)
      .backgroundColor($r('app.color.tab_bg_color'))
    }
  }
}

每次切换tab都会触发组件的aboutToAppear生命周期。

为什么不放在tabcontent?因为多个tab会导致中间所有的tab都被激活一次


更多关于HarmonyOS鸿蒙Next中tabbar切换每次都触发组件的生命周期的实战教程也可以访问 https://www.itying.com/category-93-b0.html

8 回复

更多关于HarmonyOS鸿蒙Next中tabbar切换每次都触发组件的生命周期的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这个文档里面的代码是用的TabContent,但是TabContent好像是swiper,切换的时候会出现滑动,能看到其他tab的内容一闪而过,

不用循环去做,你直接平铺TabContent就不会一闪而过了,

【问题分析】

关于Tabs对于组件的缓存,楼主可以参考一下这个文档说明,如果子组件是自定义组件的话无法使用预加载

cke_2198.png

【参考文档】

Tabs-导航与切换-ArkTS组件-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者

为什么不放在tabcontent?因为多个tab会导致中间所有的tab都被激活一次

正常情况下不会出现中间每个tab都被激活一次的。

建议你不要使用foreach循环初始化tab。

直接按照最简单的方案,多写几个TabContent就行了。

而且swiper是可以控制的,不让它滑动就行了。

开发者你好,参考一楼回复使用预加载,方案参考如下:

【解决方案】

方案一:调用Tabs组件的preloadItems接口后会一次性加载所有指定的子节点,为了性能考虑,建议分批加载子节点。具体代码示例可参考官网:示例11预加载子节点

方案二:Swiper组件能够支持预加载功能。可以利用Swiper组件来构建自定义的Tabs,并借此实现预加载功能,请参阅以下示例代码:

@Entry
@ComponentV2
struct TabsPreLoadDemo {
  @Local tabNames: string[] = ['飞机', '铁路', '自驾', '地铁', '公交', '骑行']
  @Local selectedTabIndex: number = 0
  @Local indicatorLeftOffset: number = 0
  @Local indicatorOffset: number = 0
  @Local firstWidth: number = -1
  @Local otherWidth: number = -1
  @Local swiperController: SwiperController = new SwiperController()
  @Local swiperWidth: number = 0

  build() {
    RelativeContainer() {
      Stack() {
        Rect()
          .height(30)
          .stroke(Color.Black)
          .radius(10)
          .width(this.firstWidth)
          .fill('#bff9f2')
          .position({
            left: this.indicatorLeftOffset + this.indicatorOffset,
            bottom: 0
          })
          .animation({ duration: 300, curve: Curve.LinearOutSlowIn })
      }
      .width('100%')
      .alignRules({
        center: { anchor: 'Tabs', align: VerticalAlign.Center }
      })

      Row() {
        ForEach(this.tabNames, (name: string, index: number) => {
          Row() {
            Text(name)
              .fontSize(16)
              .fontWeight(this.selectedTabIndex === index ? FontWeight.Bold : FontWeight.Normal)
              .textAlign(TextAlign.Center)
              .animation({ duration: 300 })
            Image($r('app.media.startIcon'))
              .width(14)
              .height(14)
              .margin({ left: 2 })
              .visibility(this.selectedTabIndex === index ? Visibility.Visible : Visibility.None)
              .animation({ duration: 300 })
          }
          .justifyContent(FlexAlign.Center)
          .layoutWeight(this.selectedTabIndex === index ? 1.5 : 1)
          .animation({ duration: 300 })
          .onClick(() => {
            this.selectedTabIndex = index;
            this.swiperController.changeIndex(index, false);
            this.getUIContext().animateTo({ duration: 500, curve: Curve.LinearOutSlowIn }, () => {
              this.indicatorLeftOffset = this.otherWidth * index;
            })
          })
        })
      }
      .width('100%')
      .height(30)
      .id('Tabs')
      .onAreaChange((oldValue: Area, newValue: Area) => {
        let tabWidth = newValue.width.valueOf() as number;
        this.firstWidth = 1.5 * tabWidth / (this.tabNames.length + 0.5);
        this.otherWidth = tabWidth / (this.tabNames.length + 0.5);
      })

      Swiper(this.swiperController) {
        ForEach(this.tabNames, (name: string, index: number) => {
          Column() {
            Text(`${name} - ${index}`)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
          }
          .alignItems(HorizontalAlign.Center)
          .justifyContent(FlexAlign.Center)
          .height('100%')
          .width('100%')
        })
      }
      .cachedCount(3)
      .onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {
        if (targetIndex > index) {
          this.indicatorLeftOffset += this.otherWidth;
        } else if (targetIndex < index) {
          this.indicatorLeftOffset -= this.otherWidth;
        }
        this.indicatorOffset = 0
        this.selectedTabIndex = targetIndex
      })
      .onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {
        this.indicatorOffset = 0
      })
      .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
        let move: number = this.GetOffset(extraInfo.currentOffset);
        if ((this.selectedTabIndex === 0 && extraInfo.currentOffset > 0) ||
          (this.selectedTabIndex === this.tabNames.length - 1 && extraInfo.currentOffset < 0)) {
          return;
        }
        this.indicatorOffset = extraInfo.currentOffset < 0 ? move : -move;
      })
      .onAreaChange((oldValue: Area, newValue: Area) => {
        let width = newValue.width.valueOf() as number;
        this.swiperWidth = width;
      })
      .curve(Curve.LinearOutSlowIn)
      .loop(false)
      .indicator(false)
      .width('100%')
      .id('MainContext')
      .alignRules({
        top: { anchor: 'Tabs', align: VerticalAlign.Bottom },
        bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
      })
    }
    .height('100%')
    .width('100%')
    .padding(10)
  }

  GetOffset(swiperOffset: number): number {
    let swiperMoveRatio: number = Math.abs(swiperOffset / this.swiperWidth);
    let tabMoveValue: number = swiperMoveRatio >= 1 ? this.otherWidth : this.otherWidth * swiperMoveRatio;
    return tabMoveValue;
  }
}

两种方案对比:

方案 优缺点 适用场景
方案一:preloadItems 可以分批次加载,使用方法简单。 在常规场景下,推荐使用该方法。
方案二:Swiper 无法分批预加载,且需要自定义使用方法相对复杂。 自定义Tabs,使用Swiper组件特性的场景。

针对你说的其他tab的内容一闪而过问题:

Tabs组件采用了默认的Tabs页面切换动画,会导致动画过程中闪过其他Tabs页面:

  • Tabs组件的animationMode属性的值不是AnimationMode.NO_ANIMATION,没有关闭Tabs页面切换动画。
  • Tabs组件的animationDuration属性的值不为0,没有关闭Tabs页面切换动画。
  • Tabs组件没有使用自定义Tabs页面切换动画,Tabs页面切换时中间会闪过其他Tabs页面。

【修改建议】

根据需要采用以下方案:关闭Tabs页面切换动画或者自定义Tabs页面切换动画。

  • 配置animationMode属性值为AnimationMode.NO_ANIMATION,关闭Tabs页面切换动画。
Tabs(){
  // ...
}
.animationMode(AnimationMode.NO_ANIMATION)
  • 配置animationDuration属性值为0,关闭Tabs页面切换动画。
Tabs(){
  // ...
}
.animationDuration(0)

在HarmonyOS Next中,TabBar切换时会触发组件生命周期。每次切换标签页,对应页面的aboutToAppear和aboutToDisappear方法会被调用,实现页面状态管理。系统通过ArkUI的声明式开发范式自动处理组件挂载与卸载,确保页面资源正确初始化和释放。若需保持页面状态,可使用@State装饰器或LazyForEach优化加载性能。

在HarmonyOS Next中,每次切换tab都会触发组件生命周期是因为您使用了条件渲染(if/else)来切换内容。当currentTabIndex变化时,ArkUI框架会重新构建整个build函数,导致未显示的组件被销毁,再次显示时重新创建。

建议使用Tabs组件的TabContent来管理内容切换:

Tabs({ controller: this.tabsController, barPosition: BarPosition.End, index: this.currentTabIndex }) {
  TabContent() {
    BillList()
  }
  .tabBar(this.TabItemBuilder('账单', 0, $r('app.media.bill'), $r('app.media.bill_selected')))
  
  TabContent() {
    MessageList()
  }
  .tabBar(this.TabItemBuilder('消息', 1, $r('app.media.message'), $r('app.media.message_selected')))
}

这样Tabs组件会管理各个TabContent的显示状态,只有当前激活的tab才会触发生命周期,切换时不会重复触发aboutToAppear。同时移除build函数中的条件渲染逻辑,让Tabs完全控制内容切换。

回到顶部