少湖说 | 科技自媒体

互联网,科技,数码,鸿蒙

环境搭建

1. 下载Flutter SDK,配置环境变量

鸿蒙 Flutter SDK 需要在 Gitee 下载。目前建议下载 dev 分支代码。

需要配置以下用户变量

注意鸿蒙开发需要安装Java和配置相关变量

1
2
3
4
5
6
7
8
# flutter sdk 镜像
FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
# pub 镜像
PUB_HOSTED_URL=https://pub.flutter-io.cn

DEVECO_SDK_HOME=C:\Program Files\Huawei\DevEco Studio\sdk
# Java SDK
JAVA_HOME=C:\Program Files\Huawei\DevEco Studio\jbr

配置环境变量

编辑 PATH,添加以下路径,鸿蒙开发需要配置ohpm, hvigor及node

1
2
3
4
5
6
7
C:\Program Files\Huawei\DevEco Studio\tools\ohpm\bin

C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin

C:\Program Files\Huawei\DevEco Studio\tools\node

C:\Program Files\Huawei\DevEco Studio\jbr\bin

SDK 下载完成,环境变量配置妥当后,使用 flutter doctor 检查各项是否通过。

在命令行中,运行 ohpm -v, hvigorw -v, node -v 查看是否能使用,确保各个依赖的工具,其 PATH 配置正确。

使用 echo %DEVECO_SDK_HOME%, echo %JAVA_HOME% 等检查用户变量是否生效。

环境变量发生变化时,需要重启命令行工具。

另外,需要注意的是,优先添加用户环境变量,如果是系统环境变量,可能需要注销登录或者重启系统,否则配置可能不生效。

2. 为了避免意外情况,将新建项目位置,与SDK使用相同的磁盘,如D盘。

否则可能出现 package 找不到的情况。

另外,项目目录不要过深,不然会因路径太长导致编译可能失败。

3. VsCode 无法识别设备

用 DevEco 打开项目,待项目分析完成后,Vscode 中的设备应该可以出来了。

4. 插件Har包找不到

打开DevEco运行时,出现类似以下错误

1
2
3
4
5
6
7
8
9
10
11
12
13
hpm ERROR: missing: flutter_inappwebview_ohos@/.../ohos/har/flutter_inappwebview_ohos.har, required by entry@1.0.0
ohpm ERROR: Found exception: Error: Fetch local file package error, /.../ohos/har/flutter_inappwebview_ohos.har does not exist., reached retry limit or non retryable error encountered.
ohpm ERROR: missing: video_player_ohos@/.../ohos/har/video_player_ohos.har, required by entry@1.0.0
ohpm ERROR: Found exception: Error: Fetch local file package error, /.../ohos/har/video_player_ohos.har does not exist., reached retry limit or non retryable error encountered.
ohpm ERROR: missing: path_provider_ohos@/.../ohos/har/path_provider_ohos.har, required by entry@1.0.0
ohpm ERROR: Found exception: Error: Fetch local file package error, /.../ohos/har/path_provider_ohos.har does not exist., reached retry limit or non retryable error encountered.
ohpm ERROR: missing: shared_preferences_ohos@/.../ohos/har/shared_preferences_ohos.har, required by entry@1.0.0
ohpm ERROR: Found exception: Error: Fetch local file package error, /.../ohos/har/shared_preferences_ohos.har does not exist., reached retry limit or non retryable error encountered.
ohpm ERROR: missing: image_picker_ohos@/.../ohos/har/image_picker_ohos.har, required by entry@1.0.0
ohpm ERROR: Found exception: Error: Fetch local file package error, /.../ohos/har/image_picker_ohos.har does not exist., reached retry limit or non retryable error encountered.
ohpm ERROR: missing: @ohos/flutter_ohos@/.../ohos/har/flutter.har, required by @
ohpm ERROR: Found exception: Error: Fetch local file package error, /.../ohos/har/flutter.har does not exist., reached retry limit or non retryable error encountered.
ohpm ERROR: Install failed, detail: Error: Fetch local file package error, /.../ohos/har/flutter_inappwebview_ohos.har does not exist.

此时需要在Flutter项目下运行 flutter runflutter build 以生成插件的 har 包

4. 如何自定义显示 DevEco 打开 ohos 后的项目名称

每个鸿蒙Flutter项目,用DevEco打开ohos工程后,默认显示的工程名称为 ohos,如果想自定义显示的工程名称,可以参考以下步骤:

在 ohos/.idea 目录下,新建一个 .name 文件,写入项目名称即可。

主要有两种方案

使用第三方库

