Flutter 鸿蒙 flutter_screenshot_detect 0.1.7 使用实战:监听截图并正确管理订阅
本文用
flutter_screenshot_detect 0.1.7完成一个可运行的 Flutter 鸿蒙
Demo:页面启动后监听当前应用窗口的截图事件,展示事件次数、检测方式和时间,同时可以暂停与恢复监听。代码、HAP 和截图均在
HarmonyOS 真机上实测,下文会把依赖锁定、API 用法、异常处理和资源释放一次讲清。三方库仓库: https://atomgit.com/oh-flutter/flutter_screenshot_detect
OHOS 适配分支:
feat/ohos_flutter_screenshot_detect_0.1.7OHOS 适配 TAG: 尚未发布,当前请锁定受测提交
本文受测版本:
0.1.7/1da71f4294faa3fc2584c720e3d75df770893236当前远程 HEAD:
609b0793a6f0a6917cc4a4b6f7ec717fbd141bea,后续两次提交只补充验证文档
一、最终真机效果
先看结果。我在真机上启动 Demo,订阅截图流后连续触发两次系统截图。页面计数变为 2,两条记录都显示 ohos_window_screenshot,且有各自的实际时间戳。

图 1:CHZ-AL00 / HarmonyOS 7.0.0.105 上的实际 Flutter 应用,恢复监听后成功收到第二次截图事件。
| 验证点 | 实测结果 | 证据 |
|---|---|---|
| AtomGit 依赖解析 | 锁定 1da71f4294faa3fc2584c720e3d75df770893236 | 图 3 |
| 静态分析和自动化 | 无静态问题,19 项 Dart/Widget/ArkTS 用例通过 | 图 9 |
| HAP 构建、签名、安装与启动 | 通过 | 图 1、图 3 |
| 截图事件 | 收到 method 和时间戳,计数按次增加 | 图 1、图 6 |
| 暂停监听 | 暂停期间系统截图不增加计数 | 图 7、图 8 |
| 截图文件路径 | OHOS 返回 null | 图 10 |
二、为什么在项目中用它
我要处理的是“截图发生后做一件事”,例如在票据页提醒用户注意隐私,在内容页记录一次本地行为,或在阅后即焚页面立即停止展示。如果自己分别写 Android、iOS 和 OHOS 通道,除了系统 API,还要处理事件数据格式、页面销毁、重复订阅和取消时的旧回调。
flutter_screenshot_detect 已经把这些差异收到同一个 Dart 事件流中。我的页面只需订阅 onScreenshot,收到 FlutterScreenshotEvent 后更新状态,离开页面时取消订阅。
这里有一个必须先说明的边界:它是“截图通知”库,不是“防截图”库。它不阻止系统截图,不读取相册,不获取截图内容,也不监听录屏。如果业务要屏蔽敏感页面的截图和录屏,应该使用窗口隐私模式类能力,不能把两类插件混为一谈。
2.1 选库依据
| 对比项 | 核对结果 |
|---|---|
| 库名称与版本 | flutter_screenshot_detect 0.1.7 |
| AtomGit 仓库 | oh-flutter/flutter_screenshot_detect |
| OHOS 支持 | feat/ohos_flutter_screenshot_detect_0.1.7 分支 |
| 本文受测代码 | 1da71f4294faa3fc2584c720e3d75df770893236 |
| 所需 API | onScreenshot、startListening()、dispose() |
| 许可证 | MIT |
| 结论 | 满足前台窗口截图通知需求,但尚无稳定 OHOS TAG,所以用 commit 锁定 |
截至 2026 年 9 月 12 日,远程默认 HEAD 为 609b0793a6f0a6917cc4a4b6f7ec717fbd141bea,相比真机受测代码只多了两次验证文档提交。为了让本文结果可复现,依赖和 Demo 链接都锁定实际安装到真机的代码 commit。

