Flutter 3.47 鸿蒙三方库适配实战:从 0 到 1 处理 system_settings_2

应用里「跳到系统设置」几乎是个必做功能:引导用户开通知权限、开蓝牙、连 WLAN、看应用详情。安卓有 Intent(Settings.ACTION_*) 一整套,iOS 只能打开本应用的设置页。system_settings_2 把这两套差异收进 25 个同签名方法里,本批里它和 airplane_mode_checker 都属于「系统设置」这条能力线。

本文以 system_settings_2 3.0.2 为例,记录我如何保留原有 Dart API、补齐 Flutter OH 插件入口、在鸿蒙端用 startAbility + Want 接入真实系统设置页,并在 HarmonyOS API 26 真机上完成构建、安装、启动与跳转验证。

适配仓库: oh-flutter/system_settings_2

适配分支: feat/ohos_system_settings_2_3.0.2

适配 TAG: 3.0.2-ohos-1.0.0-beta.1

一、先看最终效果

在这里插入图片描述

图 1:HarmonyOS API 26 真机启动自检台。页面顶部显示「已成功 0 次 · 失败 0 次」,下方是 25 个与上游方法一一对应的按钮。

验证点实测结果证据
Flutter OH 3.47 工程构建通过entry-default-signed.hap
安装、启动(真机)通过图 1
SystemSettings.wifi()成功跳到系统 WLAN 页图 2
返回后调用计数已成功 1 次 · 失败 0 次图 3

在这里插入图片描述

图 2:点击「WLAN · wifi」按钮后,系统真的打开了设置里的 WLAN 页面——左上角标题是「WLAN」,带返回箭头和开关,说明 startAbility + uri="wifi_entry" 生效。

在这里插入图片描述

图 3:按返回键回到自检台,顶部统计变成「已成功 1 次 · 失败 0 次」,「最近一次」显示 wifi → 成功。

二、这个库原来是怎么工作的

system_settings_2 的接口面出奇地整齐——25 个方法,全部无参数、无返回值,每个方法在 Dart 侧就是一次 invokeMethod:

Dart SystemSettings
  -> MethodChannel('system_settings_2')
  -> Android: Intent(Settings.ACTION_WIFI_SETTINGS) 等 25 个 ACTION
  -> iOS: UIApplication.openSettingsURLString(只能开本应用设置页)

覆盖的能力包括:设置首页、无线与网络、WLAN、蓝牙、NFC、移动数据、数据漫游、飞行模式、显示、声音、日期、语言、定位、隐私、安全、无障碍、存储、电量、电源、应用列表、通知管理、默认应用、关于本机,以及「本应用详情」和「本应用通知」两个针对自身应用的入口。

正因为返回类型是 void,鸿蒙侧的实现目标很明确:把调用方送到正确的设置页面,不需要回传任何数据。

三、实际环境

Flutter 鸿蒙环境搭建不在本文重复展开,直接参考 Flutter OH 环境搭建指南。

组件版本
Flutter OH3.47.4-ohos-1.0.4
Dart3.13.3
DevEco Studio26.0.0 Release
HarmonyOS SDKAPI 26
测试设备华为真机 6UMBB26319007180,HarmonyOS 7.0.0.105(API 26)
目标库system_settings_2 3.0.2
flutter --version
hdc list targets

四、确认上游版本并同步到 AtomGit

上游包地址是 https://github.com/timmaffett/system_settings_2,版本 3.0.2,使用 MIT License。适配前我在 AtomGit 目标组织中搜索了同名库,没有找到可以直接使用的 OHOS 实现。

基线核对:上游 master(db8faac,pubspec 写 3.0.2)与 pub.dev 上的 3.0.2 逐文件一致(81 个文件相同,仓库仅多出 .github / .gitignore / .vscode 等工程文件)。

git clone https://atomgit.com/oh-flutter/system_settings_2.git
cd system_settings_2
git switch feat/ohos_system_settings_2_3.0.2
git log --oneline -5

五、创建适配分支并检查工程结构

