赞
踩
五年前,有人告诉我,你可以错过其他技术,但千万不要错过 Flutter
。然而此刻,有人告诉我,如果你错过了 OpenHarmony,恐怕要错过下个时代了。
作为发展了 5
年的 FlutterCandies 社区,我们已拥有 70+
的 Flutter
组件。我们当然也不会止步于 Flutter
。我们希望把我们的 Flutter
组件也能带到 OpenHarmony 生态当中,HarmonyCandies 便是为了这一刻。
以 Flutter
开发者的角度,尽可能提供相同 Api
的 OpenHarmony 组件。
本文默认您已经有一定的 OpenHarmony 开发经验,并且阅读过以下内容。
使用的 ide
版本为 DevEco Studio 4.0 Release
OpenHarmony v4.0 Release (2023-10-26) ,开发 sdk
为 api 9
,当然也适配了 api 10
。
列表在一个 App
中最常见的呈现方式,而下拉刷新是其常见的一种效果。
在 Flutter
中你可以通过
pull\_to\_refresh\_notification
来实现一个可以自定义任何效果的下拉刷新。
在.OpenHarmony 中你则可以使用 https://github.com/HarmonyCandies/pull\_to\_refresh
来实现。
你可以通过下面的命令来下载安装
ohpm install @candies/pull_to_refresh
export enum PullToRefreshIndicatorMode {
initial, // 初始状态
drag, // 手势向下拉的状态.
armed, // 被拖动得足够远,以至于触发“onRefresh”回调函数的上滑事件
snap, // 用户没有拖动到足够远的地方并且释放回到初始化状态的过程
refresh, // 正在执行刷新回调.
done, // 刷新回调完成.
canceled, // 用户取消了下拉刷新手势.
error, // 刷新失败
}
参数 | 类型 | 描述 |
---|---|---|
maxDragOffset | number | 最大拖动距离(非必填) |
reachToRefreshOffset | number | 到达满足触发刷新的距离(非必填) |
refreshOffset | number | 触发刷新的时候,停留的刷新距离(非必填) |
pullBackOnRefresh | boolean | 在触发刷新回调的时候是否执行回退动画(默认 false ) |
pullBackAnimatorOptions | AnimatorOptions | 回退动画的一些配置(duration,easing,delay,fill) |
pullBackOnError | boolean | 刷新失败的时候,是否执行回退动画(默认 false ) |
maxDragOffset
和 reachToRefreshOffset
如果不定义的话,会根据当前容器的高度设置默认值。/// Set the default value of [maxDragOffset,reachToRefreshOffset]
onAreaChange(oldValue: Area, newValue: Area) {
if (this.maxDragOffset == undefined) {
this.maxDragOffset = (newValue.height as number) / 5;
}
if (this.reachToRefreshOffset == undefined) {
this.reachToRefreshOffset = this.maxDragOffset * 3 / 4;
}
else {
this.reachToRefreshOffset = Math.min(this.reachToRefreshOffset, this.maxDragOffset);
}
}
pullBackAnimatorOptions
的默认值如下:/// The options of pull back animation
pullBackAnimatorOptions: AnimatorOptions = {
duration: 400,
easing: "friction",
delay: 0,
fill: "forwards",
direction: "normal",
iterations: 1,
begin: 1.0,
end: 0.0
};
触发的下拉刷新事件
/// A function that's called when the user has dragged the refresh indicator
/// far enough to demonstrate that they want the app to refresh. The returned
/// [Future] must complete when the refresh operation is finished.
onRefresh: RefreshCallback = async () => true;
是否我们到达了下拉刷新的边界,比如说,下拉刷新的内容是一个列表,那么边界就是到达列表的顶部位置。
/// Whether we reach the edge to pull refresh
onReachEdge: () => boolean = () => true;
import {
PullToRefresh,
pull_to_refresh,
PullToRefreshIndicatorMode,
} from '@candies/pull_to_refresh'
@State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller();
将需要支持下拉刷新的部分,通过 @BuilderParam
修饰的 builder
回调传入,或者尾随闭包初始化组件。
PullToRefresh( { refreshOffset: 150, maxDragOffset: 300, reachToRefreshOffset: 200, controller: this.controller, onRefresh: async () => { return new Promise<boolean>((resolve) => { setTimeout(() => { // 定义的刷新方法,当刷新成功之后,返回回调,模拟 2 秒之后刷新完毕 this.onRefresh().then((value) => resolve(value)); }, 2000); }); }, onReachEdge: () => { let yOffset = this.scroller.currentOffset().yOffset; return Math.abs(yOffset) < 0.001; } }) { // 我们自定义的下拉刷新头部 PullToRefreshContainer({ lastRefreshTime: this.lastRefreshTime, controller: this.controller, }) List({ scroller: this.scroller }) { ForEach(this.listData, (item, index) => { ListItem() { Text(`${item}`,).align(Alignment.Center) }.height(100).width('100%') }, (item, index) => { return `${item}`; }) } // 必须设置 edgeEffect .edgeEffect(EdgeEffect.None) // 为了使下拉刷新的手势的过程中,不触发列表的滚动 .onScrollFrameBegin((offset, state) => { if (this.controller.dragOffset > 0) { offset = 0; } return { offsetRemain: offset, }; }) } }
你可以通过对 Controller
中 dragOffset
和 mode
的判断,创建属于自己的下拉刷新效果。如果下拉刷新失败了,你可以通过调用 Controller
的 refresh()
方法来重新执行刷新动画。
/// The current drag offset
dragOffset: number = 0;
/// The current pull mode
mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial;
下面是一个自定义下拉刷新头部的例子
@Component struct PullToRefreshContainer { @Prop lastRefreshTime: number = 0; @Link controller: pull_to_refresh.Controller; getShowText(): string { let text = ''; if (this.controller.mode == PullToRefreshIndicatorMode.armed) { text = 'Release to refresh'; } else if (this.controller.mode == PullToRefreshIndicatorMode.refresh || this.controller.mode == PullToRefreshIndicatorMode.snap) { text = 'Loading...'; } else if (this.controller.mode == PullToRefreshIndicatorMode.done) { text = 'Refresh completed.'; } else if (this.controller.mode == PullToRefreshIndicatorMode.drag) { text = 'Pull to refresh'; } else if (this.controller.mode == PullToRefreshIndicatorMode.canceled) { text = 'Cancel refresh'; } else if (this.controller.mode == PullToRefreshIndicatorMode.error) { text = 'Refresh failed'; } return text; } getDate(): String { return (new Date(this.lastRefreshTime)).toTimeString(); } build() { Row() { if (this.controller.dragOffset != 0) Text(`${this.getShowText()}---${this.getDate()}`) if (this.controller.dragOffset > 50 && this.controller.mode == PullToRefreshIndicatorMode.refresh) LoadingProgress().width(50).height(50) } .justifyContent(FlexAlign.Center) .height(this.controller.dragOffset) .width('100%') .onClick(() => { if (this.controller.mode == PullToRefreshIndicatorMode.error) { this.controller.refresh(); } }) .backgroundColor('#22808080') } }
虽然练习时长只有一个月,但通过编写第一个 ArtUI
组件,还是学到了不少东西。
先到 OpenHarmony 三方库中心仓 上面注册个账号,到 个人中心
=》组织管理
中,申请一个组织。这个组织名字以后要用到,因为普通三方作者,是不能使用 ohos
前缀的。
比如我注册的是组织名为 candies
,组件为 pull_to_refresh
。那么组件最终的名字就是 @candies/pull_to_refresh
最后用户可以通过 ohpm install @candies/pull_to_refresh
,来安装使用组件。
为啥这个要先做,因为审核很慢。
写一个组件,必然也会给这个组件创建一个演示例子,在 Flutter
中发布一个组件,你可以使用下面的结构。
package
--example
而在 OpenHarmony
里面你只能使用下面的结构,这样才能方便你修改代码。
example
--package
2
种结构的区别是, package
下面肯定会需要加 README
,LICENSE
,但是 github
,gitee
默认只会显示根目录下面的 README
,第二种结构就要多复制一份到 example
目录下面。
但是 OpenHarmony 三方库中心仓 却要求,有点难顶啊。
ide
啥时候支持下第一种结构呀!
创建一个项目。
创建一个 Static Libray
(至于其他 Module
是什么意思,请自行查看文档)
创建好的目录长这样子
oh-package.json5
中是你的组件的信息。
这里你需要把名字改成 @candies/pull_to_refresh
即 (@你的组织名字/组件名字)
一个完整的 oh-package.json5
是这样的
{ "license": "Apache-2.0", "devDependencies": {}, "keywords": [ "pull", "refresh", "pulltorefresh" ], "author": "zmtzawqlp", "name": "@candies/pull_to_refresh", "description": "Harmony plugin for building pull to refresh effects with PullToRefresh quickly.", "main": "index.ets", "repository": "https://github.com/HarmonyCandies/pull_to_refresh", "version": "1.0.0", "homepage": "https://github.com/HarmonyCandies/pull_to_refresh", "dependencies": {} }
组件项目中 Index.ets
是入口,用于导出组件。跟 Flutter
中 lib
下面带 library 组件名;
标识的 dart
文件效果一样。
export { MainPage } from './src/main/ets/components/mainpage/MainPage'
要想 Example
能引用到 pull_to_refresh
, 你还需要到
{
"license": "",
"devDependencies": {},
"author": "",
"name": "entry",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {
"@candies/pull_to_refresh": "file:../pull_to_refresh"
}
}
在准备发布之前,请先阅读 贡献三方库 下面内容。
阅读操作完毕之后,你就可以打你的 har
包了。选中你的组件项目,在 Build
下面选择 Make Module 你的组件名字
。编译完成之后,你就可以在组件项目路径 build\default\outputs\default\
中找到你即将发布的包。
最后执行 ohpm publish xxx.har
(xxx.har
为上传包的本地路径)。上传成功之后,你就可以看到你的个人中心里面的消息和状态了,耐心等待审核。
我遇到的上架的问题主要是组织名称(当然,这是我自己猜的,后面会聊到这个),ohos
不是普通三方开发者使用的前缀, ohos
的库都在 OpenHarmony-TPC: OpenHarmony third party components (gitee.com)
下面。按道理你可以 pr
到这个下面,并且加入到 ohos
中,再发布。当然更欢迎大家能加入 candies
组织,大家一起生产有趣的小组件。
第一眼看到这个状态管理装饰器的时候,好亲切的感觉。这不是就是 Flutter
里面的 (provider | Flutter Package (flutter-io.cn)) 吗?
最开始设计 pull\_to\_refresh
的时候,想着跟 Flutter
中一样,父组件里面存放管理下拉刷新的状态,然后子组件里面监听状态,达到局部刷新的效果。
第一版的设计结构如下:
CustomWidget
中提供了 @Provide('a')
CustomWidgetChild
中使用 @Consume('a')
获取状态变化。@Entry @Component struct HomePage { @Builder builder2($$: { a: string }) { Text(`${$$.a}测试`) } build() { Column() { CustomWidget() { CustomWidgetChild({ builder: this.builder2 }) } } } } @Component struct CustomWidget { @Provide('a') a: string = 'abc'; @BuilderParam builder: () => void; build() { Column() { Button('你好').onClick((x) => { if (this.a == 'ddd') { this.a = 'abc'; } else { this.a = 'ddd'; } }) this.builder() } } } @Component struct CustomWidgetChild { @Consume('a') a: string; @BuilderParam builder: ($$: { a: string }) => void; build() { Column() { this.builder({ a: this.a }) } } }
运行会报找不到 Provide
的错误。
通过分析由 ArkTS
生成的 js
文件(生成的 js
在 entry\build\default\cache\default\default@CompileArkTS\esmodule\debug
路径下面) ,我们可以分析得出:
CustomWidgetChild
其父组件实际上是 HomePage
,其内部 this
指向的也是 HomePage
,因此找不到 CustomWidget
的 @Provide
变量。
class HomePage extends ViewPU { constructor(parent, params, __localStorage, elmtId = -1) { super(parent, __localStorage, elmtId); this.setInitiallyProvidedValue(params); } setInitiallyProvidedValue(params) { } updateStateVars(params) { } purgeVariableDependenciesOnElmtId(rmElmtId) { } aboutToBeDeleted() { SubscriberManager.Get().delete(this.id__()); this.aboutToBeDeletedInternal(); } builder2($$, parent = null) { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Text.create(`${$$.a}测试`); if (!isInitialRender) { Text.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); Text.pop(); } initialRender() { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Column.create(); if (!isInitialRender) { Column.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); if (isInitialRender) { ViewPU.create(new CustomWidget(this, { builder: () => { { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); if (isInitialRender) { ViewPU.create(new CustomWidgetChild(this, { builder: this.builder2 }, undefined, elmtId)); } else { this.updateStateVarsOfChildByElmtId(elmtId, {}); } ViewStackProcessor.StopGetAccessRecording(); }); } } }, undefined, elmtId)); } else { this.updateStateVarsOfChildByElmtId(elmtId, {}); } ViewStackProcessor.StopGetAccessRecording(); }); } Column.pop(); } rerender() { this.updateDirtyElements(); } } class CustomWidget extends ViewPU { constructor(parent, params, __localStorage, elmtId = -1) { super(parent, __localStorage, elmtId); this.__a = new ObservedPropertySimplePU('abc', this, "a"); this.addProvidedVar("a", this.__a); this.builder = undefined; this.setInitiallyProvidedValue(params); } setInitiallyProvidedValue(params) { if (params.a !== undefined) { this.a = params.a; } if (params.builder !== undefined) { this.builder = params.builder; } } updateStateVars(params) { } purgeVariableDependenciesOnElmtId(rmElmtId) { } aboutToBeDeleted() { this.__a.aboutToBeDeleted(); SubscriberManager.Get().delete(this.id__()); this.aboutToBeDeletedInternal(); } get a() { return this.__a.get(); } set a(newValue) { this.__a.set(newValue); } initialRender() { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Column.create(); if (!isInitialRender) { Column.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Button.createWithLabel('你好'); Button.onClick((x) => { if (this.a == 'ddd') { this.a = 'abc'; } else { this.a = 'ddd'; } }); if (!isInitialRender) { Button.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); Button.pop(); this.builder.bind(this)(); Column.pop(); } rerender() { this.updateDirtyElements(); } } class CustomWidgetChild extends ViewPU { constructor(parent, params, __localStorage, elmtId = -1) { super(parent, __localStorage, elmtId); this.__a = this.initializeConsume("a", "a"); this.builder = undefined; this.setInitiallyProvidedValue(params); } setInitiallyProvidedValue(params) { if (params.builder !== undefined) { this.builder = params.builder; } } updateStateVars(params) { } purgeVariableDependenciesOnElmtId(rmElmtId) { } aboutToBeDeleted() { this.__a.aboutToBeDeleted(); SubscriberManager.Get().delete(this.id__()); this.aboutToBeDeletedInternal(); } get a() { return this.__a.get(); } set a(newValue) { this.__a.set(newValue); } initialRender() { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Column.create(); if (!isInitialRender) { Column.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); this.builder.bind(this)(makeBuilderParameterProxy("builder", { a: () => (this["__a"] ? this["__a"] : this["a"]) })); Column.pop(); } rerender() { this.updateDirtyElements(); } } ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent()); loadDocument(new HomePage(undefined, {})); ViewStackProcessor.StopGetAccessRecording(); export {}; //# sourceMappingURL=Index.js.map
意思就是你只能写成下面的这种形式。虽然说 CustomWidgetChild
是看起来是通过 CustomWidget
的 builder
创建出来的,但是它们依然没有父子关系,这跟 Flutter
完全不是一套原理。
@Entry @Component struct HomePage { @Provide('a') test: string = 'abc'; @Builder builder2($$: { a: string }) { Text(`${$$.a}测试`) } build() { Column() { CustomWidget() { CustomWidgetChild({ builder: this.builder2 }) } } } } @Component struct CustomWidget { @Consume('a') a: string; @BuilderParam builder: () => void; build() { Column() { Button('你好').onClick((x) => { if (this.a == 'ddd') { this.a = 'abc'; } else { this.a = 'ddd'; } }) this.builder() } } } @Component struct CustomWidgetChild { @Consume('a') a: string; @BuilderParam builder: ($$: { a: string }) => void; build() { Column() { this.builder({ a: this.a }) } } }
在自定义组件中,如果你想传入其他的组件,你需要使用到 @Builder
和 @BuilderParam
, 代码如下:
@Component struct Child { @BuilderParam aBuilder0: () => void; build() { Column() { this.aBuilder0() } } } @Entry @Component struct Parent { @Builder componentBuilder() { Text(`Parent builder `) } build() { Column() { Child({ aBuilder0: this.componentBuilder }) } } }
但是实际中写一个自定义组件的时候,会有这种需求。需要为 BuilderParam
修饰的内容的返回增加一些事件或者设置。比如下面例子,为 BuilderParamChild
的 builder
的返回增加 hitTestBehavior
设置。我这里将 builder
的返回修改为了 CommonMethod<any>
(组件都继承于该类,里面是一些公共的属性,事件),虽然这样可以让编辑器有提示,并且不报错,但是运行起来依然会提示 hitTestBehavior
找不到。
@Component struct BuilderParamTestDemo { build() { Column(){ BuilderParamChild(){ Text('测试') } } } } @Component struct BuilderParamChild { @BuilderParam builder: () => CommonMethod<any>; build() { this.builder().hitTestBehavior(HitTestMode.None) } }
错误堆栈如下:
E Error message: Cannot read property hitTestBehavior of undefined
E SourceCode:
E this.builder().hitTestBehavior.bind(this)(HitTestMode.None);
E ^
E Stacktrace:
E at initialRender (entry/src/main/ets/pages/Index.ets:20:5)
从生成的 js
中也能看到对应的代码。
"use strict"; class BuilderParamTestDemo extends ViewPU { constructor(parent, params, __localStorage, elmtId = -1) { super(parent, __localStorage, elmtId); this.setInitiallyProvidedValue(params); } setInitiallyProvidedValue(params) { } updateStateVars(params) { } purgeVariableDependenciesOnElmtId(rmElmtId) { } aboutToBeDeleted() { SubscriberManager.Get().delete(this.id__()); this.aboutToBeDeletedInternal(); } initialRender() { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Column.create(); if (!isInitialRender) { Column.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); if (isInitialRender) { ViewPU.create(new BuilderParamChild(this, { builder: () => { this.observeComponentCreation((elmtId, isInitialRender) => { ViewStackProcessor.StartGetAccessRecordingFor(elmtId); Text.create('测试'); if (!isInitialRender) { Text.pop(); } ViewStackProcessor.StopGetAccessRecording(); }); Text.pop(); } }, undefined, elmtId)); } else { this.updateStateVarsOfChildByElmtId(elmtId, {}); } ViewStackProcessor.StopGetAccessRecording(); }); } Column.pop(); } rerender() { this.updateDirtyElements(); } } class BuilderParamChild extends ViewPU { constructor(parent, params, __localStorage, elmtId = -1) { super(parent, __localStorage, elmtId); this.builder = undefined; this.setInitiallyProvidedValue(params); } setInitiallyProvidedValue(params) { if (params.builder !== undefined) { this.builder = params.builder; } } updateStateVars(params) { } purgeVariableDependenciesOnElmtId(rmElmtId) { } aboutToBeDeleted() { SubscriberManager.Get().delete(this.id__()); this.aboutToBeDeletedInternal(); } initialRender() { this.builder().hitTestBehavior.bind(this)(HitTestMode.None); } rerender() { this.updateDirtyElements(); } } ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent()); loadDocument(new BuilderParamTestDemo(undefined, {})); ViewStackProcessor.StopGetAccessRecording(); //# sourceMappingURL=Index.js.map
对应这个问题,官方的解释是
1.ArkUI
没有类似安卓基类组件。
2.目前 ArkUI
组件是没有具体的类型,也不支持组件继承。
3.如果需要给自定义构建方法添加属性,只能是套一层容器组件之后再给容器组件设置属性
4.从语法规范上来讲,BuilderParam
的方法类型就是 () => void
话虽然这样说,但我还是提出了疑问,那么有没有那种单纯的容器组件, 不管是用 Row
,还是 Column
或者其他功能容器,这里的含义都蛮奇怪的。
回答是,暂时没有。希望官方以后还是考虑一下这个,虽然我包个 Row/Column
是可以,但是感觉怪怪的。
@Component
struct BuilderParamChild {
@BuilderParam
builder: () => void;
build() {
Column() {
this.builder()
}.hitTestBehavior(HitTestMode.None)
}
}
在给组件定义参数的时候,会遇到这个参数不必须设置,但后续需要根据情况给它一个默认值。
在 Flutter
中,我们可以通过定义参数为可空,然后在后续流程中判断这个参数是否为 null
,再给它默认值。
在 ArkTS
中我第一反应是这样写:
maxDragOffset: number | null = null;
但是,当这个参数如果用 @Prop
等状态装饰器修饰的时候,它是不允许简单类型和复杂类型的联合类型。这会引起很多奇怪的问题,在 api9
上面各种 carsh
,但是 api10
看起来是支持了(顺便说说,api9
和 api10
的相同代码,效果不一样的情况比比皆是)。而且 ide
的错误定位也很奇怪,比如我在做另一个组件 LikeButton
的时候,错误堆栈直接误导了我好久,最后排除法才搞好的。
Js-Engine: ark
page: pages/Index.js
Error message: ObservedPropertySimple value must not be an object
Stacktrace:
at ObservedPropertySimple (/mnt/disk/jenkins/ci/workspace/chipset_pipeline_release/china_compile/component_code/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:2179:2179)
at SynchedPropertySimpleOneWayPU (/mnt/disk/jenkins/ci/workspace/chipset_pipeline_release/china_compile/component_code/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:3304:3304)
at CirclePainter (like_button/src/main/ets/painter/CirclePainter.ets:10:29)
at anonymous (like_button/src/main/ets/components/LikeButton.ets:461:35)
修复记录 fix on api9 · HarmonyCandies/like\_button@eefe49d (github.com)
所以你可以这样写,通过是否为 undefined
,来判断用户是否设置过这个参数。
@Prop maxDragOffset: number = undefined;
为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05
https://qr21.cn/FV7h05
入门必看:https://qr21.cn/FV7h05
1. 应用开发导读(ArkTS)
2. ……
HarmonyOS 概念:https://qr21.cn/FV7h05
如何快速入门:https://qr21.cn/FV7h05
1. 基本概念
2. 构建第一个ArkTS应用
3. 构建第一个JS应用
4. ……
开发基础知识:https://qr21.cn/FV7h05
1. 应用基础知识
2. 配置文件
3. 应用数据管理
4. 应用安全管理
5. 应用隐私保护
6. 三方应用调用管控机制
7. 资源分类与访问
8. 学习ArkTS语言
9. ……
基于ArkTS 开发:https://qr21.cn/FV7h05
1. Ability开发
2. UI开发
3. 公共事件与通知
4. 窗口管理
5. 媒体
6. 安全
7. 网络与链接
8. 电话服务
9. 数据管理
10. 后台任务(Background Task)管理
11. 设备管理
12. 设备使用信息统计
13. DFX
14. 国际化开发
15. 折叠屏系列
16. ……
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。