零基础快速上手HarmonyOS ArkTS开发1---运行Hello World、ArkTS开发语言介绍

概述:

在华为开发者大会2023年8月4日(HDC.Together)大会上,HarmonyOS 4正式发布,其实在2021年那会学习了一点鸿蒙的开发:

不过因为现在的鸿蒙手机完全兼容Android应用,所以学习动力也不是很足,一直就搁置了,直到今年华为官方出了这么一则消息才让我对于学习它有一种紧迫感了,如下:

所以。。这次必须得把它给攻克,不然未来自己的饭碗可能都不保。。

IDE升级配置:

在正式学习之前,先来将IDE进行一个升级,目前我本机的IDE是定格在这个版本:

而当时在学习时我们采用的开发语言选择的是Java,因为当时这个版本在创建项目时可以进行语言的选择:

但是!!!在网上又搜到这么一个“令人痛心”的消息:“HarmonyOS 从API8开始不再支持java作为开发语言”

这对我的打击还是蛮大的,毕竟Android我开发它主要还是使用Java语言,那为了确认这个事实,先来将IDE先升级试一下:

下载安装之后,打开会发现有个设置向导界面:

接下来则需要进行IDE的配置了,这里需要先配置Node.js和Ohpm:

点击下一步则进行HarmonyOS SDK的下载:

其中标红处有一个警告,意思是说我本机的sdk的管理模式已经发生改变了,毕竟当年学习鸿蒙开发是在2前年,所以我们点击一下“Fix”对其进行一个修复:

点击Ok则进入到下载页面:

然后点击“Finish”,修复成功之后则就可以点击“Next”了:

其中在下载的过程中发现个这个提示:

“ArkTS”,这是个什么东东?在官方这个https://developer.harmonyos.com/cn/docs/documentation/doc-guides/document-outline-js-ets-0000001282486428文档中有介绍:

哦,原来是跟TypeScript有关,那很明显它是跟Java木有关系的,然后真的新建项目时语言就选择不了Java了么?那下完之后我们新建试一下:

对于一个从Android转过来学鸿蒙的来说,简直晴天霹雳呀,其学习优势瞬间降为0了。。本来我还在犹豫再重新学它,到底是用Java还是JS,这下好了直接强制学习新语言,如果不学,直接不会。。

官方教程了解:

要学习,当得要找教程啦,既然华为是国内自己搞的,直接上官方学应该妥妥的,是的,这里推荐一个从0开始的官方教程集:https://developer.harmonyos.com/cn/documentation/teaching-video/, 先学基础知识:

其中官方既有视频教学,也有文档记录:

对于自己记笔记也非常方便。等学完了之后,还有一个综合的案例来巩固,代码都是开源的:

嗯,完美,接下来则按着官方的教程一步步来掌控它,下面的大纲则完全按照官方的这个教程集进行。

运行Hello World:

修改IDE的主题颜色:

由于我平常Android Studio使用的是白色风格,而默认DevEco-Studio是黑色系,不太习惯,改一下:

就回到了舒服的亮色系了:

创建项目:

接下来则来创建一个Hello World工程:

都保持默认,其中DeviceType有一个Tablet是个啥设备呢? 度娘:

哦,懂了,平板。接下来下一步就创建成功了,可以可以看到正在同步一些东东,其中:

前端的东东,本身ArkTS就是基于JS来扩展的,所以,貌似前端的小伙伴学习鸿蒙的优势巨大呀。 

运行Hello World:

待同步完成之后,就可以来运行看一下效果了,目前还没有创建模拟器,所以先创建:

其中可以看到可以有本地模拟器、远程模拟器、远程真机,这里学习我们默认用本地模拟器既可,目前没有可用的模拟器需要先安装,点击“Install”:

下载完成之后,就可以看到这个界面进行模拟器的添加了,整个添加的过程跟Android Studio模拟器的创建很类似:

选择手机模拟器,此时回到这个界面,需要下载系统镜像:

有将近2个G,比较大,需要耐心等待一下,等下完了之后, 点击"next"新建一个模拟器:

最后完成,则整个模拟器创建成功:

此时就可以运行程序到该模拟器上了,如下:

此时运行工程到手机上看一下效果:

此时我们可以简单修改一下内容:

其中它有一个模拟器的预览界面:

再运行一下:

了解基本工程目录:

关于工程目录这块其实官方的文档也说明得非常详细了,这里将自己觉得值得一提的过一下。

1、Project&Ohos视图:

如Android工程一样,通常在开发中会在Project和Android视图进行切换:

而在DevEco中也有两个视图:

其中Project视图就不用过多解释了,IDEA都有的,而Ohos就是对工程代码进行了一个分类了,查看起来比较清晰:

  • AppScope中存放应用全局所需要的资源文件。
  • entry是应用的主模块,存放HarmonyOS应用的代码、资源等。
  • configuration存放的是工程应用级的配置文件。

工程级目录:

其实就是整个工程的目录结构。

模块级目录:

关于这块的具体细节,可以参考官方的文档说明:

ArkTS开发语言介绍:

在尝试了第一个HelloWorld的鸿蒙应用运行效果之后,接下来则需要打基础了,先最基本的开发语言开始熟悉--ArkTS。 

编程语言介绍:

ArkTs它是一个前端的语言,还有另外两个前端的语言想必大家都知道:JavaScript和TypeScript,那这三者是啥关系呢?这里直接贴一下官方的说明:

所以要想学好ArkTs,前提是要学好JavaScript和TypeScript,而通常对于JavaScript或多或少都接触过,官方其实直接跳过它的基础学习了【并非它不重要哟,而是把时间花在刀刃上】,直接了解TypeScript的基础语法,所以下面咱们根据官方的教程将这TypeScript的在实际开发中会用得到的语法给学一学。

TypeScript快速入门:

这里的内容都可以在官方找到,我这边是cv大法,目的是自己跟着过一遍。

基础类型:

TypeScript支持一些基础的数据类型,如布尔型、数组、字符串等,下文举例几个较为常用的数据类型,我们来了解下他们的基本使用。

1、布尔值:

TypeScript中可以使用boolean来表示这个变量是布尔值,可以赋值为true或者false。

let isDone: boolean = false;

2、数字:

TypeScript里的所有数字都是浮点数,这些浮点数的类型是 number。除了支持十进制,还支持二进制、八进制、十六进制。比如定义下面这些数字变量:

let decLiteral: number = 2023;
let binaryLiteral: number = 0b11111100111;
let octalLiteral: number = 0o3747;
let hexLiteral: number = 0x7e7;

运行的话最终都会转换成十进程:

3、字符串:

TypeScript里使用 string表示文本数据类型, 可以使用双引号( ")或单引号(')表示字符串。

let name: string = "Jacky";
name = "Tom";
name = 'Mick';

4、数组:

TypeScrip有两种方式可以定义数组。

第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组。

let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>。

let list: Array<number> = [1, 2, 3];

5、元组:

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。

let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error

6、枚举:

enum类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {Red, Green, Blue};
let c: Color = Color.Green;

7、Unknown:

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。那么我们可以使用unknown类型来标记这些变量。

let notSure: unknown = 4;
notSure = 'maybe a string instead';
notSure = false;

可以看到,对于unkown类型的变量,之后可以赋值其它任何类型。

8、Void:

当一个函数没有返回值时,你通常会见到其返回值类型是 void。

function test(): void {
console.log('This is function is void');
}

9、Null 和 Undefined:

TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。

let u: undefined = undefined;
let n: null = null;

10、联合类型【实际用得比较多】:

联合类型(Union Types)表示取值可以为多种类型中的一种。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

条件语句:

TypeScript 条件语句是通过一条或多条语句的执行结果(True 或 False)来决定执行的代码块,这块其实跟Java没啥区别,有不清楚的可以上官网看一下,就列一个swich..case这种条件语句:
var grade:string = 'A'; 
switch(grade) { 
    case 'A': { 
        console.log('优'); 
        break; 
    } 
    case 'B': { 
        console.log('良'); 
        break; 
    } 
    case 'C': {
        console.log('及格'); 
        break;    
    } 
    case 'D': { 
        console.log('不及格'); 
        break; 
    }  
    default: { 
        console.log('非法输入'); 
        break;              
    } 
}

函数:

TypeScript可以创建有名字的函数和匿名函数,其创建方法如下:
// 有名函数
function add(x, y) {
  return x + y;
}

// 匿名函数
let myAdd = function (x, y) {
  return x + y;
};
1、为函数定义类型:
为了确保输入输出的准确性,我们可以为上面那个函数添加类型:
// 有名函数:给变量设置为number类型
function add(x: number, y: number): number {
  return x + y;
}

// 匿名函数:给变量设置为number类型
let myAdd = function (x: number, y: number): number {
  return x + y;
};
2、可选参数:
在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。 比如,我们想让lastName是可选的:
function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + ' ' + lastName;
    else
        return firstName;
}

