鸿蒙开发Tips

482 阅读4分钟

声明式UI

  • 声明式描述
  • 状态驱动视图更新

状态驱动视图更新

应用界面是动态的,需要根据状态不同显示不同内容

  • 状态:数据源
  • 视图:与状态相关联的UI内容
//定义状态变量
@State isComplete: boolean = false  
...
//建立状态与视图间的关系
Row(){
    if (this.isComplete) {
        Image($r('app.media.ic_ok'))
        ...
    } else {
        Image($r('app.media.ic_default'))
        ...
    }
    Text('学习ArkTS')
    ...
}
.onClick(() => {
    //改变状态变量
    this.isComplete = !this.isComplete
})

页面和组件的生命周期

// Index.ets
import router from '@ohos.router';

@Entry
@Component
struct MyComponent {
  @State showChild: boolean = true;
  @State btnColor:string = "#FF007DFF"

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow() {
    console.info('Index onPageShow');
  }
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('Index onPageHide');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('Index onBackPress');
    this.btnColor ="#FFEE0606"
    return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('MyComponent aboutToAppear');
  }

  // 组件生命周期
  aboutToDisappear() {
    console.info('MyComponent aboutToDisappear');
  }

  build() {
    Column() {
      // this.showChild为true,创建Child子组件,执行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
      Button('delete Child')
      .margin(20)
      .backgroundColor(this.btnColor)
      .onClick(() => {
        this.showChild = false;
      })
      // push到page页面,执行onPageHide
      Button('push to next page')
        .onClick(() => {
          router.pushUrl({ url: 'pages/page' });
        })
    }

  }
}

@Component
struct Child {
  @State title: string = 'Hello World';
  // 组件生命周期
  aboutToDisappear() {
    console.info('[lifeCycle] Child aboutToDisappear')
  }
  // 组件生命周期
  aboutToAppear() {
    console.info('[lifeCycle] Child aboutToAppear')
  }

  build() {
    Text(this.title).fontSize(50).margin(20).onClick(() => {
      this.title = 'Hello ArkUI';
    })
  }
}
// page.ets
@Entry
@Component
struct page {
  @State textColor: Color = Color.Black;
  @State num: number = 0

  onPageShow() {
    this.num = 5
  }

  onPageHide() {
    console.log("page onPageHide");
  }

  onBackPress() { // 不设置返回值按照false处理
    this.textColor = Color.Grey
    this.num = 0
  }

  aboutToAppear() {
    this.textColor = Color.Blue
  }

  build() {
    Column() {
      Text(`num 的值为:${this.num}`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.textColor)
        .margin(20)
        .onClick(() => {
          this.num += 5
        })
    }
    .width('100%')
  }
}

以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以使页面级别的生命周期方法生效,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时声明了组件的生命周期函数。

  • 应用冷启动的初始化流程为:
    MyComponent aboutToAppear --> MyComponent build -->
    Child aboutToAppear --> Child build --> Child build执行完毕 -->
    MyComponent build执行完毕 -->
    Index onPageShow。

  • 如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。

  • 点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。

  • 最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。

  • 退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。

Builder装饰器

按引用传递参数

传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。

如果在@Builder方法内调用自定义组件,ArkUI提供$$作为按引用传递参数的范式。

class Tmp {
  paramA1: string = ''
}

@Builder function overBuilder($$: Tmp) {
  Row() {
    Column() {
      Text(`overBuilder===${$$.paramA1}`)
      HelloComponent({message: $$.paramA1})
    }
  }
}

@Component
struct HelloComponent {
  @Link message: string;

  build() {
    Row() {
      Text(`HelloComponent===${this.message}`)
    }
  }
}

@Entry
@Component
struct Parent {
  @State label: string = 'Hello';
  build() {
    Column() {
      // Pass the this.label reference to the overBuilder component when the overBuilder component is called in the Parent component.
      overBuilder({paramA1: this.label})
      Button('Click me').onClick(() => {
        // After Click me is clicked, the UI text changes from Hello to ArkUI.
        this.label = 'ArkUI';
      })
    }
  }
}

