• flutter_exit_app 是一个跨平台"退出应用"插件:用平台原生方式退出应用,而不是在 Dart 里粗暴地 exit(0);
  • 鸿蒙上它底层调用 UIAbilityContext.terminateSelf(),走系统规范的生命周期路径,任务栈、最近任务卡片都会被正确清理;
  • 插件已适配 OpenHarmony/HarmonyOS(pubspec 中注册了 ohos 平台),API 与 Android/iOS 完全一致,业务代码零改动;

仓库代码地址:https://atomgit.com/CPF-Flutter/fluttertpc_flutter_exit_app

在这里插入图片描述


一、最终运行效果

真机(华为 TLR-AL00,HarmonyOS 6.1.0.145)运行 example 应用,首页显示平台版本并带有 Exit 按钮:

请添加图片描述

点击 Exit 按钮约 1 秒后,应用整体退出、回到桌面(注意桌面上的 flutter_exit_app 图标,应用已从最近任务消失):

请添加图片描述

验证点汇总表(全部来自本次真机实测):

功能点预期表现实测结果
插件注册hilog 出现 Adding plugin: FlutterExitAppPlugin✅
platformVersion首页显示 Running on: HarmonyOS 6.1.0✅
exitApp() 发起退出Dart 侧通道调用返回 Done(即 true)✅
原生延时 1s 后 terminateSelfhilog 出现 TerminateSelf: EntryAbility✅
应用进程真正退出aa dump 中无该应用、回到桌面✅
退出后可重新冷启动aa start 正常拉起,新 PID✅
退出动画/最近任务清理桌面 ExitAppAnimation end,任务卡片消失✅

二、库介绍与接口说明

flutter_exit_app(原库 xang555/flutter_exit_app)的定位非常纯粹:给 Flutter 应用提供一个"走平台正规流程"的退出能力。

为什么不用 Dart 的 exit(0)?因为在移动平台上 exit(0) 是直接杀死进程:

  • 跳过了原生侧的 onDestroy 等生命周期回调,原生资源来不及释放;
  • 最近任务里的快照、任务栈状态不会更新,鸿蒙上还会被系统判定为异常退出;
  • Android 上甚至可能触发系统的"应用已停止"的不良观感。

这个插件在各平台分别使用规范 API:Android 走 finishAndRemoveTask() + 延时退出进程;iOS 默认按 Apple 规范挂起应用;鸿蒙则调用 terminateSelf() 结束当前 UIAbility。

核心 API 一览(接口极少,一个就够用):

名称类型参数返回值OHOS 支持说明
FlutterExitApp.exitApp()静态方法{bool iosForceExit = false}Future<bool>✅ yes退出应用。返回 true 表示退出流程已成功发起;false 表示失败。iosForceExit 仅 iOS 有效,鸿蒙会忽略
FlutterExitApp.platformVersion静态属性/Future<String?>✅ yes获取平台版本描述字符串,鸿蒙上返回如 HarmonyOS 6.1.0

三、环境准备

本次实测环境(全部为真实环境信息):

项目版本/说明
测试设备华为 TLR-AL00 真机
系统版本HarmonyOS 6.1.0.145(API 24)
开发工具DevEco Studio(SDK 6.x,toolchains 带 hdc)
example compatibleSdkVersion6.1.1(24)(见 example/ohos/build-profile.json5)
签名产物example/ohos/entry/build/default/outputs/default/entry-default-signed.hap

⚠️ compatibleSdkVersion 说明:example 工程配置为 6.1.1(24),即要求设备系统 API 版本 ≥ 24。如果你的真机系统版本较低,安装时会报版本不兼容,需要在 build-profile.json5 中下调 compatibleSdkVersion 后重新构建。插件本身的 Dart 代码无额外版本要求(pubspec.yaml 中 flutter: ">=3.10.0")。

准备好 hdc 环境后,先确认设备连接:

hdc list targets
# 输出示例:4UQ9K25508013016

四、引入依赖

⚠️ 重要勘误:README 中的 git 地址有误

仓库 README(README.OpenHarmony_CN.md / README.OpenHarmony.md)中原先写的依赖地址是 https://gitcode.com/CPF-Flutter/fluttertpc_flutter_exit_app.git,但该地址与仓库实际远程不符。以 git remote -v 的真实输出为准:

$ git remote -v
origin  https://atomgit.com/CPF-Flutter/fluttertpc_flutter_exit_app (fetch)
origin  https://atomgit.com/CPF-Flutter/fluttertpc_flutter_exit_app (push)