let result1 = buildName('Bob');
let result2 = buildName('Bob', 'Adams'); 
3、剩余参数:
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 可以使用省略号( ...)进行定义:
function getEmployeeName(firstName: string, ...restOfName: string[]) {
  return firstName + ' ' + restOfName.join(' ');
}

let employeeName = getEmployeeName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');
4、箭头函数【重要】:
ES6版本的TypeScript提供了一个箭头函数,它是定义匿名函数的简写语法,用于函数表达式,它省略了function关键字。箭头函数的定义如下,其函数是一个语句块:
( [param1, parma2,…param n] )=> {
    // 代码块
}

其中,括号内是函数的入参,可以有0到多个参数,箭头后是函数的代码块。我们可以将这个箭头函数赋值给一个变量,如下所示:

let arrowFun = ( [param1, parma2,…param n] )=> {
    // 代码块
}

如何要主动调用这个箭头函数,可以按如下方法去调用:

arrowFun(param1, parma2,…param n)

接下来我们看看如何将我们熟悉的函数定义方式转换为箭头函数。我们可以定义一个判断正负数的函数,如下:

function testNumber(num: number) {
  if (num > 0) {
    console.log(num + ' 是正数');
  } else if (num < 0) {
    console.log(num + ' 是负数');
  } else {
    console.log(num + ' 为0');
  }
}

其调用方法如下:

testNumber(1)   //输出日志:1 是正数

如果将这个函数定义为箭头函数,定义如下所示:

let testArrowFun = (num: number) => {
  if (num > 0) {
    console.log(num + ' 是正数');
  } else if (num < 0) {
    console.log(num + ' 是负数');
  } else {
    console.log(num + ' 为0');
  }
}

其调用方法如下:

testArrowFun(-1)   //输出日志:-1 是负数

后面,我们在学习HarmonyOS应用开发时会经常用到箭头函数。例如,给一个按钮添加点击事件,其中onClick事件中的函数就是箭头函数。

Button("Click Now")
  .onClick(() => {
    console.info("Button is click")
  })

类:

对于熟悉Java的童鞋来说是再熟悉不过了,这里来看一下使用TypeScript是如何定义一个类的,例如,我们可以声明一个Person类,这个类有3个成员:一个是属性(包含name和age),一个是构造函数,一个是getPersonInfo方法,其定义如下所示:
class Person {
  private name: string
  private age: number

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public getPersonInfo(): string {
    return `My name is ${this.name} and age is ${this.age}`;
  }
}

通过上面的Person类,我们可以定义一个人物Jacky并获取他的基本信息,其定义如下:

let person1 = new Person('Jacky', 18);
person1.getPersonInfo();
继承:
TypeScript中允许使用继承来扩展现有的类,对应的关键字为extends。比如:
class Employee extends Person {
  private department: string

  constructor(name: string, age: number, department: string) {
    super(name, age);
    this.department = department;
  }

  public getEmployeeInfo(): string {
    return this.getPersonInfo() + ` and work in ${this.department}`;
  }
}

