鸿蒙HarmonyOS开发指南:UI动态操作基础用法规范

193 篇文章 0 订阅
189 篇文章 0 订阅

简介

本文详细介绍了UI动态操作的基本用法,着重介绍了BuilderNode自定义节点和NodeController生命周期等内容。结合这些基础用法,提供了三方广告的场景案例作为参考。案例展示了如何使用UI动态操作实现三方广告,其最后的实现效果图如下所示。

图1 实现效果图

UI动态操作包含组件动态创建、卸载等相关操作。在声明式范式中,组件仅在build环节中被创建,开发者无法在其他生命周期阶段进行组件的创建,从而引起页面加载慢等问题。与声明式范式不同,ArkUI框架提供的UI动态操作支持组件的预创建。组件预创建可以满足开发者在非build生命周期中进行组件创建,创建后的组件可以进行属性设置、布局计算等操作。之后在页面加载时进行使用,可以极大提升页面响应速度。如下图所示,利用组件预创建机制,可以利用动画执行过程空闲时间进行组件创建,在动画结束后,再进行更新。

图2 组件预创建原理图

UI动态操作基础用法

BuiderNode自定义节点基础用法

自定义节点BuilderNode能够挂载原生组件,下面详细介绍下BuilderNode创建、显示、更新、移除和替换。

  • 创建自定义节点。

首先,准备好需要挂载的节点,代码如下所示。

// src\main\ets\pages\Index.ets
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';

class Params {
  text: string = 'Hello World';
  constructor(text: string) {
    this.text = text;
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({bottom: 36})
  }
}
......

其次,使用构造函数创建BuilderNode实例。创建的时候需要声明BuilderNode中所使用的WrappedBuilder的类型。创建BuilderNode对象的时候必须要传入对应的UIContext对象。若BuilderNode作为RenderNode的子节点存在,要求设置RenderOptions的selfIdealSize属性。

然后,使用BuilderNode的build方法,构建组件树。方法build()需要传入两个参数,第一个参数为通过wrapBuilder()封装的全局@Builder方法。第二个参数为对应的@Builder方法所需的参数对象。若@Builder方法不带参数或者存在默认参数,则build()的第二个参数可以不设置。

// src\main\ets\pages\Index.ets
......
class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = '';

  constructor(message: string) {
    super();
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    // 创建BuilderNode实例
    this.textNode = new BuilderNode(context);
    // 设置selfIdealSize属性
    // this.textNode = new BuilderNode(context, {selfIdealSize: {width: 100, height :100}});
    // 使用build方法构建组件树
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    // 返回需要显示的节点
    return this.textNode.getFrameNode();
  }
}
......
  • 显示自定义节点

显示自定义节点依赖声明式渲染容器NodeContainer以及对应的控制类NodeController。

首先,重写NodeController中的makeNode方法。MakeNode方法中返回的节点会显示在对应的NodeContainer中。由于makeNode需要返回的为一个FrameNode,因此如果预期显示BuidlerNode的时候需要调用对应的BuidlerNode的getFrameNode的方法,获取其根节点,详细代码如上TextNodeController中所示。

然后,在页面内新增声明式渲染容器NodeContainer,创建工具类NodeController。通过NodeController将MakeNode中返回的节点在声明式渲染容器中进行显示。