图 2:AtomGit origin、OHOS 适配分支、当前 HEAD 和干净工作区的核对结果。
三、实测环境与能力范围
| 组件 | 实测版本 |
|---|---|
| Flutter OH | 3.41.10-ohos-1.0.1 |
| Dart | 3.11.5 |
| DevEco Studio | 26.0.0 Release |
| HarmonyOS SDK | API 26,Demo 兼容 API 18 |
| 真机 | CHZ-AL00 |
| 系统 | HarmonyOS 7.0.0.105 |
| 三方库 | flutter_screenshot_detect 0.1.7 |
Flutter 鸿蒙环境搭建不在本文重复,可参考 Flutter OH 环境搭建指南。
截至 2026 年 9 月 12 日,版本号最大的 Flutter OH 标签是预览版 3.44.9+ohos-0.0.1-canary1,最新正式稳定标签仍是 3.41.10-ohos-1.0.1。本文选用已完成插件构建、签名和真机回归的稳定版,不把其他 Demo 在预览 SDK 上的结果当成本库实测结论。
| API 或能力 | 用途 | 本文是否演示 | 真机结论 |
|---|---|---|---|
onScreenshot | 监听截图事件 | 是 | 通过 |
StreamSubscription.cancel() | 暂停页面订阅 | 是 | 通过 |
| 重新订阅 | 恢复事件投递 | 是 | 通过 |
startListening() / dispose() | 便捷回调接口 | 代码与自动化 | 未单独做真机页面 |
| 多实例共享事件流 | 避免实例互相中断 | 自动化 | 未真机验收 |
| 后台、Ability 重建、控制中心截图 | 更广生命周期与入口 | 否 | 未验证 |
调用链可以简化为:
Flutter 页面 -> onScreenshot 广播流 -> EventChannel -> OHOS 当前窗口 screenshot 事件
四、从 AtomGit 引入依赖
仓库内的 example 用 path 依赖指向上一级插件,这是 Flutter 插件仓库的常见写法。普通业务工程不在同一仓库内,应改为 AtomGit Git 依赖。由于当前没有 OHOS 稳定 TAG,本文直接锁定已验证 commit:
dependencies:
flutter:
sdk: flutter
flutter_screenshot_detect:
git:
url: https://atomgit.com/oh-flutter/flutter_screenshot_detect.git
ref: 1da71f4294faa3fc2584c720e3d75df770893236
执行:
flutter pub get
flutter pub deps
pubspec.lock 中应该看到类似下面的结果:
flutter_screenshot_detect:
dependency: "direct main"
description:
path: "."
ref: "1da71f4294faa3fc2584c720e3d75df770893236"
resolved-ref: "1da71f4294faa3fc2584c720e3d75df770893236"
url: "https://atomgit.com/oh-flutter/flutter_screenshot_detect.git"
source: git
version: "0.1.7"
如果声明中暂时使用分支名,那么 ref 会显示分支,resolved-ref 仍应是具体 commit。发布项目时不要只看 pub get 成功,还要核对 resolved-ref,否则分支后续变化可能让构建结果漂移。

图 3:已构建的无签名 HAP 摘要和隔离验证宿主的 AtomGit resolved-ref。
五、鸿蒙宿主配置
这个库不需要截图权限、相册权限或存储权限。它监听的是当前 Ability 窗口的公共 screenshot 事件,不读文件。插件自身的 ohos/src/main/module.json5 也没有声明任何权限。
接入时只需要确认两点:
- 使用支持 OHOS 的 Flutter SDK,不要误用标准 Flutter SDK 构建鸿蒙宿主。
- 保留
pubspec.yaml中插件的ohos.pluginClass,让 Flutter 生成注册代码,不要手工修改GeneratedPluginRegistrant.ets。
Demo 宿主的 ohos.permission.INTERNET 来自 Flutter 示例工程,不是截图监听所必需的权限。业务项目应按自己的网络功能决定是否保留,不要为了这个插件增加无关权限。

图 4:源码核对显示 OHOS 使用当前窗口的 screenshot 事件,并回传 method、timestamp 和可空 path。使用方不需要复制这段原生代码。
六、核心 API 用法
6.1 直接订阅 onScreenshot
onScreenshot 是 Stream<FlutterScreenshotEvent>,适合页面需要保存 StreamSubscription 并主动暂停、恢复或销毁的场景。
final FlutterScreenshotDetect _detector = FlutterScreenshotDetect();
StreamSubscription<FlutterScreenshotEvent>? _subscription;
void startScreenshotListening() {
_subscription ??= _detector.onScreenshot.listen(
(event) {
debugPrint('method=${event.method}');
debugPrint('time=${event.timestamp}');
debugPrint('path=${event.path}');
},
onError: (Object error) {
debugPrint('screenshot listener failed: $error');
},
);
}
Future<void> stopScreenshotListening() async {
final subscription = _subscription;
_subscription = null;
await subscription?.cancel();
}
_subscription ??= 防止同一页面因重复点击又新建一条订阅。停止时先把字段置空,页面就可以立即进入“已停止”状态,再等待底层取消完成。
6.2 startListening 便捷接口
只需一个回调、不需要中途切换时,也可以使用:
final detector = FlutterScreenshotDetect();
detector.startListening((event) {
debugPrint(event.toString());
});
void dispose() {
detector.dispose();
super.dispose();
}
重复调用 startListening() 会用新回调替换该实例的旧回调,detector.dispose() 只会取消由 startListening() 创建的内部订阅。如果代码是直接通过 onScreenshot.listen() 创建订阅,这条订阅属于调用方,必须自己调用 subscription.cancel()。这是最容易遗漏的资源释放点。
6.3 理解事件字段
| 字段 | OHOS 实际值 | 使用建议 |
|---|---|---|
method | ohos_window_screenshot | 可用于日志和跨平台统计,不要据此推断截图文件位置 |
timestamp | Dart DateTime | 用于页面展示或行为时序,OHOS 源数据单位转为微秒,实际精度仍是毫秒 |
path | null | 必须按可空值处理,不要拼凑文件路径 |