如 使用flutter_inappwebview插件,在 pubspec.lock 文件中配置:

1
2
3
4
flutter_inappwebview:
git:
url: https://gitcode.com/openharmony-sig/flutter_inappwebview.git
path: "flutter_inappwebview"

或者使用 webview_flutter 插件

1
2
3
4
webview_flutter:
git:
url: https://gitcode.com/openharmony-sig/flutter_packages.git
path: "packages/webview_flutter/webview_flutter"

编写原生 ArkTS 代码实现 PlatformView

创建 entryablitiy

在 src/main/module.json5中配置ablitiy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
],
"orientation": "landscape"
}
],

cat src/main/entryablity/CustomFactory.ets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { BinaryMessenger } from '@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryMessenger';
import MessageCodec from '@ohos/flutter_ohos/src/main/ets/plugin/common/MessageCodec';
import PlatformViewFactory from '@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewFactory';
import { CustomView } from './CustomView';
import common from '@ohos.app.ability.common';
import PlatformView from '@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView';

export class CustomFactory extends PlatformViewFactory {
message: BinaryMessenger;

constructor(message: BinaryMessenger, createArgsCodes: MessageCodec<Object>) {
super(createArgsCodes);
this.message = message;
}

public create(context: common.Context, viewId: number, args: Object): PlatformView {
return new CustomView(context, viewId, args, this.message);
}
}

cat src/main/entryablity/CustomPlugin.ets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import  { FlutterPlugin,
FlutterPluginBinding } from '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin';
import StandardMessageCodec from '@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMessageCodec';
import { CustomFactory } from './CustomFactory';

export class CustomPlugin implements FlutterPlugin {
getUniqueClassName(): string {
return 'CustomPlugin';
}

onAttachedToEngine(binding: FlutterPluginBinding): void {
binding.getPlatformViewRegistry()?.
registerViewFactory('com.rex.custom.ohos/customView', new CustomFactory(binding.getBinaryMessenger(), StandardMessageCodec.INSTANCE));
}

onDetachedFromEngine(binding: FlutterPluginBinding): void {}
}

cat src/main/entryablity/CustomView.ets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import MethodChannel, {
MethodCallHandler,
MethodResult
} from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel';
import PlatformView, { Params } from '@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView';
import common from '@ohos.app.ability.common';
import { BinaryMessenger } from '@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryMessenger';
import StandardMethodCodec from '@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMethodCodec';
import MethodCall from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall';
import { webview } from '@kit.ArkWeb';

@Component
struct ButtonComponent {
@Prop params: Params
customView: CustomView = this.params.platformView as CustomView
@StorageLink('numValue') storageLink: string = "first"
@State bkColor: Color = Color.Red

private webviewController: WebviewController = new webview.WebviewController()

build() {
Web({src: 'https://baidu.com', controller: this.webviewController})
.domStorageAccess(true)
.fileAccess(true)
.mixedMode(MixedMode.All)
.databaseAccess(true)
.userAgent("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36")
}
}

@Builder
function ButtonBuilder(params: Params) {
ButtonComponent({ params: params })
.backgroundColor(Color.Transparent)
}

AppStorage.setOrCreate('numValue', 'test')

@Observed
export class CustomView extends PlatformView implements MethodCallHandler {
numValue: string = "test";

methodChannel: MethodChannel;
index: number = 1;

constructor(context: common.Context, viewId: number, args: ESObject, message: BinaryMessenger) {
super();
// 注册消息通道
this.methodChannel = new MethodChannel(message, `com.rex.custom.ohos/customView${viewId}`, StandardMethodCodec.INSTANCE);
this.methodChannel.setMethodCallHandler(this);
}

onMethodCall(call: MethodCall, result: MethodResult): void {
// 接受Dart侧发来的消息
let method: string = call.method;
let link1: SubscribedAbstractProperty<number> = AppStorage.link('numValue');
switch (method) {
case 'getMessageFromFlutterView':
let value: ESObject = call.args;
this.numValue = value;
link1.set(value)
console.log("nodeController receive message from dart: " + this.numValue);
result.success(true);
break;
}
}

public sendMessage = () => {
console.log("nodeController sendMessage")
//向Dart侧发送消息
this.methodChannel.invokeMethod('getMessageFromOhosView', 'natvie - ' + this.index++);
}

getView(): WrappedBuilder<[Params]> {
return new WrappedBuilder(ButtonBuilder);
}

dispose(): void {
}
}

cat src/main/entryablity/EntryAbility.ets

1
2
3
4
5
6
7
8
9
10
11
12
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import { CustomPlugin } from './CustomPlugin';