通过上面的Employee类,我们可以定义一个人物Tom,这里可以获取他的基本信息,也可以获取他的雇主信息,其定义如下:

let person2 = new Employee('Tom', 28, 'HuaWei');
person2.getPersonInfo();
person2.getEmployeeInfo();

在TypeScript中,有public、private、protected修饰符,这个跟Java差不多。

模块:【重要】

随着应用越来越大,通常要将代码拆分成多个文件,即所谓的模块(module)。模块可以相互加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数。

两个模块之间的关系是通过在文件级别上使用 import 和 export 建立的。模块里面的变量、函数和类等在模块外部是不可见的,除非明确地使用 export 导出它们。类似地,我们必须通过 import 导入其他模块导出的变量、函数、类等。

导出:
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出,例如我们要把NewsData这个类导出,代码示意如下:
export class NewsData {
  title: string;
  content: string;
  imagesUrl: Array<NewsFile>;
  source: string;

  constructor(title: string, content: string, imagesUrl: Array<NewsFile>, source: string) {
    this.title = title;
    this.content = content;
    this.imagesUrl = imagesUrl;
    this.source = source;
  }
}
导入:
要想使用其它模块的export出来的类,则需要通过如下进行类的导入,模块的导入操作与导出一样简单。 可以使用以下 import形式之一来导入其它模块中的导出内容。
import { NewsData } from '../common/bean/NewsData';

这块在学习Vue时经常碰到:

 

迭代器:

当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。一些内置的类型如Array,Map,Set,String,Int32Array,Uint32Array等都具有可迭代性。

1、for..of 语句:

for..of会遍历可迭代的对象,调用对象上的Symbol.iterator方法。 下面是在数组上使用for..of的简单例子:

let someArray = [1, "string", false];

for (let entry of someArray) {
    console.log(entry); // 1, "string", false
}

2、for..of vs. for..in 语句:

for..of和for..in均可迭代一个列表,但是用于迭代的值却不同:for..in迭代的是对象的键,而for..of则迭代的是对象的值。比如:

let list = [4, 5, 6];

for (let i in list) {
    console.log(i); // "0", "1", "2",
}

for (let i of list) {
    console.log(i); // "4", "5", "6"
}

ArkTS基础知识:

有了TypeScript基础语法的入门,接下来就可以进入ArkTs的学习了。

ArkTs简介:

关于这块,官网这块已经把它的生世今生说得非常详细了:

用一张官方的介绍图来了解下:

其中引言中有这么一句话:“Mozilla创造了JS,Microsoft创建了TS,Huawei进一步推出了ArkTS。”,另外从两个角度来阐述了当时创造ArkTS的一些现状分析:

等于设计的目的是让开发者更加简单高效,然后又说到了Google的Dart和Jetpack Compose,这个对于搞Android来说的很亲切:

ArkTS的开发框架图如下【我没看懂】:

而对于咱们开发来说,下图更加有意义:

清晰地阐述声明式开发规范的组成部分,这块简单了解一下。

UI描述规范:

在视频中,以这么一个案例代码来阐述ArkTS代码的一些规范,这样理解起来就比较直观,这个案例是这么一个列表:

当点击一行时,则会变色:

其对应的主要代码是:

这写法是不是跟Flutter很相似?如果说你不了解其代码的规则,那么写起来就会摸不着头脑,所以接下来按照视频所说来了解ArkTS声明式UI描述的一些基本概念。

装饰器:

定义:它是用来装饰类、结构、方法和变量,赋予其特殊的含义。 

代码:

而这段代码中的装饰器有2个:

1、@Component装饰器:

ArkTS通过struct声明组件名,并通过@Component装饰器,来构成一个自定义组件,用@Component装饰的struct ListItemComponent代表一个自定义的结构体,名字是ListItemComponent,它是可重用的UI单元,可以与其它组件组合。

2、@State装饰器:

关于它可以参考官方这块的说明,已经非常详情了:

也就是被它装饰的变量isChange值发生改变时,会触发该变量所对应的自定义组件ListItemComponent的UI界面进行自动刷新,

UI描述:

定义:声明式的方式描述UI结构