git tag 实际输出(节选):

$ git tag
2.0.0-ohos-1.0.0-beta.1
2.1.2-ohos-1.0.0-beta.1
v1.1.3
v2.0.0
v2.1.0

当前适配分支为 br_3.27(git branch 可见,另有 br_3.27_dev、br_3.41_dev 等开发分支)。本次实测所用的 HEAD 正好落在 TAG 2.0.0-ohos-1.0.0-beta.1 上(git describe --tags 输出可证)。本文已同步修正仓库 README 中的所有错误地址,读者以修正后的为准。

正确的依赖写法(Flutter 3.22 / 3.27):

dependencies:
  flutter_exit_app:
    git:
      url: https://atomgit.com/CPF-Flutter/fluttertpc_flutter_exit_app.git
      ref: 2.0.0-ohos-1.0.0-beta.1   # 对应 br_3.27 分支的适配 TAG

然后拉取依赖:

flutter pub get

TAG 命名规则:原库版本-ohos-适配版本-betax。不同 TAG 之间的变更见仓库 CHANGELOG.OpenHarmony.md(目前记录了 v1.0.0:支持 OpenHarmony 平台,基于 terminateSelf() 实现 exitApp 与 platformVersion)。

五、代码接入

下面以 example 工程真实代码(example/lib/main.dart)为基准,分小节讲清接入全过程。

5.1 引入插件

import 'package:flutter_exit_app/flutter_exit_app.dart';

代码逐段分析

  • 插件只有一个对外入口文件 lib/flutter_exit_app.dart,导入即用;
  • Dart 侧类 FlutterExitApp 是纯静态工具类(私有构造函数 FlutterExitApp._(),见 lib/flutter_exit_app.dart:12),不需要实例化,也不需要管理生命周期;
  • 通道名与方法名集中定义在 lib/src/channel_name.dart,避免魔法字符串散落。

5.2 查询平台版本(initState 中初始化)

example 的首页在 initState 里异步查询平台版本并展示:

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';

  
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion =
          await FlutterExitApp.platformVersion ?? 'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }
  // ...
}

代码逐段分析

  • FlutterExitApp.platformVersion 是一个 getter,内部通过 MethodChannel 调用原生 getPlatformVersion 方法;
  • 用 try/catch 捕获 PlatformException——平台消息随时可能失败(插件未注册、原生异常等),这是官方推荐的防御式写法;
  • ?? 'Unknown platform version' 处理原生返回 null 的情况;
  • if (!mounted) return; 是关键细节:异步等待期间 Widget 可能已被销毁,此时调用 setState 会抛异常,必须先检查 mounted;
  • 实测鸿蒙上返回 HarmonyOS 6.1.0(来自原生 deviceInfo.distributionOSName + distributionOSVersion 拼接)。

5.3 调用退出应用

首页的 Exit 按钮就是退出入口:

TextButton(
  onPressed: () {
    FlutterExitApp.exitApp(iosForceExit: true);
  },
  child: const Text('Exit'),
),

代码逐段分析

  • 一行调用即可退出,无需 context、无需 navigator;
  • iosForceExit: true 只对 iOS 生效(强制杀进程,有审核风险);在鸿蒙上该参数会被完全忽略,行为等同于 exitApp();
  • exitApp() 返回 Future<bool>:true 表示原生已接受退出请求(注意不代表进程立刻消失,鸿蒙原生侧还有约 1 秒的延时,下文原理篇详述);false 表示失败(如原生侧拿不到 Ability 上下文时返回 NO_ACTIVITY 错误,Dart 侧捕获后转成 false);
  • 实测点击后约 1 秒应用退出,与原生实现中 setTimeout(..., 1000) 的设计吻合。

5.4 进阶:确认后退出(防误触)

直接放在按钮上容易误触,example 的"质量加固"系列 Demo(example/lib/quality_reinforcement/003_confirm_exit/confirm_exit_page.dart 对应场景)演示了生产级写法——先弹确认框再退出:

Future<void> showExitConfirmation(BuildContext context) async {
  final shouldExit = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('退出应用'),
      content: const Text('确定要退出应用吗?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          child: const Text('确定'),
        ),
      ],
    ),
  );

  if (shouldExit == true) {
    await FlutterExitApp.exitApp();
  }
}