图 5:另一次真机运行样本,页面显示 ohos_window_screenshot 和真实事件时间。OHOS 页面没有显示 path,因为它为 null。
七、完整可运行 Demo
下面是本次受测 example/lib/main.dart 的完整主流程。它保留最近 100 条事件,可以暂停和恢复,同时会把原生注册失败显示到页面。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenshot_detect/flutter_screenshot_detect.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
home: const ScreenshotDetectDemo(),
);
}
}
class ScreenshotDetectDemo extends StatefulWidget {
const ScreenshotDetectDemo({super.key});
State<ScreenshotDetectDemo> createState() => _ScreenshotDetectDemoState();
}
class _ScreenshotDetectDemoState extends State<ScreenshotDetectDemo> {
final FlutterScreenshotDetect _detector = FlutterScreenshotDetect();
final List<FlutterScreenshotEvent> _events = [];
StreamSubscription<FlutterScreenshotEvent>? _subscription;
String? _error;
bool _changing = false;
void initState() {
super.initState();
_start();
}
void _start() {
_subscription = _detector.onScreenshot.listen((event) {
if (!mounted) return;
setState(() {
_events.insert(0, event);
if (_events.length > 100) _events.removeLast();
});
}, onError: (Object error) {
if (!mounted) return;
setState(() => _error = error.toString());
});
}
Future<void> _toggle() async {
setState(() {
_changing = true;
_error = null;
});
try {
final subscription = _subscription;
if (subscription == null) {
_start();
} else {
_subscription = null;
await subscription.cancel();
}
} catch (error) {
if (mounted) setState(() => _error = error.toString());
} finally {
if (mounted) setState(() => _changing = false);
}
}
void dispose() {
final subscription = _subscription;
if (subscription != null) unawaited(subscription.cancel());
_detector.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final listening = _subscription != null;
return Scaffold(
appBar: AppBar(
title: const Text('Screenshot Detector Example'),
),
body: SafeArea(
child: Column(
children: [
ListTile(
title: Text('Screenshots detected: ${_events.length}'),
subtitle: Text(listening ? 'Listening' : 'Stopped'),
trailing: IconButton(
tooltip: listening ? 'Stop listening' : 'Start listening',
onPressed: _changing ? null : _toggle,
icon: Icon(listening ? Icons.pause : Icons.play_arrow),
),
),
if (_error != null)
Padding(
padding: const EdgeInsets.all(16),
child: SelectableText(_error!),
),
Expanded(
child: ListView.builder(
itemCount: _events.length,
itemBuilder: (context, index) {
final event = _events[index];
return ListTile(
leading: const Icon(Icons.screenshot),
title: Text('Method: ${event.method}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Time: ${event.timestamp}'),
if (event.path != null) Text('Path: ${event.path}'),
],
),
);
},
),
),
],
),
),
);
}
}
页面的调用顺序很直接:initState() 创建订阅,原生事件到达后插入列表头部,图标按钮调用 _toggle() 取消或重建订阅,页面销毁时再次做幂等取消。mounted 判断用来防止页面已经退出后仍调用 setState(),_changing 则避免用户连续点击导致取消与恢复交叉。
八、真机操作过程
这轮截图不只保留了最终页,还保留了初始、暂停和恢复三个中间状态。

图 6:启动后计数为 0,页面显示 Listening。捕获这张系统截图后,插件随即收到第一个事件。

图 7:第一次事件到达后点击暂停,状态变为 Stopped,计数为 1。
我在 Stopped 状态下再次让系统截图,然后点击播放图标恢复订阅。如果取消没有生效,这时计数应该已经变成 2;实际页面仍然是 1。