按值传递参数

调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。

@Builder function overBuilder(paramA1: string) {
  Row() {
    Text(`UseStateVarByValue: ${paramA1} `)
  }
}
@Entry
@Component
struct Parent {
  @State label: string = 'Hello';
  build() {
    Column() {
      overBuilder(this.label)
    }
  }
}

ForEach循环渲染列表

ForEach(this.totalTasks, (item: string) => {
    ToDoItem({ content: item, haha : "hahahaha" })
}, (item: string) => JSON.stringify(item))

UIAbility

官方 应用程序入口—UIAbility的使用

  • 是系统调度的单元,提供窗口用于界面绘制
  • 包含用户界面的应用组件,用于和用户进行交互
  • 监听前后台切换、销毁

我理解鸿蒙中开发页面类似于Android中的单Activity多Fragment架构。UIAbility像Activity,module.json5中配置UIAbility的launchType相当于Android的task Affinity。

启动模式:

影响的是‘最近任务列表’展示样式

  • singleton(单实例模式)
  • multiton(多实例模式)
  • specified(指定实例模式)

应用的UIAbility实例已创建,该UIAbility配置为单实例模式,再次调用startAbility()方法启动该UIAbility实例。由于启动的还是原来的UIAbility实例,并未重新创建一个新的UIAbility实例,此时只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。

这不就是对应的Android的SingleTask启动模式吗

UIAbility生命周期:

  • Create
  • Foreground
  • Background
  • Destroy

WindowStage生命周期:

  • WindowStageCreate
  • WindowStageDestroy

页面跳转和参数接收

方式一:router.pushUrl

router.pushUrl({
  url: 'pages/Second',
  params: {
    src: 'Index页面传来的数据',
  }
}, router.RouterMode.Single)
  • router.RouterMode.Single 单实例模式

    如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近同url页面会被移动到栈顶,移动后的页面为新建页

  • router.RouterMode.Standard 多实例模式

    如果目标页面的url在页面栈中不存在同url页面,按多实例模式跳转,页面栈的元素数量会加1

方式二:router.replaceUrl

router.replaceUrl({
  url: 'pages/Second',
  params: {
    src: 'Index页面传来的数据',
  }
}, router.RouterMode.Single)
  • 单实例模式下:如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近同url页面会被移动到栈顶,替换当前页面,并销毁被替换的当前页面,移动后的页面为新建页,页面栈的元素数量会减1
  • 如果目标页面的url在页面栈中不存在同url页面,按多实例模式跳转,页面栈的元素数量不变。

注:哈?这不就是类似Android的Fragment replace吗?互相替换

接收参数

import router from '@ohos.router';

@Entry
@Component
struct Second {
  @State name: string = router.getParams()?.['name'] ?? "待办"
  // 页面刷新展示
  // ...
}

页面返回

返回上一个页面

router.back();

返回到指定页面

router.back({ url: 'pages/Index' });

在Second页面中,调用router.back()方法返回上一个页面或者返回指定页面时,根据需要继续增加自定义参数,例如在返回时增加一个自定义参数src。 在Index页面通过调用router.getParams()方法,获取Second页面传递过来的自定义参数

router.back({
  url: 'pages/Index',
  params: {
    src: 'Second页面传来的数据',
  }
})

使用资源引用类型

可以将硬编码写到entry/src/main/resources下的资源文件

string.json中定义Button显示的文本

{
  "string": [
    {
      "name": "login_text",
      "value": "登录"
    }
  ]
} 

在float.json中定义Button的宽高和字体大小

{
  "float": [
    {
      "name": "button_width",
      "value": "300vp"
    },
    {
      "name": "button_height",
      "value": "40vp"
    },
    {
      "name": "login_fontSize",
      "value": "18fp"
    }
  ]
}