git switch -c feat/ohos_system_settings_2_3.0.2

关键目录如下:

system_settings_2/
├── lib/system_settings_2.dart                  # 上游唯一的 Dart 文件,一行未改
├── ohos/
│   ├── index.ets                               # OHOS 插件导出入口
│   └── src/main/ets/components/plugin/
│       └── SystemSettingsPlugin.ets            # 通道实现(约 200 行)
├── example/lib/main.dart                       # 真机验证自检台
├── example/ohos/                               # Flutter OH 宿主工程
├── pubspec.yaml                                # 声明 ohos 平台
└── README.OpenHarmony_CN.md                    # 鸿蒙适配说明

pubspec.yaml 中的关键声明是:

flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: SystemSettingsPlugin

pluginClass 必须和 ohos/index.ets 导出的类名一致,否则 Dart 端能编译,运行时却不会注册插件。

六、适配过程:注册通道并对齐语义

6.1 通道注册

本库只有一个方法通道,没有事件通道:

const METHOD_CHANNEL: string = 'system_settings_2';

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.channel = new MethodChannel(binding.getBinaryMessenger(), METHOD_CHANNEL);
  this.channel.setMethodCallHandler(this);
  Log.i(TAG, 'system_settings_2 channel registered');
}

6.2 核心:25 个方法映射成「bundleName + uri」

鸿蒙没有「Settings.ACTION_*」这种枚举,但系统设置应用接受 Want 里的 uri 参数来定位子页面。所以实现里建了一张「Dart 方法名 → 子页面 uri」的映射表:

/** 鸿蒙"设置"应用:所有跳转都落到它身上。 */
const SETTINGS_BUNDLE: string = 'com.huawei.hmos.settings';
const SETTINGS_ABILITY: string = 'com.huawei.hmos.settings.MainAbility';

function buildPageUris(): Map<string, string> {
  const pages: Map<string, string> = new Map();
  pages.set('system', '');                              // 空串 = 设置首页
  pages.set('wireless', 'wifi_entry');
  pages.set('wifi', 'wifi_entry');
  pages.set('bluetooth', 'bluetooth_entry');
  pages.set('nfc', 'more_connections_entry');
  pages.set('data-usage', 'mobile_network_entry');
  pages.set('display', 'display_entry');
  pages.set('sound', 'sound_entry');
  pages.set('date', 'date_and_time_entry');
  pages.set('locale', 'language_and_region_entry');
  pages.set('apps', 'application_info_entry');
  // ... 共 25 项
  return pages;
}

onMethodCall 就退化成一次查表 + 一次 startAbility:

onMethodCall(call: MethodCall, result: MethodResult): void {
  const uri: string | undefined = PAGE_URIS.get(call.method);
  if (uri === undefined) {
    result.notImplemented();
    return;
  }
  this.openSettings(call.method, uri)
    .then((): void => { result.success(null); })
    .catch((error: BusinessError): void => {
      result.error('ERROR', `Failed to open system settings: ${error.message}`, null);
    });
}

6.3 「本应用」的两个页面靠参数定位

app 和 app-notifications 不是独立 uri,而是同一个页面 + 带上自己的包名:

const PARAM_PUSH: string = 'pushParams';

if (method === 'app' || method === 'app-notifications') {
  const params: Record<string, Object> = {};
  params[PARAM_PUSH] = ability.context.abilityInfo.bundleName;
  want.parameters = params;
}

6.4 插件必须实现 AbilityAware

这是本库与「纯数据类插件」最大的区别:startAbility 是 UIAbilityContext 的能力,而插件默认拿不到 UIAbility。

export default class SystemSettingsPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private ability: UIAbility | null = null;

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.ability = binding.getAbility();
  }

  onDetachedFromAbility(): void {
    this.ability = null;
  }
}

如果没有 UIAbility 就跳不了页面,所以 openSettings 里先做严格判断:

if (ability === null) {
  throw new Error('UIAbility is not attached, cannot start settings ability');
}

宁可报错也不静默失败——这比返回一个假的成功值更好排查。

