2
头图

在移动应用开发领域,Flutter和HarmonyOS Next是两个备受瞩目的创新技术,它们分别代表了Google和华为对于跨平台开发的独特理解和实践。

从一个Flutter开发者的角度,对比两个平台的区别,相信能帮助Flutter开发人员快速入门HarmonyOS Next开发。

一、定位

Flutter:Google的跨平台UI工具包

Flutter是由Google推出的一款开源UI软件开发工具包,专注于为移动、Web及桌面端提供统一的开发体验。其核心技术包括Dart编程语言和Skia图形渲染引擎,允许开发者使用一套代码库同时构建iOS和Android应用,实现“一次编写,处处运行”。

HarmonyOS Next:华为的全场景分布式操作系统

HarmonyOS Next则是华为推出的面向万物互联时代的全场景分布式操作系统,它不仅支持手机,还覆盖穿戴设备、电视、车机等多种智能终端。

Flutter更侧重于提供一致高效的跨平台UI开发体验,而HarmonyOS Next则致力于打破设备壁垒,打造全场景生态下的分布式应用开发环境。两者虽有显著区别,但都都是基于“现代”编程语言实现的框架,开发体验优秀,能够帮助开发人员快速开发出漂亮的应用。

二、 Flutter与Ark UI的对比

尽管Flutter和HarmonyOS Next在设计理念和侧重点上有所不同,但它们都在不同程度上借鉴了React的思想,让我们快速对比一下他们的异同,能够帮助Flutter开发者在几分钟快速写出一个简单的HarmonyOS的应用。

IDE

Flutter主要推荐使用Google自家的Android Studio或Visual Studio Code作为开发环境。而HarmonyOS使用HUAWEI DevEco Studio开发,最新的版本也是powered by idea,和AS可以说是同根同源了。在IDE这部分,目前来说二者不分伯仲。但是随着AI辅助编程的加入(比如copilot),用户生态产生的数据的多少,可能会左右AI插件的完善度,从而对开发效率产生较大的影响。

Dart vs ArkTs 

TSFlutter采用Dart作为开发语言,以其高效的JIT/AOT编译机制和简洁易读的语法受到开发者喜爱;而HarmonyOS Next的Ark UI Toolkit则基于TypeScript,具备强类型检查和良好的跨平台特性,便于开发者编写可扩展性强且易于维护的代码。虽然二者语法结构存在差异,但都有良好的开发工具支持和活跃的社区资源,Flutter开发者可以通过学习Ark TS的基本语法和API,逐渐适应HarmonyOS的开发模式。

对比一下同样一段UI的代码实现。

ArkTs的代码

Column({ space: 20 }) {
  Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%')
  Row().width('90%').height(50).backgroundColor(0xF5DEB3)
  Row().width('90%').height(50).backgroundColor(0xD2B48C)
  Row().width('90%').height(50).backgroundColor(0xF5DEB3)
}.width('100%')

Dart的代码

SizedBox(
        width: double.infinity,
        child: Column(
          children: [
            SizedBox(
              height: MediaQuery.of(context).size.width * .9,
              child: Text("space: 20",
                  style: TextStyle(color: Colors.grey, fontSize: 15)),
            ),
            Container(
                color: Color(0xF5DEB3), width: double.infinity, height: 50),
            Container(
                color: Color(0xD2B48C), width: double.infinity, height: 50),
            Container(
                color: Color(0xF5DEB3), width: double.infinity, height: 50),
          ],
        ))

TSX的代码

<Column style={{width: '100%'}}>
  <Text style={{width: '90%', fontSize:15, textColor: 'grey'}}>space: 20</Text>
  <Row style={{height:50, backgroundColor: 0xF5DEB3}}></Row>
  <Row style={{height:50, backgroundColor: 0xF5DEB3}}></Row>
  <Row style={{height:50, backgroundColor: 0xF5DEB3}}></Row>
</Column>

如果有了解过android compose,你会发现ark UI的写法和它非常相似。而对比Flutter原本的写法,它显然要简洁的多,甚至对比tsx\jsx的写法 也非常简洁明了。

UI布局

在布局部分Ark UI和Flutter的区别非常大,Ark UI更近似于web开发,而Flutter更接近安卓。

Flutter遵循3条原则:

- 上层 widget 向下层 widget 传递约束条件, 

  • 下层 widget 向上层 widget 传递大小信息, 
  • 上层 widget 决定下层 widget 的位置。

Ark UI的布局和WPF的非常相似,并且多了一些独特的布局(比如RelativeContainer),对于Flutter开发者来说或许需要相对更多的学习成本。

我制作了一个非常简单的对照表格,方便Flutter开发者快速对照使用。

image.png

总体来说Ark UI的提供的布局已经能够满足我90%以上的应用开发需求,而剩余的10%,我相信只要Ark UI开放更多底层API,可以非常容易的的实现。

状态管理

响应式UI避不开的就是状态管理,Ark UI有一点让人非常开心的是,它已经帮我们选择好了最佳方案,直接查看官方文档就可以快速上手使用。

image.png

Flutter

  • getx, riverpod, bloc, provider, setState.... 各种各样,千奇百怪,令人头晕😵‍

💫Ark UI

  • AppStorage:全局状态的中枢
  • LocalStorage:页面级的数据共享

相对于前端生态的百花齐放(混乱)状态管理库,ark ui直接给出了终极答案:页面状态Localstorage+全局单例状态Appstorage的方案。并且官方给出的文档非常简洁。

有一点特别值得提的是装饰器的使用。由于语言特性的限制,dart的装饰器,通常需要搭配生成器来使用。比如作者常用的isar,在定义完model以后,并不能直接使用,还需要使用build_runner来生成代码。

@collection
class Storage {
  Id id = Isar.autoIncrement;

  @Index(unique: true, replace: true)
  late String key;

  late String value;
}

而Ark TS由于TS的语言属性,能够非常快的实现一个普通变量变成一个状态。

// for simple type
@State count: number = 0;
// value changing can be observed
this.count = 1;

以上的特性非常类似于dart 中的rxdart

final count = 0.obs;
count.value = 1;

路由管理

Flutter 使用的是 Flutter Navigator,它提供了一种灵活且可定制的方式来管理页面导航。开发者可以通过定义路由规则和页面组件,轻松实现页面之间的跳转和传参。

HarmonyOS Next 则采用了华为自己的路由管理系统,它与 HarmonyOS 的分布式特性紧密结合,允许开发者在不同设备和场景中进行页面导航。

image.png

HarmonyOS的路由调用和小程序的非常相似,相对于Flutter路由调用方式更加格式化。

router.pushUrl({
  url: 'pages/routerpage2',
  params: {
    data1: 'message',
    data2: {
      data3: [123, 456, 789]
    }
  }
}, (err) => {
  if (err) {
    console.error(`pushUrl failed, code is ${err.code}, message is ${err.message}`);
    return;
  }
  console.info('pushUrl success');
});

特别需要说明的是arkui的弹窗逻辑和Flutter有比较大的区别,Flutter里面的modal、dialog、bottomsheet实际上是一个路由页面,通过路由的open、pop来进行打开。而arkui则是通过定义的controller来进行控制。

@CustomDialog
struct CustomDialogExample {
  controller: CustomDialogController
  build() {
    Column() {
      Text('我是内容')
      .fontSize(20)
      .margin({ top: 10, bottom: 10 })
    }
  }
}
dialogController: CustomDialogController = new CustomDialogController({
    builder: CustomDialogExample({}),
})
Flex({justifyContent:FlexAlign.Center}){
  Button('click me')
    .onClick(() => {
      this.dialogController.open()
    })
}.width('100%')

总的来说,Flutter的路由系统相对较复杂,系统也提供了更底层,更细致的颗粒度控制。而ArkUI对路由进行了封装,更容易调用,但是同样的类似于bottomsheetdialog等需要自己进行封装。

动画

在Flutter中,动画类型主要可以分为两类:一类是状态机动画(StatefulAnimation),另一类是帧动画(Frame-Based Animation)。

状态机动画

这种动画依赖于动画的开始、结束状态,以及动画的曲线(如线性、缓进等)来进行。在Flutter中,状态机动画主要通过AnimationControllerCurvedAnimation来实现。

AnimationController controller;
CurvedAnimation curve;
 
@override
void initState() {
  super.initState();
  controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  controller.forward();
}
 
@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: curve,
    builder: (BuildContext context, Widget child) {
      return Container(
        height: 100.0,
        width: 100.0,
        color: Color.lerp(Colors.red, Colors.blue, curve.value),
      );
    },
  );
}