代码逐段分析

  • showDialog<bool> 泛型标注让 pop 携带类型化的结果,避免 dynamic;
  • "取消"返回 false、"确定"返回 true,shouldExit == true 才真正调用退出,形成标准的二次确认流程;
  • 退出前建议先完成数据落盘、取消定时器/订阅——terminateSelf() 不会等你的异步任务。

5.5 进阶:与系统返回键结合(“再按一次退出”)

很多 App 习惯"按返回键提示再按一次退出"。鸿蒙 Flutter 工程里可以用 PopScope(Flutter 3.16+)组合实现:

PopScope<bool>(
  canPop: false,
  onPopInvokedWithResult: (didPop, result) async {
    if (didPop) return;
    final now = DateTime.now().millisecondsSinceEpoch;
    if (now - _lastBackPress < 2000) {
      await FlutterExitApp.exitApp();   // 2 秒内二次返回 → 退出
    } else {
      _lastBackPress = now;
      // 提示:再按一次退出应用
    }
  },
  child: const Scaffold(body: ...),
)

代码逐段分析

  • canPop: false 拦截系统返回手势/返回键,事件转入 onPopInvokedWithResult;
  • 用时间戳判断 2 秒内是否连续按返回:是则调用 FlutterExitApp.exitApp() 走鸿蒙规范的 terminateSelf() 退出,否则仅提示;
  • 这样系统返回键的退出路径也统一收敛到插件上,行为与桌面图标退出一致。

六、运行与验证

构建产物已签名,直接安装到真机并启动(bundleName 以 pack.info 实际内容为准,见第八节 FAQ 中的坑):

hdc install -r entry-default-signed.hap
hdc shell hilog -r          # 清空日志缓冲
hdc shell aa start -a EntryAbility -b com.hurbes.color_extractor_example

启动成功后抓取插件注册日志(hilog 原文):

10-22 01:39:26.970 24590 24590 W A000FF/hurbes.color_extractor_example/Flutter: FlutterEngineCxnRegistry --> Adding plugin: FlutterExitAppPlugin

点击 Exit 按钮,约 1 秒后抓取退出过程日志(hilog 原文):

10-22 01:40:00.708 24590 24977 I C01331/hurbes.color_extractor_example/Context: [ability_context_impl.cpp721]scb call, TerminateSelf: EntryAbility
10-22 01:40:00.725  3418  3418 W A01B01/com.ohos.sceneboard/HOME: PageDesktopModel: getExitAppData formCardId:undefined;cardId:undefined
10-22 01:40:00.726  3418  3418 W A01B01/com.ohos.sceneboard/HOME: PageDesktopModel: getExitAppData bundleName: com.hurbes.color_extractor_example, abilityName: EntryAbility, startType: 0, ...
10-22 01:40:01.431  3418  3418 W A01B01/com.ohos.sceneboard/HOME: SCBScenePanelAnimator: ExitAppAnimation end.

日志逐条解读:

  • 第 1 条:ability_context_impl.cpp 中的 TerminateSelf: EntryAbility——原生插件调用 terminateSelf() 生效的系统侧记录,时间戳比点击晚约 1 秒,与源码中的 setTimeout(…, 1000) 完全对应;
  • 第 2、3 条:系统桌面(sceneboard/HOME)收到应用退出通知,清理最近任务数据;
  • 第 4 条:ExitAppAnimation end——退出动画播放完毕,应用回到桌面。

进程级验证:退出后执行 hdc shell "aa dump -a | grep hurbes" 无任何输出,说明应用进程已完全退出;再次 aa start 能正常冷启动(新 PID 25335,旧 PID 24590 已销毁)。

交互验证通过 hdc shell uitest uiInput click 518 445 模拟点击 Exit 按钮,全流程截图见第一节。

七、工作原理

7.1 MethodChannel 通道与调用链路

Dart 侧通道定义(lib/flutter_exit_app.dart:17):


static MethodChannel channel = const MethodChannel('flutter_exit_app');

方法名定义(lib/src/channel_name.dart):

class ChannelName {
  static const String exitApp = 'com.laoitdev.exit.app';
}

完整调用链路:

┌─────────────────────────── Flutter (Dart) ───────────────────────────┐
│ FlutterExitApp.exitApp()                                             │
│   └─ channel.invokeMethod('com.laoitdev.exit.app',                   │
│                            {"killIosProcess": iosForceExit})          │
└──────────────────────────────┬───────────────────────────────────────┘
                               │ MethodChannel: 'flutter_exit_app'
                               ▼ (BinaryMessenger / IMessenger)