export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
this.addPlugin(new CustomPlugin())
}
}

创建 pages

cat src/main/ets/pages/index.ets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import common from '@ohos.app.ability.common';
import { FlutterPage } from '@ohos/flutter_ohos'

let storage = LocalStorage.getShared()
const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS'

@Entry(storage)
@Component
struct Index {
private context = getContext(this) as common.UIAbilityContext
@LocalStorageLink('viewId') viewId: string = "";

build() {
Column() {
FlutterPage({ viewId: this.viewId })
}
}

onBackPress(): boolean {
this.context.eventHub.emit(EVENT_BACK_PRESS)
return true
}
}

在src/main/resources/base/profile/main_page.json中配置路由

1
2
3
4
5
{
"src": [
"pages/Index"
]
}

在 Dart 侧调用该 PlatformView

1
2
3
4
5
6
7
8
Scaffold(
appBar: AppBar(title: Text('code')),
body: OhosView(
viewType: 'com.rex.custom.ohos/customView',
// onPlatformViewCreated: _onPlatformViewCreated,
creationParams: const <String, dynamic>{'initParams': 'hello world'},
creationParamsCodec: const StandardMessageCodec(),
)

参考资料

在《鸿蒙 Flutter 开发中集成 Webview》,介绍了如果在 Flutter 中集成 Webview. 本文则为 Webview 的调试方法。

配置 Webview

CustomView.ets 文件中,在生命周期aboutToAppear处配置允许调试:

1
2
3
aboutToAppear() {
webview.WebviewController.setWebDebuggingAccess(true);
}

找到 devtools 的端口

运行 App,使用 hdc 命令连接设备,查找相关端口

1
2
3
4
5
6
7
8
# 连接设备
hdc shell

# 找到相关进程
cat /proc/net/unix | grep devtools

#0: 00000002 0 10000 1 1 2318187 @webview_devtools_remote_43406
#0: 00000002 0 10000 1 1 20785 @webview_devtools_remote_6312

如上面所示,webview_devtools_remote_43406 即为我们要调试的页面

开启端口转发

将设备中的端口转发到开发电脑上

1
2
3
hdc fport tcp:9222 localabstract:webview_devtools_remote_43406

# Forwardport result:OK

在 Chrome 中找打 Webview 并开始调试

在 Chrome 中打开 chrome://inspect/#devices页面,观察页面中RemoteTarget 处出现了相关页面

选择需要调度的页面,点击 inspect,弹出 DevTools 窗口,开启页面调度

devtools

其他

如果要在 Webview 注入 js 代码,可在 Web 组件配置处使用runJavaScript方法注入 JavaScript 脚本,如

1
2
3
4
5
6
7
8
9
Web({src: 'https://baidu.com', controller: this.webviewController})
.domStorageAccess(true)
.fileAccess(true)
.mixedMode(MixedMode.All)
.databaseAccess(true)
.javaScriptAccess(true)
.onPageEnd(() => {
this.webviewController.runJavaScript("document.querySelector('meta[http-equiv=\"Content-Security-Policy\"]').remove()");
})

参考资料

在鸿蒙Flutter开发中,如果涉及到使用原生功能,就要使用插件。使用插件有两种方式,一种是自己编写原生ArkTS代码,在Dart侧调用。另外一种是使用第三方代码。

方式一:编写原生 ArkTS 代码

该方案可以使用 PlatformView 或者 MethodChannel 调用。

  1. PlatformView 即为在 Flutter 侧创建一个 View,然后在 Native 侧渲染。PlatformView 封装了底层的 View。

  2. MethodChannel 即通过 MethodClannel 调用原生Native 方法。

具体操作可以分别参考文章 鸿蒙 Flutter 开发中集成 Webview使用 ArkTs 开发 Flutter 鸿蒙平台插件

方式二:使用第三方代码

1.在pub.flutter.dev/github/gitee/ophm查找使用的插件,如果插件已经适配鸿蒙,则可以像其他Flutter插件一样正常使用。

2.如果插件尚未适配鸿蒙,则需要寻找适配的插件库。配置方法如下

3.如果使用的第三方插件,其底层以的库没有适配鸿蒙,则需要通过overrider配置其鸿蒙化的替代插件,否则会在运行时报错。如下面所示:

1
2
3
4
5
6
7
8
9
dependencies:
path_provider: ^2.1.0

dependency_overrides:
# ohos
path_provider:
git:
url: "https://gitcode.com/openharmony-sig/flutter_packages.git"
path: "packages/path_provider/path_provider"

这里需要注意的时,如果不存在依赖冲突,dependency_overrides 可能不生效。也就是说,查看 pubspec.lock 文件,发现依赖的插件库,不存在 **_ohos 库,则说明 overrides不生效,此时使用以下方式,修改 pubspec_overrides.yaml 文件,手动添加文件。

如果 overrides 不生效, 打开 pubspec_overrides.yaml,添加以下内容,再次运行 pub get, 发现 pubspec.lock 成功添加了 **_ohos 库。

1
2
3
4
5
6
dependency_overrides:
# ohos
path_provider:
git:
url: "https://gitcode.com/openharmony-sig/flutter_packages.git"
path: "packages/path_provider/path_provider"

另外,如果没有找到使用的鸿蒙化插件,则可以考虑自行编写垮端调用代码,或者编写新的插件库,作为原插件库的特定平台实现。

参考资料

本文讲述如何开发一个 Flutter 鸿蒙插件,如何实现 Flutter 与鸿蒙的混合开发,以及双端消息通信。

Flutter侧,编写 MethodChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const MethodChannel _methodChannel = MethodChannel('xxx.com/app');


/// 获取token
static Future<dynamic> getToken() {
return _methodChannel.invokeMethod("getPrefs", 'token');
}

/// 设置token
static Future<dynamic> setToken(String token) {
return _methodChannel
.invokeMethod("setPrefs", {'key': 'token', 'value': token});
}

代码生命了一个 methodChannel, 并实现了 token 存错的调用方法。

ArkTs侧,实现调用

编写 src/main/ets/entryability/EntryAbility.ets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import ForestPlugin from './ForestPlugin';
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';
import { preferences } from '@kit.ArkData';

let dataPreferences: preferences.Preferences | null = null;

export default class EntryAbility extends FlutterAbility {

onWindowStageCreate(windowStage: window.WindowStage): void {
super.onWindowStageCreate(windowStage);
preferences.getPreferences(this.context, 'forestStore', (err: BusinessError, val: preferences.Preferences) => {
if (err) {
console.error("Failed to get preferences. code =" + err.code + ", message =" + err.message);
return;
}
dataPreferences = val;
console.info("Succeeded in getting preferences1.");
})
}

configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
this.addPlugin(new ForestPlugin());
}
}