代码:其实也就是这块的代码:

是以自定义的形式描述该自定义组件ListItemComponent的UI结构。

内置组件:

定义:系统提供的基础组件和容器组件等,可以直接调用。

代码:

Row是水平方向布局的容器组件,而Text是文本组件用来展示一段文字。

属性方法:

定义:用来设置组件的属性。

代码:

 

事件方法:

定义:设置组件对事件的响应逻辑。

代码:

这篇主要是先来学习常用的两个装饰器@Component和@Entry,内置组件、属性方法、事件方法等都会在之后进行详细学习。

自定义组件:

再学习装饰器之前,先来了解一下自定义组件的基本概念,其实这个应该都或多或少都接触过,比如Android开发就有自定义View,其实HarmonyOS为开发者定义了丰富的组件:

但是这些组件在实际开发中往往满足不了,为了提升开发效率通常会将内置组件进行组合成一个自定义组件,以便于其它页面可以灵活高效的使用,这种组件就叫自定义组件,比如咱们举的这个水果排行版界面中,就会使用三个自定义组件来进行UI的搭建:

这三个自定义组件是标题栏、排行榜列表头、排行榜列表项,所以自定义组件在实际开发中是必须得要掌握的,下面则来具体看一下如何自定义一个组件。

常用装饰器@Component与@Entry:

下面以自定义一个标题栏组件为例进行一下细节说明:

1、定义名为TitleComponent的自定义组件:

也就是它:

2、定义名为RankPage的自定义组件,在其中使用TitleComponent组件:

其中当一个组件使用另一个组件的内容时,这个组件就被称为父组件,也就是RankPage是父组件:

而被使用的组件则称之为子组件:

那么系统是如何知道TitleComponet是自定义组件呢?其实是通过@Component装饰器和struct关键字对TitleComponet进行了修饰,struct是组件的数据结构,@Component是组件化的标志:

使用@Entry装饰的自定义组件作为页面的入口,会在页面加载时首先进行渲染,

注意:一个页面有且仅有一个@Entry,只有被@Entry修饰的组件或者子组件才会在页面中进行显示。

3、导出自定义组件:

通常自定义组件的定义和使用它的父组件在不同的文件中,因此需要将自定义组件导出以供外部使用,而导出是使用export关键字:

然后使用时则需要使用import进行导入:

4、自定义组件生命周期回调函数:

自定义组件有如下两个生命周期回调函数:

aboutToAppear():在创建自定义组件的实例后,到执行其build函数之前执行,可以在此函数中对UI需要展示的数据进行初始化,或者申请定时器资源等操作,这样在后续build()函数中可以使用这些数据和资源来进行UI展示,其类似于Android Activity中的onCreate()生命周期函数。比如在水果排行版实例中:

对水果相关的数据进行初始化,用于后续排行版的数据展示。

aboutToDisappear():在自定义组件实例被销毁时调用,通常做一些释放的逻辑,其类似于Android Activity中的onDestroy()生命周期函数:

注:这些回调函数是私有的,是由系统来调用的,无法手动来调用。

5、@Entry修饰的页面入口组件生命周期回调函数:

前面也说过,对于由@Entry修饰的页面它是一个入口组件,对于这种它还有如下三个生命周期:

onPageShow():当打开应用,处于前台时页面显示,该函数会被触发,类似于Android中的onResume()函数。

onPageHide():当应用切到后台时,该函数会被触发,类似于Android中的onStop()函数。

onBackPress():当返回时该函数会被触发,返回true表示页面自己处理返回逻辑不进行页面返回,而如果返回false则交由系统处理返回逻辑,默认返回false。

代码如下:

关于生命周期相关的进一步细节可以参考:https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/arkts-custom-component-lifecycle-0000001482395076-V3,再贴一张官方的图:

渲染控制:

通过渲染控制语法可以让咱们更加自由的绘制UI界面。

条件渲染:使用if/else if/else进行条件渲染

还是看水果排行版的这个实例,在UI上有这么一个效果:

前三名的样式和后面的样式是有区分的,咱们就可以使用这个条件渲染进行条件判断:

 

循环渲染:使用ForEach迭代数组,并为每个数组项创建相应的组件

比如水果列表的显示,就需要使用到它:

关于它的API说有可以参考:https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-rendering-control-foreach-0000001524537153-V3,关于这个地址是如何获得的呢?其实它来自于https://developer.harmonyos.com/,跟android的类似,点指南就可以搜到内容:

然后里面对于开发的知识描述得非常详细,它是需要传三个参数:

第一个参数则是循环渲染的数据源数组;

第二个参数生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。

第三个参数为匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值:

状态管理:

现在创建和控制自定义组件已经学会了,那如何让组件动起来并且能够根据用户的输入或者数据的变化呈现不同的效果呢?水果排行应用有两个交互场景:点击选中某一行水果和刷新排行版数据,接下来就看一下如何通过管理组件的状态来实现这么两个功能。

关于状态管理可以参考官方这块的说明:

@State:

装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。

这里以点击某一行水果排行榜其文字颜色会发生变化为例来看一下此状态管理是如何弄的:

看一下代码实现:

定义了一个isChange变量用来判断排行榜列表上的水果名称和得票数的字体颜色是否改变,其中就用到了@State这个装饰器对该变量进行修饰,然后再Text组件中则根据这个变量进行颜色的判断:

然后在行点击时对该状态变量进行一下值更改:

@Link: 

通过@State装饰器我们可以实现一个组件内部数据更新UI的场景,但是!!!对于复杂的应用会由多个自定义的组件组成,那不同组件之间数据变化如何进行UI更新呢?比如这么一个场景:

则需要使用@Link和@State这两个装饰器配合使用来实现此功能了,所以先了解一下@Link:“装饰的变量可以和父组件的@State变量建立双向数据绑定,需要注意的是:@Link变量不能在组件内部进行初始化。”

下面来看一下此场景的功能实现代码细节:

1、TitleComponent:

先在头部组件中使用@Link定义一个是否需要刷新的变量,并且它受点击事件的影响:

由于装饰的变量可以和父组件的@State变量建立双向数据绑定【那双向数据绑定的样式是啥样呢?在下面的代码中会有体现的】,那么任何一方所做的修改都会反映给另一方,对isRefreshData的更改将同步修改父组件对应的@State变量,从而自动触发父组件的UI刷新,

2、RankPage:

接下来就可以回到父组件定义三个@State变量:

其中在创建子组件时,先对其成员变量进行一个初始化:

由于isRefreshData是使用@Link在子组件中进行修饰的,所以在父组件中必须使用引用来进行初始化:

通过这种方式就创建了子组件和父组件之间的双向数据绑定了,当子组件的isRefreshData值变化时,其父组件的isSwitchDataSource值也会跟着进行变化,这样在构建列表时就可以根据这个isSwitchDataSource变量来切换不同的数据源了:

其中核心是要理解“数据的双向绑定”。

总结:

最后关于ArkTS基础知识还有个练习:

这块下次继续,重学鸿蒙,感受最大的就是语言已经彻底抛弃Java,让你已经找不到Android那个Activity的身影了,如果不提前学到时等明年鸿蒙5彻底不兼容APK时,身为Android程序可能会比较被动,因为哪个公司都不会放弃华为这么大的一个用户群体,那很显然地都需要将公司的APP来适配鸿蒙,所以趁还有时间及早学习它是很有必要的。另外整个学习由于官方教程已经弄得非常人性了,所以学习起来也比较流畅,当然前提是一步步一个脚印的来进行学习,欲速则不达。

另外对于Android程序还有一个“好消息”:昨天雷布斯在微博上发了个这。。

“Xiaomi HyperOS”,擦,未来Android程序员真的酸爽,以后是不是还会有OPPO OS、魅族 OS... 怎么说呢,是挑战也是机遇,市场有需求才有你立足的根本,拥抱变化~~

posted on 2023-10-18 13:02  cexo  阅读(1227)  评论(0编辑  收藏  举报

导航