┌────────────────────────── ArkTS 原生 (ohos) ──────────────────────────┐
│ FlutterExitAppPlugin.onMethodCall(call, result)                      │
│   └─ case ChannelName.EXIT_APP → handleExitApp(result)               │
│        ├─ result.success('Done')          ← 先回包,返回 true          │
│        └─ setTimeout(() => {               ← 延时 1000ms              │
│             context.terminateSelf();       ← 真正的退出动作            │
│           }, 1000)                                                   │
└───────────────────────────────────────────────────────────────────────┘

7.2 ArkTS 插件实现剖析(ohos/src/main/ets)

插件类声明(FlutterExitAppPlugin.ets):

export default class FlutterExitAppPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private context: common.UIAbilityContext | null = null;
  private exitTimerId: number = -1;
  // ...
}

代码逐段分析

  • 实现 FlutterPlugin 获得引擎绑定生命周期,实现 MethodCallHandler 接收 Dart 调用——这是 flutter_ohos 适配层插件的标准形态;
  • context 类型是 common.UIAbilityContext,后续退出就靠它。

通道注册(onAttachedToEngine):

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.channel = new MethodChannel(binding.getBinaryMessenger(), 'flutter_exit_app');
  this.channel.setMethodCallHandler(this);
}

代码逐段分析

  • 通道名 'flutter_exit_app' 与 Dart 侧严格一致,两端靠这个字符串握手;
  • messenger 从 binding 获取,保证消息走当前 Flutter 引擎的通信管道。

上下文获取(onAttachedToAbility)——这是很多插件翻车的地方:

onAttachedToAbility(binding: AbilityPluginBinding): void {
  const ability = binding.getAbility();
  if (ability != null && ability.context != null) {
    this.context = ability.context;
  }
}

代码逐段分析

  • flutter_ohos 的插件有 engine 级和 ability 级两类绑定回调。UIAbilityContext 必须从 Ability 侧拿,在 engine 回调里是拿不到的;
  • 拿到 UIAbilityContext 才有资格调用 terminateSelf()——如果拿不到,后面会走 result.error('NO_ACTIVITY', ...) 分支,Dart 侧 exitApp() 返回 false。

退出核心逻辑(handleExitApp):

private handleExitApp(result: MethodResult): void {
  if (this.context == null) {
    result.error('NO_ACTIVITY', 'Activity is not available', null);
    return;
  }

  const contextRef: common.UIAbilityContext = this.context;
  try {
    this.clearExitTimer();
    result.success('Done');
    this.exitTimerId = setTimeout(async () => {
      this.exitTimerId = -1;
      try {
        await contextRef.terminateSelf();
      } catch (innerError) {
        hilog.error(..., 'terminateSelf failed: %{public}s', message);
      }
    }, 1000);
  } catch (e) {
    result.error('EXIT_FAILED', `Failed to schedule exit: ${message}`, null);
  }
}

代码逐段分析

  • 先 result.success('Done') 再延时退出:顺序很讲究。如果先 terminateSelf(),Ability 立即销毁,通道可能来不及把结果回传给 Dart。先回包 Done,Dart 侧 res == "Done" 得到 true,1 秒后应用才真正退出;
  • setTimeout(…, 1000) 的 1 秒缓冲给 Flutter 侧留出了收尾时间(回包传输、动画等),实测点击到 TerminateSelf 日志的间隔正好约 1 秒;
  • terminateSelf() 是异步的(返回 Promise),用 await + try/catch 包裹,失败时通过 hilog.error 打出原因而不是静默吞掉;
  • 鸿蒙退出应用的正确姿势就是 UIAbilityContext.terminateSelf():它由系统 Ability 框架调度,会正常走 EntryAbility 的 onDestroy 生命周期、更新任务栈与最近任务、播放退出动画。对比 process.exit() 这类硬杀进程的手段,terminateSelf() 才是系统推荐 API;
  • clearExitTimer() 在 onDetachedFromEngine / onDetachedFromAbility 中都会调用,防止插件注销后定时器还持有 context 引发泄漏或崩溃——细节拉满。

platformVersion 实现:

case 'getPlatformVersion':
  const systemName: string = deviceInfo.distributionOSName;
  const systemVersion: string = deviceInfo.distributionOSVersion;
  result.success(`${systemName} ${systemVersion}`);