export {dataPreferences};

该文件使的原生页面在加载时,配置 Flutter 引擎,注册插件。 Flutter初始化时,同时初始化了 首选项dataPreferences,以备后用。

编写 src/main/ets/entryability/ForestPlugin.ets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { Any, BasicMessageChannel, EventChannel, FlutterManager, FlutterPlugin, Log, MethodCall, MethodChannel, StandardMessageCodec} from '@ohos/flutter_ohos';
import { FlutterPluginBinding } from '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin';
import { batteryInfo } from '@kit.BasicServicesKit';
import { MethodCallHandler, MethodResult } from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel';
import { preferences } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import {dataPreferences} from './EntryAbility';
import router from '@ohos.router'
import { webviewRouterParams } from '../pages/Webview';

const TAG = "[flutter][plugin][forest]";

export default class ForestPlugin implements FlutterPlugin {
private channel?: MethodChannel;
private basicChannel?: BasicMessageChannel<Any>;
private api = new ForestApi();
private dataPreferences: preferences.Preferences | null = null;

onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "xxx.com/app");
this.channel.setMethodCallHandler({
onMethodCall : (call: MethodCall, result: MethodResult) => {
console.log(`${TAG}-->[${call.method}]: ${JSON.stringify(call.args)}`);
switch (call.method) {
case "getPrefs":
this.api.getPrefs(String(call.args), result);
break;
case "setPrefs":
let key = String(call.argument("key"));
let value = String(call.argument("value"));
this.api.setPrefs(key, value);
default:
result.notImplemented();
break;
}
}
})
}
//···
onDetachedFromEngine(binding: FlutterPluginBinding): void {
Log.i(TAG, "onDetachedFromEngine");
this.channel?.setMethodCallHandler(null);
}

getUniqueClassName(): string {
return "ForestPlugin";
}

以上代码实现了一个插件类,其核心实现了FlutterPlugin中的 onAttachedToEngine方法,该方法在 Flutter 引擎加载成功后调用。

onMethodCall中接收来自 Flutter 的消息调用,分别实现了 ‘getPrefs’ 和 ‘setPrefs’ 两个回掉,其中 getPrefs有返回值,通过 result.success(val);(见下)异步返回,
setPrefs没有返回值。