图 8:恢复监听时计数保持 1,证明暂停期间的截图没有被投递给 Flutter。
最后在 Listening 状态再做一次系统截图,页面增加到 2,结果就是开头的图 1。这组过程同时验证了正常投递、取消无投递和重订阅后恢复投递,不是只看到 App 能打开就结束。
九、检查、构建与设备验证
2026 年 9 月 11 日在仓库根目录和 example 中复跑:
flutter analyze
flutter test
node --test test/ohos_lifecycle_test.cjs
cd example
flutter analyze
flutter test
flutter build hap --debug --no-codesign
| 检查项 | 实测结果 |
|---|---|
| 插件静态分析 | No issues found |
| 插件 Dart 测试 | 10 项通过 |
| ArkTS 生命周期测试 | 8 项通过,Node 有实验性 API 警告 |
| Example 静态分析 | No issues found |
| Example Widget 测试 | 1 项通过 |
| 无签名 HAP | 构建成功 |
| 签名、覆盖安装和启动 | 真机通过 |

图 9:静态分析无问题,10 项 Dart 和 9 项 ArkTS/Widget 用例共 19 项通过。
最早一轮真机验收还用 HDC UITest 注入音量减和电源组合键,订阅时计数从 0 变为 1,取消后保持 1,重新订阅后再变为 2。补图轮使用系统 snapshot_display 复现了同样的页面状态。两类触发都是系统真实截图,不是向 Flutter 伪造一个事件 Map。

图 10:另一轮真机记录汇总,保留了 count、method、path 以及取消与重订阅结果。
本次只验证了一台 API 26 真机、当前前台 Ability 窗口和两类系统截图触发方式。控制中心入口、后台投递、多实例真机行为、Ability 重建和其他系统版本没有全部覆盖,所以结论是“本次环境通过”,不是“所有鸿蒙设备全面兼容”。
十、实际接入容易踩的坑
Q1:收到事件却没有 path
- 现象:
method和timestamp正常,path是null。 - 原因: OHOS 公共窗口截图回调不提供文件保存路径。
- 解决方法: 把事件当作通知,业务对 path 做可空处理,不自行拼凑路径。
- 验证结果: 真机两次事件均正常投递,path 保持
null。
Q2:调用 detector.dispose() 后直接订阅仍在
- 现象: 页面通过
onScreenshot.listen()创建订阅,只调用detector.dispose()后以为已经取消。 - 原因:
dispose()只管理startListening()在 detector 内部持有的订阅,直接订阅属于调用方。 - 解决方法: 保存
StreamSubscription,在页面dispose()中调用cancel()。 - 验证结果: Dart 单测覆盖了两种订阅的所有权边界。
Q3:暂停按钮快速连点后状态乱了
- 现象: 取消订阅尚未完成时又执行恢复,界面状态和底层订阅可能不一致。
- 原因:
StreamSubscription.cancel()是异步操作,连续切换会让两次状态变更交叉。 - 解决方法: 用
_changing暂时禁用按钮,等本次切换完成后再允许点击。 - 验证结果: Widget 测试覆盖启动、暂停、恢复和页面销毁。
Q4:把监听误当成防截图
- 现象: 接入后用户仍能保存截图。
- 原因: 插件只在系统截图后投递事件,不修改窗口安全属性。
- 解决方法: 需要隐私防护时另行接入窗口防截图能力;需要事后提示或记录时才使用本库。
- 验证结果: 真机截图实际生成,同时 App 收到事件,与库的设计边界一致。
十一、什么时候适合使用
适合
- 票据、证件、聊天或内容页在截图后显示隐私提示。
- 在前台页面中统计截图行为,且可以接受 OHOS 不返回文件路径。
- 项目愿意在正式 OHOS TAG 发布前锁定已验证 commit。
暂不适合
- 需要阻止截图或录屏的强安全页面。
- 必须获取截图文件、图片内容或精确保存路径的功能。
- 需要保证所有截图入口、所有系统版本和后台状态都投递事件的场景。
十二、总结
flutter_screenshot_detect 在这个 Flutter 鸿蒙 Demo 中完成了一件很具体的事:当前应用窗口发生系统截图时,将事件方式和时间送到 Dart 页面。使用时最重要的是锁定真正受测的 AtomGit commit、为直接流订阅成对调用 cancel(),并把 OHOS 的 path=null 当作正常平台行为。
本次静态分析、19 项自动化、无签名 HAP 构建和 API 26 真机的订阅、取消、恢复都已通过。它不阻止截图、不读取文件、不监听录屏,这些限制需要在产品需求确认时先说清。
十三、参考链接
- flutter_screenshot_detect AtomGit 仓库
- 完整 example(受测提交)
- Flutter OH 版本标签
- Flutter OH 环境搭建指南
- HarmonyOS Window screenshot 事件文档
欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
更多推荐
所有评论(0)