帧动画

这种动画通过一系列的图片来实现,类似于GIF效果。在Flutter中,帧动画主要通过AnimationControllerTween来实现。

List<Image> _frames;
AnimationController controller;
Animation<int> animation;
 
@override
void initState() {
  super.initState();
  _frames = [...]; // 加载你的图片
  controller = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,
  )..repeat(); // 动画重复播放
  animation = IntTween(begin: 0, end: _frames.length - 1).animate(controller);
}
 
@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: animation,
    builder: (BuildContext context, Widget child) {
      return _frames[animation.value];
    },
  );
}

Ark UI通过frame的方式实现动画,官方文档给出的例子:

import animator from '@ohos.animator';

@Entry
@Component
struct AnimatorTest {
  private TAG: string = '[AnimatorTest]'
  private backAnimator: any = undefined
  private flag: boolean = false
  @State wid: number = 100
  @State hei: number = 100

  create() {
    let _this = this
    this.backAnimator = animator.create({
      duration: 2000,
      easing: "ease",
      delay: 0,
      fill: "forwards",
      direction: "normal",
      iterations: 1,
      begin: 100,
      end: 200
    })
    this.backAnimator.onfinish = function () {
      _this.flag = true
      console.info(_this.TAG, 'backAnimator onfinish')
    }
    this.backAnimator.onrepeat = function () {
      console.info(_this.TAG, 'backAnimator repeat')
    }
    this.backAnimator.oncancel = function () {
      console.info(_this.TAG, 'backAnimator cancel')
    }
    this.backAnimator.onframe = function (value) {
      _this.wid = value
      _this.hei = value
    }
  }

  aboutToDisappear() {
    // 由于backAnimator在onframe中引用了this, this中保存了backAnimator,
    // 在自定义组件消失时应该将保存在组件中的backAnimator置空,避免内存泄漏
    this.backAnimator = undefined;
  }

  build() {
    Column() {
      Column() {
        Column()
          .width(this.wid)
          .height(this.hei)
          .backgroundColor(Color.Red)
      }
      .width('100%')
      .height(300)

      Column() {
        Row() {
          Button('create')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              this.create()
            })
        }
        .padding(10)

        Row() {
          Button('play')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              this.flag = false
              this.backAnimator.play()
            })
        }
        .padding(10)

        Row() {
          Button('pause')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              this.backAnimator.pause()
            })
        }
        .padding(10)

        Row() {
          Button('finish')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              this.flag = true
              this.backAnimator.finish()
            })
        }
        .padding(10)

        Row() {
          Button('reverse')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              this.flag = false
              this.backAnimator.reverse()
            })
        }
        .padding(10)

        Row() {
          Button('cancel')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              this.backAnimator.cancel()
            })
        }
        .padding(10)

        Row() {
          Button('reset')
            .fontSize(30)
            .fontColor(Color.Black)
            .onClick(() => {
              if (this.flag) {
                this.flag = false
                if(this.backAnimator){
                  this.backAnimator.reset({
                    duration: 3000,
                    easing: "ease-in",
                    delay: 0,
                    fill: "forwards",
                    direction: "alternate",
                    iterations: 3,
                    begin: 100,
                    end: 300
                  })
                }
              } else {
                console.info(this.TAG, 'Animation not ended')
              }
            })
        }
        .padding(10)
      }
    }
  }
}

1. 首先需要定义页面内的动画变量

@State wid: number = 100
@State hei: number = 100

2. 然后通过animator创建动画控制器

this.backAnimator = animator.create({
      duration: 2000,
      easing: "ease",
      delay: 0,
      fill: "forwards",
      direction: "normal",
      iterations: 1,
      begin: 100,
      end: 200
    })

3. 接着定义好的frame中间值

this.backAnimator.onframe = function (value) {
      _this.wid = value
      _this.hei = value
    }

4. 最后播放动画即可

this.backAnimator.play() //播放
this.backAnimator.pause() //暂停
this.backAnimator.finish()  //结束

Ark UI的动画使用和react native比较相似,使用比较简单。但是对比react native缺少复杂动画的支持,比如组合动画、合成动画值等,复杂的动态差值还需要自己手搓,实现成本高。Fluuter开发者通过阅读文档 ,相信可以非常快速的使用ark ui的动画,而更多复杂动画的实现 还需要未来官方的支持。