以下为 ForestApi的具体实现,使用了 HarmonyOS 中的首选项 API 设置和读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ForestApi {
getPrefs(key: string, result: MethodResult) {
dataPreferences?.get(key, '', (err: BusinessError, val: preferences.ValueType) => {
if (err) {
console.error(`${TAG} Failed to get value of ${key}. code =` + err.code + ", message =" + err.message);
result.success('');
}
console.info(`${TAG} Succeeded in getting value of ${key}:${val}.`);
result.success(val);
})

}

setPrefs(key: string, value: string) {
dataPreferences?.put(key, value, (err: BusinessError) => {
if (err) {
console.error(`${TAG} Failed to put value of ${key}. code =` + err.code + ", message =" + err.message);
return;
}
console.info(`${TAG} Succeeded in putting value of ${key}.`);
})
}

clearPrefs(key: string) {
dataPreferences?.delete(key, (err: BusinessError) => {
if (err) {
console.error("Failed to delete the key 'startup'. code =" + err.code + ", message =" + err.message);
return;
}
console.info(`Succeeded in deleting the key ${key}.`);
})
}
}

注意事项

1.双端初始化methodChannel中的名称必须保持一致,如 xxx.com/app.
2.arkTS侧通过 result.success(val) 返回数据,该过程是异步的,故在 Dart 侧需要使用 await 或者回调函数取值。
3.通信中默认只支持基础的数据类型,复杂类型的需要进行序列化或编解码。
4.在Dart 侧接收的数据为 dymanic 类型,需要进行数据类型转换。

参考资料

鸿蒙Flutter混合开发主要有两种形式。

1.基于har

将flutter module打包成har包,在原生鸿蒙项目中,以har包的方式引入。

其优点是主项目开发者可以不关注Flutter实现,不需要安装配置Flutter开发环境,缺点是无法及时修改Flutter代码,也不存在热重载。

2.基于源码

通过源码依赖的当时,在原生鸿蒙项目处,引入Flutter模块。

其优点是方便维护和更新Flutter代码,也可以使用热重载。缺点是需要搭建Flutter开发环境,开发人员需要掌握Flutter。

其项目结构类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.
├── AppScope
│   ├── app.json5
│   └── resources
│   ├── base
│   └── rawfile
├── build-profile.json5
├── dependencies
│   ├── hvigor-4.1.1.tgz
│   ├── hvigor-ohos-arkui-x-plugin-3.1.0.tgz
│   └── hvigor-ohos-plugin-4.1.1.tgz
├── entry
│   ├── build-profile.json5
│   ├── hvigorfile.ts
│   ├── oh-package.json5
│   └── src
│   └── main
├── flutter_module
│   ├── BuildProfile.ets
│   ├── Index.ets
│   ├── build-profile.json5
│   ├── consumer-rules.txt
│   ├── hvigorfile.ts
│   ├── libs
│   │   └── arm64-v8a
│   ├── obfuscation-rules.txt
│   ├── oh-package.json5
│   └── src
│   ├── main
│   ├── ohosTest
│   └── test
├── har
│   ├── GT-HM-1.0.4.har
│   ├── flutter.har
│   ├── flutter_boost.har
│   ├── flutter_module.har
│   └── lib_base.har
├── hvigor
│   └── hvigor-config.json5
├── hvigorfile.ts
├── local.properties
├── oh-package.json5
├── package-lock.json
└── package.json

参考资料

1.环境搭建

参考文章鸿蒙Flutter实战:01-搭建开发环境搭建好开发环境。IDE 安装好 DevEco 和 VsCode/Android Studio。

2.配置

如果是 vscode, 可以在 .vscode/launch.json 文件中,增加以下配置

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "ohos-app (attach mode)",
"cwd": "packages/apps/ohos_app",
"request": "attach",
"type": "dart",
},
{
"name": "ohos_app",
"cwd": "packages/apps/ohos_app",
"request": "launch",
"type": "dart"
},

添加成功后,会在运行和调度的 Tab 栏目中,出现启动的选项。这里添加了两个配置,一个是 Attach 模式,一个是普通的运行模式。

3.查看日志

查看日志,可以在运行Flutter处的IDE调试控制台查看 Flutter 项目日志,可以使用 hdc hilog 命令或DevEco 查看系统日志。

4.调试 Flutter

主要有两种调试方案。

方案一

在IDE 中直接运行 Flutter 项目,IDE 可选择 Andriod Studio 或者 VsCode,在调试栏点击 Debug 运行。

方案二

适应DecEco运行鸿蒙项目,注意需要打开的是ohos鸿蒙目录代码,待IDE分析结束后,点击运行。