代码逐段分析

  • 用 @kit.BasicServicesKit 的 deviceInfo 读取系统名与版本号,拼接成 HarmonyOS 6.1.0 这样的字符串回传,与实测截图一致。

八、FAQ

Q1:鸿蒙上为什么不能直接在 Dart 里 exit(0)?

exit(0) 直接终止进程,完全绕过 Ability 框架:onDestroy 不会执行、最近任务快照不清理、系统级资源(如与 UIAbility 绑定的窗口、任务栈记录)状态不一致,属于"野路子"退出。正确姿势是通过本插件走到原生 terminateSelf(),由系统规范地结束 Ability。实测也证明 terminateSelf 后系统桌面会收到完整的退出通知并清理任务数据(见第六节日志)。

Q2:terminateSelf() 之后应用进程立刻消失吗?

不是立即。插件先回包 Done,再延时 1 秒才调用 terminateSelf();terminateSelf() 本身也是异步的,系统还要走生命周期、动画等流程。所以 Dart 侧拿到 true 后,应用还会存活约 1~2 秒。不要在这段窗口期假设进程已死,也不要依赖退出接口做数据保存——先保存,再退出。

Q3:exitApp() 返回 false 是什么原因?怎么排查?

false 意味着 Dart 侧捕获了 PlatformException。鸿蒙上最常见的两种:一是原生 context == null 返回 NO_ACTIVITY(插件未与 Ability 正常绑定,检查是否用的是 flutter_ohos 定制的宿主工程);二是 terminateSelf 被系统拒绝。用 hdc shell "hilog -x | grep FlutterExitAppPlugin" 看原生错误日志即可定位。

Q4:为什么装 HAP 报 install version downgrade(错误码 9568263)?

设备上已装过同 bundleName 的更高版本。本次实测就遇到了:用 hdc uninstall <bundleName> 卸载后重装即可。另外注意——这个 example 的真实 bundleName 要以 pack.info 为准:AppScope/app.json5 里写的是 com.flutter_exit_app.example,但实际构建产物 pack.info 里是 com.hurbes.color_extractor_example(构建时配置被改过且未回填)。aa start 时用错 bundleName 会报 ability 不存在。

Q5:iosForceExit: true 在鸿蒙上有影响吗?和其他平台的差异是什么?

完全没有影响,鸿蒙会忽略该参数。三平台行为对照:Android 结束 Activity 并移除任务后延时退出进程;iOS 默认仅挂起(符合 Apple 审核规范),iosForceExit: true 才强杀进程;鸿蒙一律 terminateSelf(),语义上等价于 Android 的"结束 + 移除任务"。跨平台业务代码无需任何平台判断。

Q6:退出后任务中心里还有应用卡片吗?会丢数据吗?

实测退出后卡片随之消失(桌面日志 ExitAppAnimation end),因为 terminateSelf() 由系统清理任务栈,这正是它比 exit(0) 优雅的地方。数据方面:只要退出前把需要持久化的数据落盘( preferences、数据库等),就不会丢失;但 Ability 的内存状态、页面栈会随退出全部销毁,下次启动是全新冷启动(实测新 PID 验证过)。如果产品诉求是"退到后台不杀进程",那不该用本插件,直接 moveAbilityToBackground 即可。

九、结语

flutter_exit_app 是那种"小而美"的插件:API 只有一个 exitApp(),鸿蒙适配层不到 200 行 ArkTS,却把"退出应用"这件容易踩坑的事做成了标准答案——先回包、延时退出、terminateSelf() 走系统正规流程、定时器防泄漏。对鸿蒙 Flutter 开发者来说,需要退出场景(设置页"退出登录并关闭应用"、隐私政策拒绝退出、返回键二次退出等)时,直接引入即可。

本次真机实测全部验证点通过,唯一的问题是 README 中的仓库地址写错(gitcode → atomgit),已修正。

能力对照表:

能力AndroidiOSOpenHarmony/HarmonyOS
exitApp() 退出应用✅ finish + 移除任务 + 退出进程✅ 默认挂起(iosForceExit 可强杀)✅ terminateSelf()(本次实测通过)
platformVersion✅✅✅(返回 HarmonyOS x.y.z,实测通过)
iosForceExit 参数忽略生效忽略(不影响功能)
无需特殊权限✅✅✅
走系统生命周期/清理任务栈✅✅✅(实测任务卡片消失)
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