后台任务background task

相比较于Flutter的后台服务是依赖于系统运行环境的,比如安卓、ios和web的不同,导致没有一个统一完整的接口,需要引入不同的第三方包。HarmonyOS由官方提供了统一的方法:

1. 短时任务

适用于实时性要求高、耗时不长的任务,例如状态保存。

image.png

import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import { BusinessError } from '@ohos.base';


let id: number;         // 申请短时任务ID
let delayTime: number;  // 本次申请短时任务的剩余时间

// 申请短时任务
function requestSuspendDelay() {
  let myReason = 'test requestSuspendDelay';   // 申请原因。每调用一次申请接口就会有一个新id,短时任务的数量会增加一个  
  let delayInfo = backgroundTaskManager.requestSuspendDelay(myReason, () => {
    // 回调函数。应用申请的短时任务即将超时,通过此函数回调应用,执行一些清理和标注工作,并取消短时任务
    console.info('suspend delay task will timeout');
    backgroundTaskManager.cancelSuspendDelay(id);
  })
  id = delayInfo.requestId;
  delayTime = delayInfo.actualDelayTime;
}

let id: number; // 申请短时任务ID

async function getRemainingDelayTime() {
  backgroundTaskManager.getRemainingDelayTime(id).then((res: number) => {
    console.info('Succeeded in getting remaining delay time.');
  }).catch((err: BusinessError) => {
    console.error(`Failed to get remaining delay time. Code: ${err.code}, message: ${err.message}`);
  })
}

let id: number; // 申请短时任务ID

function cancelSuspendDelay() {
  backgroundTaskManager.cancelSuspendDelay(id);
}

2. 长时任务

适用于长时间运行在后台、用户可感知的任务,例如后台播放音乐、导航、设备连接等,使用长时任务避免应用进程被挂起。

  1. 需要申请ohos.permission.KEEP_BACKGROUND_RUNNING权限,配置方式请参见配置文件权限声明
  2. 声明后台模式类型。

在module.json5配置文件中为需要使用长时任务的UIAbility声明相应的长时任务类型(配置文件中填写长时任务类型的配置项)。