当app在鸿蒙设备上启动成功后,立即在 Vscode 中调出 Command Pallet,找到 Flutter Attach ,将 Flutter 调试器连接至宿主机

然后就是增加断点,使用hot reload 重新加载 Flutter,调试项目代码。

调试 ArkTs

需要使用 DevEcho 打开项目,点击运行旁边的 Debug Entry 按钮,开始程序调试。

调试 Webview

参考文章 鸿蒙Flutter实战:04-如何使用DevTools调试Webview进行 Webview 调试。

背景

原来使用Flutter开发的项目,需要适配鸿蒙。

环境搭建

见文章[鸿蒙Flutter适配指南],搭建开发环境,使用fvm管理多版本SDK。

模块化

原有项目保持模块化,拆分为 apps/common/components/modules/plugins等目录,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.
├── README.md
├── analysis_options.yaml
├── melos.yaml
├── melos_ogw-flutter.iml
├── node_modules
├── packages
│ ├── README.md
│ ├── apps
│ │ ├── app
│ │ ├── dsm_app
│ │ ├── ohos_app
│ │ └── web
│ ├── common
│ │ ├── domains
│ │ ├── extensions
│ │ ├── services
│ │ └── widgets
│ ├── components
│ │ ├── image_uploader
│ │ ├── player
│ │ └── scroll_banner
│ ├── modules
│ │ ├── address
│ │ ├── community
│ │ ├── home
│ │ ├── invoice
│ │ ├── me
│ │ ├── message
│ │ ├── order
│ │ ├── shop
│ │ ├── support
│ │ ├── updater
│ └── plugins
│ ├── image_picker
│ ├── printer
├── pubspec.lock
├── pubspec.yaml
└── yarn.lock
  1. plugins 是依赖于原生平台的插件,

  2. components 是平台无关的组件,

  3. common 里面是领域对象,小组件,服务类,扩展等,平台无关,里面均为纯 Dart 代码。

  4. apps 是应用外壳,通过组合不同的模块,向不同的平台打包。

  5. 使用 melos 管理多包仓库。

其中apps下的项目,则为需要打包成各平台,各app的入口项目。里面主要为项目配置代码,模块依赖配置,以及特定的平台适配代码。

在apps目录下新建鸿蒙项目,先把壳项目在鸿蒙中跑起来,确保没有问题。依次再添加依赖项,首先添加纯dart编写的包,再添加依赖于原生代码/插件的包。注意挨个添加依赖,不要一次添加太多依赖,方便排查定位问题,

解决版本依赖问题,鸿蒙 Flutter 项目目前需要依赖于3.7版本,如果原项目使用了更低的版本,则可将原项目SDK依赖升级至3.7;如果原项目SDK版本高于3.7,则有两种方案:一种是降级原项目SDK依赖为3.7;另外一种是使用多分支方案。

如果需要使用 Flutter 3.22 版本,参见文章 鸿蒙Flutter实战:11-使用 Flutter SDK 3.22.0

特定平台工程

在 apps 目录下新建一个项目,该项目运行鸿蒙平台适配和打包。

1
flutter create --platforms ohos ohos_app

目录结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.
├── README.md
├── analysis_options.yaml
├── assets
│ ├── icons
│ │ ├── 2.0x
│ │ ├── 3.0x
│ │ └── placeholder.png
│ └── images
│ ├── 2.0x
│ └── 3.0x
├── build
│ ├── ...
├── env
├── lib
│ ├── config
│ │ ├── easy_refresh.dart
│ │ ├── routes.dart
│ │ └── theme.dart
│ └── main.dart
├── ohos
│ ├── AppScope
│ │ ├── app.json5
│ │ └── resources
│ ├── build-profile.json5
│ ├── entry
│ │ ├── build
│ │ ├── build-profile.json5
│ │ ├── hvigorfile.ts
│ │ ├── oh-package-lock.json5
│ │ ├── oh-package.json5
│ │ ├── oh_modules
│ │ └── src
│ ├── har
│ │ ├── ...
│ ├── hvigor
│ │ └── hvigor-config.json5
│ ├── hvigorfile.ts
│ ├── local.properties
│ ├── oh-package-lock.json5
│ ├── oh-package.json5
│ └── oh_modules
│ └── ...
├── pubspec.lock
└── pubspec.yaml

可以看到,该项目只是一个壳工程,没有太多代码,主要为项目的一些特定配置,如主题、路由等,以及App入口初始化配置。

编辑 pubspec.yaml 文件,添加组件和模块依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
environment:
sdk: '>=2.19.6 <3.0.0'
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# 下拉刷新
easy_refresh: ^3.0.4+2
flutter_dotenv: ^5.1.0
go_router: ^6.0.0