在color.json中定义Button的背景颜色

{
  "color": [
    {
      "name": "button_color",
      "value": "#1890ff"
    }
  ]
}

然后在Button组件通过“$r('app.type.name')”的形式引用应用资源。app代表应用内resources目录中定义的资源;type代表资源类型(或资源的存放位置),可以取“color”、“float”、“string”、“plural”、“media”;name代表资源命名,由开发者定义资源时确定。

Button('登录', { type: ButtonType.Capsule, stateEffect: true })
  .width(300)
  .height(40)
  .fontSize(16)
  .fontWeight(FontWeight.Medium)
  .backgroundColor('#007DFF')
  
修改为
  
Button($r('app.string.login_text'), { type: ButtonType.Capsule })
  .width($r('app.float.button_width'))
  .height($r('app.float.button_height'))
  .fontSize($r('app.float.login_fontSize'))
  .backgroundColor($r('app.color.button_color'))

容器组件:Column & Row

Column&Row组件的使用 官方 Column 官方 Row

  • justifyContent:设置子组件在主轴方向上的对齐格式
  • alignItems:设置子组件在交叉轴方向上的对齐格式

容器组件:List & Grid

官方 List组件和Grid组件的使用 官方 List API参考 官方 性能提升的推荐方法

List:

@Entry
@Component
struct ListDemo {
  private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  build() {
    Column() {
      List({ space: 10 }) {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text(`${item}`)
              .width('100%')
              .height(100)
              .fontSize(20)
              .fontColor(Color.White)
              .textAlign(TextAlign.Center)
              .borderRadius(10)
              .backgroundColor(0x007DFF)
          }
        }, item => item)
      }
    }
    .padding(12)
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

这种方式会导致所有item都加载

可以采用懒加载的方式:

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = []

  public totalCount(): number {
    return 0
  }

  public getData(index: number): any {
    return undefined
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener')
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener')
      this.listeners.splice(pos, 1)
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded()
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to)
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: Array<string> = new Array(100).fill('test')

  public totalCount(): number {
    return this.dataArray.length
  }

  public getData(index: number): any {
    return this.dataArray[index]
  }

  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data)
    this.notifyDataAdd(index)
  }

  public pushData(data: string): void {
    this.dataArray.push(data)
    this.notifyDataAdd(this.dataArray.length - 1)
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource()

  build() {
    Scroll() {
      List() {
        LazyForEach(this.data, (item: string, index: number) => {
          ListItem() {
            Text('item value: ' + item + (index + 1)).fontSize(20).margin(10)
          }.width('100%')
        })
      }.width('100%').height(500)
    }.backgroundColor(Color.Pink)
  }
}

Gird:

@Entry
@Component
struct GridExample {
  // 定义一个长度为16的数组
  private arr: string[] = new Array(16).fill('').map((_, index) => `item ${index}`);

  build() {
    Column() {
      Grid() {
        ForEach(this.arr, (item: string) => {
          GridItem() {
            Text(item)
              .fontSize(16)
              .fontColor(Color.White)
              .backgroundColor(0x007DFF)
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
        }, item => item)
      }
      .columnsTemplate('1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .height(300)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(0xF1F3F5)
  }
}

示例代码中创建了16个GridItem列表项,但是不可滚动。

columnsTemplate的值为'1fr 1fr 1fr 1fr',表示这个网格为4列,将Grid允许的宽分为4等分,每列占1份;
rowsTemplate的值为'1fr 1fr 1fr 1fr',表示这个网格为4行,将Grid允许的高分为4等分,每行占1份。

将示例代码中GridItem的高度设置为固定值,例如100;仅设置columnsTemplate属性,不设置rowsTemplate属性,就可以实现Grid列表的滚动:

容器组件:Tabs

官方 Tabs组件的使用

自定义TabBar样式(底部tab效果):

@Entry
@Component
struct TabsExample {
  @State currentIndex: number = 0;
  private tabsController: TabsController = new TabsController();

  @Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
        .size({ width: 25, height: 25 })
      Text(title)
        .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
    }
    .width('100%')
    .height(50)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.currentIndex = targetIndex;
      this.tabsController.changeIndex(this.currentIndex);
    })
  }

  build() {
    Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
      TabContent() {
        Column().width('100%').height('100%').backgroundColor('#00CB87')
      }
      .tabBar(this.TabBuilder('首页', 0, $r('app.media.home_selected'), $r('app.media.home_normal')))

      TabContent() {
        Column().width('100%').height('100%').backgroundColor('#007DFF')
      }
      .tabBar(this.TabBuilder('我的', 1, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
    }
    .barWidth('100%')
    .barHeight(50)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
  }
}