6.5 子页面 uri 是系统约定,必须有兜底

这些 uri 不属于公开 API,不同系统版本或机型上可能存在差异。所以每个方法都做了「打不开就退回首页」的兜底:

try {
  await context.startAbility(want);
  Log.i(TAG, `${method} -> ${SETTINGS_BUNDLE} uri="${uri}" (ok)`);
} catch (error) {
  const err: BusinessError = error as BusinessError;
  Log.w(TAG, `${method} -> uri="${uri}" failed code=${err.code} ${err.message}, ` +
    'falling back to settings home');
  await context.startAbility({
    bundleName: SETTINGS_BUNDLE,
    abilityName: SETTINGS_ABILITY,
  } as Want);
}

这样即使某个子页面在特定设备上不存在,用户也至少会落在设置首页,而不是什么都不发生。

方法OpenHarmony 实现
system()startAbility,不带 uri → 设置首页
wifi() / wireless()uri="wifi_entry"
bluetooth()uri="bluetooth_entry"
nfc()uri="more_connections_entry"
airplaneMode() / dataRoaming() / dataUsage()uri="mobile_network_entry"
app() / appNotifications()uri="application_info_entry" / "notification_entry" + pushParams=<本应用包名>
其余 19 个各自对应的 *_entry

七、构建配置与签名

这里踩了两个坑,都值得记下来。

7.1 compatibleSdkVersion 必须写 "26.0.0"

上游示例工程沿用旧模板,写的是 '5.1.0(18)'。用本机 Flutter OH 3.47.4 构建时直接失败:

00306004 Specification Limit Violation
The project's compatibleSdkVersion: 18 cannot be lower than
the minimum compatible version 24 required by the dependencies: @ohos/flutter_ohos.

但改成 '6.0.0(20)' 仍报同样的错,改成 '26.0.0(26)' 则报 00308018 Unknown Error。正确写法是不带括号的纯版本号:

{
  "products": [
    {
      "name": "default",
      "signingConfig": "default",
      "compatibleSdkVersion": "26.0.0",
      "targetSdkVersion": "26.0.0",
      "runtimeOS": "HarmonyOS"
    }
  ]
}

7.2 签名证书绑定了 bundleName

配置好签名后仍然失败:

00303074 Configuration Error
The bundleName in app.json5/hvigorfile.ts does not match the bundleName
in the generated SigningConfigs.

原因是调试证书是按包名签发的。本库示例的 app.json5 写的是 xyz.hiveright.system_settings_2_example,而证书对应的是 com.example.flutter_oh_demo。把 AppScope/app.json5 的 bundleName 改成证书对应的值即可:

{
  "app": {
    "bundleName": "com.example.flutter_oh_demo",
    ...
  }
}

证书、p12、p7b 和密码没有提交到仓库,读者应在自己的 DevEco Studio 中生成调试签名。

八、通过 AtomGit 引入适配版

在另一个 Flutter OH 应用中使用时,建议锁定 TAG:

dependencies:
  system_settings_2:
    git:
      url: https://atomgit.com/oh-flutter/system_settings_2.git
      ref: 3.0.2-ohos-1.0.0-beta.1
flutter pub get

然后在 pubspec.lock 检查 source: git、AtomGit 地址和 resolved-ref,确认没有解析到 pub.dev 的同名版本。

本库不需要任何权限声明——startAbility 打开系统设置应用属于正常的应用间跳转,不涉及敏感权限。

九、构建、安装和真机验证

cd example
flutter pub get
flutter analyze
flutter build hap --debug
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.flutter_oh_demo

构建输出为 ✓ Built build/ohos/hap/entry-default-signed.hap.,hdc 报告 install bundle successfully 与 start ability successfully。

真机上按本文截图顺序操作:启动自检台(图 1)→ 点击「WLAN · wifi」→ 系统打开 WLAN 设置页(图 2)→ 按返回键回到自检台,统计变为「已成功 1 次 · 失败 0 次」(图 3)。

排查时按 SystemSettingsPlugin 过滤 hilog,可以看到每个方法到底是「落到目标页」还是「退回了首页」:

hdc shell hilog -x | grep SystemSettingsPlugin
SystemSettingsPlugin --> wifi -> com.huawei.hmos.settings uri="wifi_entry" (ok)

十、适配结果和当前边界

25 个方法全部接通,Dart 层一行未改。真机上点击 WLAN 确实打开了系统设置里的 WLAN 页面,说明 startAbility + uri 这条路径是真实生效的。

已知边界:

  • 子页面 uri 依赖系统实现:表里未覆盖或某版本不存在的页面会退回设置首页(日志会写明 falling back);
  • 部分安卓概念在鸿蒙没有独立页面:NFC 归在「更多连接」下,数据漫游/飞行模式归在「移动网络」下,因此共用同一个 uri;
  • app / app-notifications 依赖 pushParams:设置应用是否支持该参数由系统决定,不支持时同样退回首页;
  • 没有模拟 iOS 的「只能开本应用设置页」限制:system() 等方法照常打开对应页面,行为比 iOS 更接近安卓;
  • 示例的 Dart SDK 约束被放宽:上游示例写 sdk: '>=2.15.1 <3.0.0'(Dart 2),鸿蒙工具链是 Dart 3.13,无法直接构建,因此改成 >=2.15.1 <4.0.0(与主包一致)。

十一、FAQ

Q1:为什么不用 settings.getValueSync 之类的接口?

  • 现象: @ohos.settings 确实是「读写系统设置项」的接口,但它面向的是读取配置值(如飞行模式状态),不是「打开设置页面」。
  • 解决方法: 本库的语义是「把用户送到设置页」,属于应用间跳转,必须用 startAbility + Want。
  • 验证结果: 真机点击 WLAN 后系统设置被拉起。

Q2:没有 UIAbility 会怎样?

  • 现象: startAbility 是 UIAbilityContext 的能力,纯引擎插件拿不到。
  • 解决方法: 实现 AbilityAware,在 onAttachedToAbility 里保存 UIAbility;未附着时抛出明确错误。
  • 验证结果: 正常宿主下可跳转;异常情况下 Dart 侧收到 Failed to open system settings 而不是静默无响应。

Q3:某个子页面打不开怎么办?

  • 现象: 子页面 uri 属于系统约定,某些版本/机型上可能不存在。
  • 解决方法: 实现里对每个方法都做了兜底——打不开具体页面就退回设置首页,并在 hilog 记录失败码。
  • 验证结果: 兜底路径可用,用户不会落到「什么都没发生」的状态。

Q4:和 airplane_mode_checker 有什么区别?

  • 现象: 两者都跟系统设置有关,但 airplane_mode_checker 是读状态 + 监听变化,本库是打开设置页。
  • 解决方法: 按需要选择;如果只是想让用户自己去开飞行模式,用本库的 airplaneMode();如果要读当前状态,用 airplane_mode_checker。
  • 验证结果: 两库在同一台设备上均可用。

Q5:构建时报 compatibleSdkVersion 冲突怎么解决?

  • 现象: 00306004 Specification Limit Violation,提示当前版本低于 @ohos/flutter_ohos 要求的 24。
  • 解决方法: 写成纯版本号 "26.0.0",不要写 '5.1.0(18)' 或 '26.0.0(26)'。
  • 验证结果: 改完后 MergeProfile 通过。

十二、总结

system_settings_2 3.0.2 已完整接入鸿蒙:25 个「打开设置页」方法统一用 startAbility + Want 实现,靠 uri 定位子页面、靠 pushParams 定位本应用页面,并对打不开的子页面做了退回首页的兜底。真机上点击 WLAN 真实拉起了系统 WLAN 设置页,Dart 层一行未改。

这个库最值得记的一条经验是:鸿蒙的「打开某页」不是枚举映射,而是 bundleName + uri 的约定。uri 不属于公开 API,所以兜底逻辑不是可选项而是必需品——否则换个系统版本,用户点了没反应,排查起来会很痛苦。

十三、参考资料和仓库地址

欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