# 领域对象
domains:
path: '../../common/domains'
# 通用服务类
services:
path: '../../common/services'
# 纯 Dart UI 组件
widgets:
path: '../../common/widgets'
# 模块: 收货地址
address:
path: '../../modules/address'
# 模块: 帮助中心
support:
path: '../../modules/support'
# 模块:个人中心
me:
path: '../../modules/me'
# 模块:消息通知
message:
path: '../../modules/message'
# 模块:订单
order:
path: '../../modules/order'
# 模块:商城
shop:
path: '../../modules/shop'
# 模块:首页
home:
path: '../../modules/home'

插件鸿蒙化适配

部分第三方插件以及插件依赖的其他库,如果没有适配鸿蒙,则可以通过 override配置鸿蒙化的版本

1
2
3
4
5
6
dependency_overrides:
# ohos
path_provider:
git:
url: "https://gitcode.com/openharmony-sig/flutter_packages.git"
path: "packages/path_provider/path_provider"

编译运行

运行 Flutter 项目,查看相关日志和运行界面,针对出现的问题再单独处理。

查看日志,可以在运行 Flutter 处的 IDE 调试控制台查看 Flutter 项目日志,可以使用 hdc hilog 命令或 DevEco 查看系统日志。

1. 学习路径应该是怎样的,需要掌握哪些技术才具备鸿蒙 Flutter 开发能力

1.1 学习和掌握 Flutter 开发技术,这块需要在Flutter社区学历 Flutter开发文档
1.2 学习鸿蒙基础概念和知识,推荐学习 鸿蒙生态应用开发白皮书, ArkTS 语言, ArkUI,
HarmonyOS 第一课

2. MatePad 应用适配问题

如果出现 app 在 Matepad 上无法全屏的问题,需要在 ohos/entry/main/module.json5中配置设备类型:

1
2
3
4
5
6
7
"deviceTypes": [
"phone",
"tablet",
"car",
"2in1",
'default'
],

需要增加 tablet 平板设备的适配。

如果在 Matepad 上运行时设备没有全屏,则可以需要删除 App 重装安装或者重启设备。因为相关的配置存在缓存,适配类型发生变化时,存在没有更新的问题,导致无法全屏。

3. 模拟器

模拟器与真机有较大差异,如果出现模拟器异常情况,优先确实真机是否正常运行,以排除模拟器自身问题。

4. debug 版本运行报错

Error while initializing the Dart VM

1
2
3
4
5
依次执行以下操作
设置环境变量 export FLUTTER_STORAGE_BASE_URL=https://flutter-ohos.obs.cn-south-1.myhuaweicloud.com
删除 /bin/cache 目录下的缓存
执行 flutter clean,清除项目编译缓存
运行 flutter run -d $DEVICE --debug

5. 如何更换 App 图标和名称

找到 ohos/AppScope/resources/base/media/app_icon.png,替换相应的文件

找到 ohos/AppScope/resources/base/element/string.json 文件,修改以下配置

1
2
3
4
5
6
7
8
{
"string": [
{
"name": "app_name",
"value": "中文名称"
}
]
}

6. flutter run 运行 App 报错,提示命令找不到

1
2
3
4
5
6
7
8
9
Launching lib/main.dart on 127.0.0.1:5555
start hap build..-e ERROR: node: /Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm: line 7: node: commandnot found
-e ERROR: NODE_HOME: /Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm: line 11: /node:
o such file or directory
-e ERROR: NODE_HOME: /Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm: line 25: /bin/noc
e: No such file or directory
-e ERROR: Failed to find the executable 'node’ command, please check the following possible causes:e1. Node]s is not installed.e2.'node'command not added to PATH;
eand the 'NoDE HOME' variable is not set in the environment variables to match your NodeJsinstallation location.
ProcessException: The command failedCommand: ohpm clean

检查环境变量配置,配置成功后,检查是否已生效。通过 source ~/.zshrc 或重启命令行程序,甚至重启 IDE/系统,直至变量生效。

7.是否可以使用 Flutter 开发元服务

目前不行,元服务大小有限制 (2M),Flutter 构建产物过大,不符合这一要求

8. 如何自定义显示 DevEco 打开 ohos 后的项目名称

每个鸿蒙Flutter项目,用DevEco打开ohos工程后,默认显示的工程名称为 ohos,如果想自定义显示的工程名称,可以参考以下步骤:

在 ohos/.idea 目录下,新建一个 .name 文件,写入项目名称即可。

参考资料

SDK 安装