// src\main\ets\pages\Index.ets
......
@Entry
@Component
struct Index {
  @State message: string = "hello";
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  
  build() {
    Row() {
      Column() {
        NodeContainer(this.textNodeController)
          .width('100%')
          .height(100)
          .backgroundColor('#FFF0F0F0')
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}
  • 更新自定义节点

更新自定义节点,可参考BuilderNode的update方法。

  • 移除自定义节点

通过条件控制语句可以将NodeContainer节点进行移除或者显示。如示例代码,将this.isShow更改为false则将节点从界面上移除。

// src\main\ets\pages\Index.ets
......
@Entry
@Component
struct Index {
  @State message: string = "hello";
  @State isShow: boolean = true;
  private textNodeController: TextNodeController = new TextNodeController(this.message);

  build() {
    Row() {
      Column() {
        if (this.isShow) {
          NodeContainer(this.textNodeController)
            .width('100%')
            .height(100)
            .backgroundColor('#FFF0F0F0')
        }
        Button('isShow')
          .onClick(() => {
            this.isShow = false;
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}
  • 替换自定义节点

动态将NodeContainer上的节点替换,依赖于NodeController的makeNode和rebuild方法。rebuild方法会触发makeNode的回调,刷新NodeContainer上显示的节点;makeNode方法返回的为null,则移除NodeContainer下挂载的节点。

......
class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = '';

  constructor(message: string) {
    super();
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    // 加上判空处理,只有第一次创建BuilderNode时,才会执行下列代码;替换节点时,textNode不为null
    if (this.textNode == null) {
      this.textNode = new BuilderNode(context);
      this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    }

    return this.textNode.getFrameNode();
  }

  replaceBuilderNode(newNode: BuilderNode<Object[]>) {
    this.textNode = newNode;
    // rebuild方法会重新调用makeNode方法
    this.rebuild();
  }
}
......

开发者可以根据实际情况创建新的节点,参考示例代码如下所示:

......
@Entry
@Component
struct Index {
  @State message: string = "hello";
  @State isShow: boolean = true;
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  // private count = 0;

  build() {
    Row() {
      Column() {
        if (this.isShow) {
          NodeContainer(this.textNodeController)
            .width('100%')
            .height(100)
            .backgroundColor('#FFF0F0F0')
        }
        Button('replaceNode')
          .onClick(() => {
            this.textNodeController.replaceBuilderNode(this.buildNewNode());
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }

  buildNewNode(): BuilderNode<[Params]> {
    let uiContext: UIContext = this.getUIContext();
    let message = 'newNode';
    let textNode = new BuilderNode<[Params]>(uiContext);
    textNode.build(wrapBuilder<[Params]>(buildText), new Params(message))
    return textNode;
  }
}

NodeController生命周期

NodeController用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。下面,对其常用生命周期函数进行说明。

  • makeNode必须要重写的方法,用于构建节点树、返回节点挂载在对应NodeContainer中。在对应NodeContainer创建绑定当前NodeController的时候调用、或者通过rebuild方法调用刷新。
  • aboutToResize当controller对应的NodeContainer在Mesure的时候进行回调,入参为节点的布局大小。
  • aboutToAppear当controller对应的NodeContainer在onAppear的时候进行回调。
  • aboutToDisappear当controller对应的NodeContainer在onDisappear的时候进行回调。
export abstract class NodeController {
  abstract makeNode(uiContext: UIContext): FrameNode | null;
  aboutToResize?(size: Size): void;
  aboutToAppear?(): void;
  aboutToDisappear?(): void;
  rebuild(): void;
  onTouchEvent?(event: TouchEvent): void;
}

三方广告SDK实践案例

场景描述

应用的广告业务一般都由三方广告商提供。三方广告商通过广告SDK的方式,提供广告内容与广告页面交互逻辑,并且支持通过点击查看广告详情。应用接入广告SDK提供的广告服务,需要在应用页面提供布局空间为广告预留布局空间。在应用开发过程中,通过接入广告SDK,调用广告服务的接口,在预留的的布局空间,向应用用户展示广告信息。以应用开屏广告为例,如下图所示,在应用启动时且在应用主界面显示之前,会展示广告和广告倒计时,用户可以点击跳过广告直接进入应用主界面。

图3 应用场景图

实现方案介绍

ArkUI开发框架是基于ArkTS的声明式开发范式的应用开发框架,和传统的命令式UI相比,声明式UI使用抽象的语法声明来描述用户界面,而命令式UI侧重编写详细的操作来构建用户界面。对于三方广告场景,声明式UI无法提前灵活的声明广告SDK提供的广告组件。

ArkUI框架从API11开始新增开放命令式的渲染节点,通过声明式容器预留节点位置和命令式渲染节点,支持动态操作UI。UI 动态化技术可解决业务模块动态加载的诉求。在应用业务场景中,通常框架开发与业务开发不是同一人员,因此,框架无法通过声明式的方式,提前加入业务方逻辑。如下图所示,业务模块A,B,C 需显示在主框架中,主框架不感知具体业务的模块逻辑,仅需把业务模块动态添加或移除。

图4 模块关系图

对于三方广告SDK场景,如下图所示。广告SDK负责维护广告组件,使用自定义节点BuilderNode挂载原生图文等组件节点来提供广告内容与广告页面交互逻辑;接入广告SDK的应用通过NodeContainer预留布局空间,用于展示广告SDK提供的广告组件。NodeController用于绑定自定义广告组件节点与NodeContainer布局组件。

图5 三方广告SDK场景图

案例参考

以一个案例来展示三方广告SDK如何提供广告组件,以及应用如何集成广告SDK提供的广告组件。案例效果如下:

图6 实现效果图

整体实现步骤如下所示:

  1. 广告SDK创建@Builder自定义构建函数。
  2. 广告SDK创建自定义广告节点控制器。
  3. 广告SDK对外提供广告节点控制器。
  4. 应用界面展示广告。

详细说明实现步骤如下。首先,广告SDK创建@Builder自定义构建函数。广告SDK提供各种类型的广告,需要在广告SDK中创建广告组件的@Builder自定义构建函数。以开屏广告为例来讲解,代码如下。其中ADNodeBuilder()为自定义构建函数,用于构建广告组件节点,该广告节点示例代码中包含一个广告图片、一段文本和一个可以跳转广告详情的按钮。ADNodeParams参数类用于为ADNodeBuilder() 自定义构建函数提供参数化数据,以展示不同的广告信息。

// adsdk\src\main\ets\model\ADNode.ets
import Constants from '../common/Constants';
import storeCurrentTime from '../Utils/TimeUtils';

// 广告SDK对外提供的广告组件节点的参数
class ADNodeParams {
  ad_img_url: string = ''
  ad_link: string = ''
  ad_text: string = ''
}

// 广告SDK对外提供的广告组件节点的自定义构建函数
@Builder
function ADNodeBuilder(params: ADNodeParams) {
  //该广告节点示例,包含一个广告图片和按钮,可以跳转到指定的链接
  Stack({ alignContent: Alignment.Bottom }) {
    Image(params.ad_img_url)
      .objectFit(ImageFit.Contain)
      .height(Constants.IMAGE_HEIGHT)
      .width(Constants.IMAGE_WIDTH)
    Button(params.ad_text)
      .backgroundColor(Color.Gray)
      .alignSelf(ItemAlign.End)
  }.height(Constants.AD_NODE_HEIGHT)
  .width(Constants.AD_NODE_WIDTH)
  .onClick(() => {
    // 跳转至对应的应用或页面
  })
}
......


其次,广告SDK创建自定义广告节点控制器。ADNodeController类继承NodeController,用于绑定NodeContainer容器,它持有一个BuilderNoder自定义节点,可以挂载原生组件。NodeController可通过BuilderNode持有的FrameNode将其挂载到NodeContainer上。

NodeController类中的函数makeNode函数会在NodeController实例绑定的NodeContainer组件创建的时候回调,回调方法将返回一个节点,将该节点挂载至NodeContainer。

// adsdk\src\main\ets\model\ADNode.ets
......
// 自定义广告节点控制器
class ADNodeController extends NodeController {
  private node: BuilderNode<Object[]> | null = null;

  constructor(node: BuilderNode<Object[]>) {
    super();
    this.node = node;
  }

  // 当NodeController绑定的NodeContainer挂载显示时,触发此回调
  // 可以加一个打点,记录标明广告被真实的显示出来的时间
  aboutToAppear(): void {
    console.info("ADController aboutToAppear");
    storeCurrentTime(Constants.AD_SHOW_START_TIME);
  }

  // 当NodeController绑定的NodeContainer卸载消失时,触发此回调
  // 可以加一个打点,记录标明广告退出的时间
  aboutToDisappear(): void {
    console.info("ADController aboutToDisappear");
    storeCurrentTime(Constants.AD_SHOW_END_TIME);

  }

  // 当NodeController实例绑定的NodeContainer创建的时候进行回调。回调方法将返回一个节点,将该节点挂载至NodeContainer。
  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.node == null) {
      return null;
    }
    return this.node.getFrameNode();
  }

  // 更新渲染节点
  update(params: ADNodeParams) {
    if (this.node != null) {
      console.info(`update params:${JSON.stringify(params)}`)
      this.node.update(params)
    }
  }
}
......

然后,广告SDK对外提供广告节点控制器。Advertising单例类用于对外提供广告。广告类型有开屏广告、插屏广告、Banner广告、贴片广告等,getADNode()函数根据广告类型,获取对应的广告组件的NodeController节点控制器实例,获取到的NodeController实例和应用NodeContainer组件绑定,用于在应用侧展示广告。getADNode()函数会进一步调用私有函数createSplashADNodeController(),该函数首先使用广告组件的自定义函数ADNodeBuilder()及其需要的参数实例化BuilderNode自定义节点,然后创建一个ADNodeController()实例并返回。

// adsdk\src\main\ets\model\ADNode.ets
......
// 三方广告SDK对外提供接口的交互类
export class Advertising {
  private static instance: Advertising;
  public static readonly SPLASH_SCREEN_AD = 0;

  // 获取交互类单例
  public static getInstance(): Advertising {
    if (Advertising.instance == null) {
      Advertising.instance = new Advertising();
    }
    return Advertising.instance;
  }

  // 根据广告类型,获取对应的广告组件的NodeController节点控制器实例
  public getADNode(type: Number, uiContext: UIContext): NodeController | null {
    // 根据应用指定的类型,返回特定的广告节点控制器实例
    if (type === Advertising.SPLASH_SCREEN_AD) {
      return this.createSplashADNodeController(uiContext);
    }
    return null;
  }

  // 构建广告组件的自定义节点,并使用自定义节点创建NodeController广告控制器实例
  private createSplashADNodeController(uiContext: UIContext): NodeController {
    let node = new BuilderNode<[ADNodeParams]>(uiContext);
    node.build(new WrappedBuilder<[ADNodeParams]>(ADNodeBuilder), {
      ad_img_url: Constants.ad_img_url,
      ad_link: Constants.ad_link,
      ad_text: Constants.ad_text
    })
    return new ADNodeController(node);
  }
}

接着,应用界面展示广告。应用页面的aboutToAppear()方法中调用广告SDK接口getADNode(),获取NodeController广告节点控制器实例,和应用页面广告NodeContainer布局空间进行绑定。通过传入的广告类型参数的不同,展示不同的广告图文组件,从而完成广告的动态展示。

// entry\src\main\ets\pages\SplashScreenPage.ets
import { NodeController, router } from '@kit.ArkUI';
import { CommonConstants } from '../constants/CommonConstants';

import { Advertising } from '@adsdk/sdkdemo'



// SplashScreenPage为应用的入口页面,在该页面上展示广告,用户可以点击跳过广告,也可以等待广告倒计时结束进入应用主页。
@Entry
@Component
struct SplashScreenPage {
  @State pageShowTime: number = CommonConstants.TIME_DEFAULT_VALUE;
  @State intervalID: number = CommonConstants.INTERVAL_ID_DEFAULT;
  private controller: NodeController | null = null;

  aboutToAppear(): void {
    // 获取当前页的UIContext
    let uiContext = this.getUIContext();
    // 调用三方广告SDK提供的单例接口获取广告节点控制器
    this.controller = Advertising.getInstance()
      .getADNode(Advertising.SPLASH_SCREEN_AD, uiContext);
  }

  build() {
    Column() {
      Stack({ alignContent: Alignment.TopStart }) {
        Image($r('app.media.ic_splash_page_background'))
          .width(CommonConstants.IMAGE_WIDTH)
          .height(CommonConstants.IMAGE_HEIGHT)

        // 使用NodeContainer进行布局占位,为广告节点提供布局空间
        NodeContainer(this.controller)
        AdvertiseIcon()
        SkipButton({ secondsCount: (CommonConstants.DELAY_SECONDS - this.pageShowTime) })
      }
      .layoutWeight(CommonConstants.STACK_LAYOUT_WEIGHT)
      .width(CommonConstants.STACK_WIDTH)
    }
    .alignItems(HorizontalAlign.Start)
    .width(CommonConstants.COLUMN_WIDTH)
    .height(CommonConstants.COLUMN_HEIGHT)
  }

  /**
   * When the SplashScreenPage is displayed, switch to the next page after 3 seconds.
   */
  onPageShow() {
    this.intervalID = setInterval(() => {
      this.pageShowTime += CommonConstants.INCREMENT_VALUE;
      if (this.pageShowTime > CommonConstants.DELAY_SECONDS) {
        router.replaceUrl({
          url: CommonConstants.MAIN_PAGE_URL
        });
      }
    }, CommonConstants.INTERVAL_DELAY);
  }

  /**
   * When the SplashScreenPage is hide, clear interval.
   */
  onPageHide() {
    clearInterval(this.intervalID);
  }
}

@Component
struct SkipButton {
  @Prop secondsCount: number = 0;

  build() {
    Flex({
      direction: FlexDirection.Row,
      justifyContent: FlexAlign.End
    }) {
      Text($r('app.string.skip', this.secondsCount))
        .backgroundColor($r('app.color.rect_area_background'))
        .borderRadius(CommonConstants.SKIP_BUTTON_RADIUS)
        .fontColor(Color.White)
        .width($r('app.float.skip_text_width'))
        .height($r('app.float.skip_text_height'))
        .fontSize($r('app.float.skip_text_size'))
        .margin({
          right: $r('app.float.skip_text_margin_right'),
          top: $r('app.float.skip_text_margin_top')
        })
        .textAlign(TextAlign.Center)
        .border({ width: CommonConstants.SKIP_TEXT_BORDER_WIDTH })
        .borderColor($r('app.color.rect_stroke'))
        .onClick(() => {
          router.replaceUrl({
            url: CommonConstants.MAIN_PAGE_URL
          });
        })
    }
  }
}

@Component
struct AdvertiseIcon {
  build() {
    Flex({
      direction: FlexDirection.Column,
      justifyContent: FlexAlign.End
    }) {
      Image($r('app.media.ic_advertise'))
        .objectFit(ImageFit.Contain)
        .width($r('app.float.advertise_button_width'))
        .height($r('app.float.advertise_button_height'))
    }
    .margin($r('app.float.advertise_icon_margin'))
  }
}

最后,我们介绍一下进阶用法。上文介绍了广告SDK场景的基础实现方案。投放广告时,通常需要统计广告的显示时长等。我们通过在自定义节点控制器NodeController的生命周期函数中打点标记广告开始显示和消失的时间,从而可以实现广告显示时间的统计。下面,我们进一步完善广告SDK。

在ADNodeController类中,当NodeController绑定的NodeContainer挂载显示时,触发aboutToAppear()生命周期回调,可以在此函数中记录标明广告被真实的显示出来的时间。当NodeController绑定的NodeContainer卸载消失时,触发aboutToDisappear()生命周期回调,可以在此函数中记录标明广告退出的时间。其中storeCurrentTime()工具类函数用于保存广告展示的开始和结束时间数据。

// 当NodeController绑定的NodeContainer挂载显示时,触发此回调
// 可以加一个打点,记录标明广告被真实的显示出来的时间
aboutToAppear(): void {
  console.info("ADController aboutToAppear");
  storeCurrentTime(Constants.AD_SHOW_START_TIME);
}

// 当NodeController绑定的NodeContainer卸载消失时,触发此回调
// 可以加一个打点,记录标明广告退出的时间
aboutToDisappear(): void {
  console.info("ADController aboutToDisappear");
  storeCurrentTime(Constants.AD_SHOW_END_TIME);
}

广告展示时间数据可以上传运营平台,本案例中,为方便演示,在应用主界面展示广告显示的时间,对应代码如下。在应用主界面展示执行前,在aboutToAppear()函数中获取广告开始展示和消失的时间,赋值给状态变量@State ad_show_time,实现在界面上展示广告实际的显示时长。

// entry\src\main\ets\pages\MainPage.ets
import { CommonConstants } from '../constants/CommonConstants';

/**
 * 开屏广告展示之后,进入应用主页,页面上展示广告组件显示时长
 */
@Entry
@Component
struct MainPage {
  @State ad_show_time: number = 0;

  // 获取广告开始展示和消失的时间,并在界面上展示广告实际的显示时长
  aboutToAppear(): void {
    setTimeout(() => {
      let startTime: number = AppStorage.get(CommonConstants.AD_SHOW_START_TIME) as number;
      let endTime: number = AppStorage.get(CommonConstants.AD_SHOW_END_TIME) as number;
      this.ad_show_time = Math.round((endTime - startTime) / CommonConstants.MS_TO_SECOND);
    }, CommonConstants.TIMEOUT)
  }

  // 自定义构建函数,展示广告实际显示时长
  build() {
    Row() {
      Column() {
        Text($r('app.string.main_page_content'))
          .fontSize($r('app.float.hello_world_font_size'))
          .fontWeight(FontWeight.Bold)
        Blank()
        Text(this.ad_show_time.toString())
          .fontSize($r('app.float.hello_world_font_size'))
          .fontWeight(FontWeight.Bold)
      }
      .width(CommonConstants.MAIN_PAGE_COLUMN_WIDTH)
    }
    .height(CommonConstants.MAIN_PAGE_ROW_WIDTH)
  }
}

最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。 

点击领取→【纯血版鸿蒙全套最新学习资料】(安全链接,放心点击希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、(南向驱动、嵌入式等)鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。


 鸿蒙(HarmonyOS NEXT)最新学习路线

有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料

HarmonyOS Next 最新全套视频教程

 《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

《鸿蒙开发基础》

  • ArkTS语言
  • 安装DevEco Studio
  • 运用你的第一个ArkTS应用
  • ArkUI声明式UI开发
  • .……

《鸿蒙开发进阶》

  • Stage模型入门
  • 网络管理
  • 数据管理
  • 电话服务
  • 分布式应用开发
  • 通知与窗口管理
  • 多媒体技术
  • 安全技能
  • 任务管理
  • WebGL
  • 国际化开发
  • 应用测试
  • DFX面向未来设计
  • 鸿蒙系统移植和裁剪定制
  • ……

《鸿蒙进阶实战》

  • ArkTS实践
  • UIAbility应用
  • 网络案例
  • ……

大厂面试必问面试题

鸿蒙南向开发技术

鸿蒙APP开发必备

鸿蒙生态应用开发白皮书V2.0PDF


请点击→纯血版全套鸿蒙HarmonyOS学习资料

总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。 

                   

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值