装饰器:@State、@Prop、@Link、@Provide & @Consume

  • @State:当前组件中使用
  • @Prop:是父传子单向同步,当父改变子能监听到并刷新UI,但是子改变了但是父不会感知。写法上,父组件把@State变量传给子组件,子组件对接收的这个变量要用@Prop修饰
  • @Link:是父子双向同步,各自的修改对方都能监听到。写法上,父组件把@State变量传给子组件,子组件对接收的这个变量要用@Link修饰
  • @Provide & @Consume:可以跨组件的双向同步,不局限于父子关系。写法上,当前组件中的变量用@Provide修饰,另一个组件对接收的这个变量要用@Consume修饰

以上涉及跨组件的,需要传递的,都需要变量名相同。

监听值改变的回调:@Watch

@Link @Watch('hahahaha') targetData: Array<TaskItemBean>;
hahahaha() {
    hilog.error(0, "zktzkt", "hahahaha");
}

一旦targetData变化了,hahahaha()函数就会被回调

显式动画 animateTo

官方API 属性动画

通用属性变化时,可以通过属性动画实现渐变过渡效果。
支持的属性包括:width、height、backgroundColor、opacity、scale、rotate、translate等。

只要该属性是在animateTo的闭包函数中修改的,那么由其引起的所有变化都会按照animateTo的动画参数执行动画过渡到终点值

animateTo({ duration: CommonConstants.DURATION }, () => {
    this.isExpanded = !this.isExpanded;
})

属性动画 animation

想要组件随某个属性值的变化而产生动画,此属性需要加在animation属性之前。

有的属性变化不希望通过animation产生属性动画,可以放在animation之后。

产生属性动画的属性本身需满足一定的要求,并非任何属性都可以产生属性动画。目前支持的属性包括width、height、position、opacity、backgroundColor、scale、rotate、translate等

@Entry
@Component
struct LayoutChange2 {
  @State myWidth: number = 100;
  @State myHeight: number = 50;
  @State flag: boolean = false;
  @State myColor: Color = Color.Blue;

  build() {
    Column({ space: 10 }) {
      Button("text")
      .type(ButtonType.Normal)
      .width(this.myWidth)
      .height(this.myHeight)
      // animation只对其上面的type、width、height属性生效,时长为1000ms,曲线为Ease
      .animation({ duration: 1000, curve: Curve.Ease })
      // animation对下面的backgroundColor、margin属性不生效
      .backgroundColor(this.myColor)
      .margin(20)
            Button("area: click me")
              .fontSize(12)
              .onClick(() => {
                // 改变属性值,配置了属性动画的属性会进行动画过渡
                if (this.flag) {
                  this.myWidth = 100;
                  this.myHeight = 50;
                  this.myColor = Color.Blue;
                } else {
                  this.myWidth = 200;
                  this.myHeight = 100;
                  this.myColor = Color.Pink;
                }
                this.flag = !this.flag;
              })
          }

    }
}

安装三方库

执行命令

ohpm i @ohos/axios

成功后会在oh-package.json5中的dependencies节点下会自动生成"@ohos/axios": "^2.2.0"