参考[鸿蒙Flutter实战:01-搭建开发环境]文章的说明,首先安装 Flutter SDK 3.22.0。

目前鸿蒙化Flutter SDK 3.22 还未正式发布,现在可以使用 https://gitee.com/harmonycommando_flutter/flutter 进行前期测试验证。

使用 FVM 进入 目录 ~/fvm/versions/, 克隆以上仓库。

1
git clone https://gitee.com/harmonycommando_flutter/flutter.git custom_3.22.0

接下来使用 fvm list 命令查看 SDK版本 列表。

1
2
3
4
5
6
7
┌───────────────┬─────────┬─────────────────┬──────────────┬──────────────┬────────┬───────┐
│ Version │ Channel │ Flutter Version │ Dart Version │ Release Date │ Global │ Local │
├───────────────┼─────────┼─────────────────┼──────────────┼──────────────┼────────┼───────┤
│ custom_3.22.0 │ │ Need setup │ │ │ │ │
├───────────────┼─────────┼─────────────────┼──────────────┼──────────────┼────────┼───────┤
│ 3.22.0 │ stable │ 3.22.0 │ 3.4.0 │ May 13, 2024 │ ● │ │
└───────────────┴─────────┴─────────────────┴──────────────┴──────────────┴────────┴───────┘

可以看到,SDK中出现了两个版本,其中使用命令 fvm global 3.22.0 将 官方的3.22.0 设置成了全局默认版本。鸿蒙化的 SDK 需要配置安装,我们稍后进入项目,执行安装。

项目配置

1.进入项目根目录,如果项目还未创建,则使用 flutter create 命令创建项目

1
flutter create my_app

2.在当前项目目录,设置使用的 Flutter SDK 版本

1
fvm use custom_3.22.0

此时会自动安装 sdk 版本,运行成功后如果再运行 fvm list, 可以看到 SDK 已经准备就绪。

1
2
3
4
5
┌───────────────┬─────────┬─────────────────┬──────────────┬──────────────┬────────┬───────┐
│ Version │ Channel │ Flutter Version │ Dart Version │ Release Date │ Global │ Local │
├───────────────┼─────────┼─────────────────┼──────────────┼──────────────┼────────┼───────┤
│ custom_3.22.0 │ │ 3.22.0-ohos │ 3.4.0 │ │ │ │
├───────────────┼─────────┼─────────────────┼──────────────┼──────────────┼────────┼───────┤

同时,配置命令执行完成后,将会在项目目录中创建 .fvm 目录,里面 flutter_sdk 会软连接到实际的 custom_3.22.0 SDK 目录。

查看 .vscode/settings.json 文件可以发现,自动创建了一条配置 flutter sdk 的项目:

1
"dart.flutterSdkPath": ".fvm/versions/custom_3.22.0"

如果项目使用了 melos, 则需要在 melos.yaml 文件的底部,添加以下配置,使得 melos 可以使用自定义的 flutter sdk

1
sdkPath: .fvm/versions/custom_3.22.0

3.如果项目已经创建,还未添加鸿蒙平台支持,则使用以下命令添加鸿蒙平台支持。

1
flutter create --platforms ohos .

其中,.代表当前目录。

目录结构类似如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
├── README.md
├── analysis_options.yaml
├── assets
├── build
├── env
├── lib
│ ├── config
│ └── main.dart
├── melos_ohos_app.iml
├── ohos
│ ├── AppScope
│ ├── build-profile.json5
│ ├── entry
│ ├── har
│ ├── hvigor
│ ├── hvigorfile.ts
│ ├── local.properties
│ ├── oh-package-lock.json5
│ ├── oh-package.json5
│ └── oh_modules
├── pubspec.lock
├── pubspec.yaml
└── pubspec_overrides.yaml

创建命令执行成功后,项目中会出现 ohos目录,这里面存放的就是鸿蒙平台的相关代码。

签名

1.在运行项目前,先对项目进行签名,否则在运行过程中会出现这样的错误

1
请通过DevEco Studio打开ohos工程后配置调试签名(File -> Project Structure -> Signing Configs 勾选Automatically generate signature)

2.用 DevEco 打开上面的 ohos 目录,注意不是项目目录,是项目下面的 ohos 鸿蒙目录,然后根据提示依次打开 File -> Project Structure -> Signing Configs, 点击自动签名即可。

3.签名成功后,文件 ohos/build-profile.json5 会自动更新,里面的字段 signingConfigs 出现相应的签名配置信息。

运行

运行 Flutter 项目,在项目根目录使用 fvm flutter run 或者在 IDE 中点击运行按钮

参考资料

0%