"module": {
     "abilities": [
         {
             "backgroundModes": [
             // 长时任务类型的配置项
             "audioRecording"
             ], 
         }
     ],
     ...
 } @Entry
 @Component
 struct Index {
   @State message: string = 'ContinuousTask';
  // 通过getContext方法,来获取page所在的UIAbility上下文。
   private context: Context = getContext(this);

   startContinuousTask() {
     let wantAgentInfo: wantAgent.WantAgentInfo = {
       // 点击通知后,将要执行的动作列表
       // 添加需要被拉起应用的bundleName和abilityName
       wants: [
         {
           bundleName: "com.example.myapplication",
           abilityName: "com.example.myapplication.MainAbility"
         }
       ],
       // 指定点击通知栏消息后的动作是拉起ability
       operationType: wantAgent.OperationType.START_ABILITY,
       // 使用者自定义的一个私有值
       requestCode: 0,
       // 点击通知后,动作执行属性
       wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
     };

     // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
     wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
        backgroundTaskManager.startBackgroundRunning(this.context,
          backgroundTaskManager.BackgroundMode.AUDIO_RECORDING, wantAgentObj).then(() => {
          console.info(`Succeeded in operationing startBackgroundRunning.`);
        }).catch((err: BusinessError) => {
          console.error(`Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
        });
     });
   }

   stopContinuousTask() {
      backgroundTaskManager.stopBackgroundRunning(this.context).then(() => {
        console.info(`Succeeded in operationing stopBackgroundRunning.`);
      }).catch((err: BusinessError) => {
        console.error(`Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
      });
   }

   build() {
     Row() {
       Column() {
         Text("Index")
           .fontSize(50)
           .fontWeight(FontWeight.Bold)

        Button() {
           Text('申请长时任务').fontSize(25).fontWeight(FontWeight.Bold)
         }
         .type(ButtonType.Capsule)
         .margin({ top: 10 })
         .backgroundColor('#0D9FFB')
         .width(250)
         .height(40)
         .onClick(() => {
           // 通过按钮申请长时任务
           this.startContinuousTask();

           // 此处执行具体的长时任务逻辑,如放音等。
         })

         Button() {
           Text('取消长时任务').fontSize(25).fontWeight(FontWeight.Bold)
         }
         .type(ButtonType.Capsule)
         .margin({ top: 10 })
         .backgroundColor('#0D9FFB')
         .width(250)
         .height(40)
         .onClick(() => {
           // 此处结束具体的长时任务的执行

           // 通过按钮取消长时任务
           this.stopContinuousTask();
         })
       }
       .width('100%')
     }
     .height('100%')
   }
 }

特别值得一提的是:HarmonyOS竟然提供了跨设备或跨应用的调用方式。可以想象一下开发可以通过这个方式实现各种穿戴设备和手机应用的互动。比如,常用的比如运动APP时间提醒等。

3. 延迟任务

对于实时性要求不高、可延迟执行的任务,系统提供了延迟任务,即满足条件的应用退至后台后被放入执行队列,系统会根据内存、功耗等统一调度。

image.png

// 创建workinfo
const workInfo: workScheduler.WorkInfo = {
  workId: 1,
  networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,
  bundleName: 'com.example.application',
  abilityName: 'MyWorkSchedulerExtensionAbility'
}

try {
  workScheduler.startWork(workInfo);
  console.info(`startWork success`);
} catch (error) {
  console.error(`startWork failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
}

4. 代理提醒

代理提醒是指应用退后台或进程终止后,系统会代理应用做相应的提醒。适用于定时提醒类业务,当前支持的提醒类型包括倒计时、日历和闹钟三类。

总的来说,由于ts相较于dart缺少isolate等语法上的能力,在异步多任务方面有一些缺失,但是由于深度绑定系统的原因,系统给提供了更多更丰富的后台服务能力,在这方面遥遥领先Flutter。

ArkTS扩展的TS语法

ArktTS扩展了ts的语法,豆包帮我总结了一下它和ts的语法区别:

  1. 装饰器:ArkTS 和 TypeScript 都支持类型注解,但在一些细节上可能有所不同。例如,在 ArkTS 中,类型注解的语法可能会有一些特定的约定或扩展。
  2. 模块系统:ArkTS 和 TypeScript 的模块系统可能存在差异。这可能包括模块的导入和导出方式,以及模块的命名空间和可见性规则等。
  3. 对象字面量:在定义对象时,ArkTS 和 TypeScript 可能在属性访问和方法定义的语法上有所不同。例如,属性初始化的方式或方法签名的表示可能会有差异。
  4. 函数参数:ArkTS 和 TypeScript 对函数参数的类型注解和默认值的处理方式可能有所不同。这可能影响到函数调用和参数传递的语法。
  5. 泛型:如果 ArkTS 支持泛型,那么泛型的语法和用法可能与 TypeScript 略有不同。例如,泛型参数的声明和约束可能会有一些差异。

对于Flutter开发者而言,ts的语法其实更加松散 更少约束。尤其是对json对象的处理方面,对比dart的各种模板语法和代码生成,可以说是非常简单了。而装饰器的使用,更能减少函数式的嵌套地狱。

数据管理和状态管理

非常相似的是HarmonyOS,上来就直接给出了最佳解决方案。

  • 用户首选项(Preferences):提供了轻量级配置数据的持久化能力,并支持订阅数据变化的通知能力。不支持分布式同步,常用于保存应用配置信息、用户偏好设置等。
  • 键值型数据管理(KV-Store):提供了键值型数据库的读写、加密、手动备份能力。分布式功能暂不支持。
  • 关系型数据管理(RelationalStore):提供了关系型数据库的增删改查、加密、手动备份能力。分布式功能暂不支持。
  • 分布式数据对象(DataObject):独立提供对象型结构数据的分布式能力。分布式功能暂不支持。
  • 跨应用数据管理(DataShare):提供了向其他应用共享以及管理其数据的方法。仅系统应用可用,非系统应用无需关注,下文不做具体介绍。

以下表格能非常快的帮助Flutter用户过度到HarmonyOS:

image.png

HarmonyOS还有非常多和Flutter不同的地方,并且有意思的地方,更多的信息可以登录HarmonyOS的官方网站查看详情。而通过这篇文章,我相信Flutter开发人员能够非常有信心的杀入到HarmonyOS开发的蓝海市场里去了。


geeeeek
6 声望0 粉丝