原理

Flutter 本身没有防截屏 API,需要借助原生平台能力:

平台 原理
Android 设置 WindowManager.LayoutParams.FLAG_SECURE
iOS 覆盖一层 UITextField.isSecureTextEntry 的视图层

推荐 secure_display 插件,它封装了双端原生实现,调用简单。

安装

1
flutter pub add secure_display

快速上手

1
2
3
4
5
6
7
8
9
import 'package:secure_display/secure_display.dart';

final _secureScreen = SecureScreen();

// 开启防截屏
await _secureScreen.activate();

// 关闭防截屏(恢复截屏能力)
await _secureScreen.dispose();

完整实现

实际项目中不能只是”调一下 API”,需要生命周期感知状态管理

1. 核心服务

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
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:secure_display/secure_display.dart';

/// 防截屏策略服务
/// 负责管理 SecureScreen 实例的生命周期
abstract final class ScreenCaptureService {
static SecureScreen? _activeSecureScreen;

/// 根据业务条件开启/关闭防截屏
static Future<void> syncScreenCapture({
required bool shouldBlock,
}) async {
// Web 平台不支持 FLAG_SECURE
if (kIsWeb) return;
if (!Platform.isAndroid && !Platform.isIOS) return;

if (shouldBlock) {
// 已激活则跳过,避免重复调用
_activeSecureScreen ??= SecureScreen();
await _activeSecureScreen!.activate();
} else {
// 关闭:先释放再置空
await _activeSecureScreen?.dispose();
_activeSecureScreen = null;
}
}

/// Widget 销毁时调用,确保释放,防止状态泄漏
static Future<void> dispose() async {
await _activeSecureScreen?.dispose();
_activeSecureScreen = null;
}
}

关键细节

  • _activeSecureScreen ??= SecureScreen() — 懒创建,已激活时不重复 new,避免不必要的开销
  • dispose()= null — 确保原生资源完全释放
  • kIsWeb 和平台判断 — Web 端不支持 FLAG_SECURE,需要前置 return

2. 生命周期感知

App 切到后台再回来,系统可能清除 FLAG_SECURE,需要在 resumed 时重新激活:

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
import 'package:flutter/material.dart';

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
// 记录当前是否处于防截屏状态
// 用于 resumed 时判断是否需要重新激活
bool _isSecure = false;

@override
void initState() {
super.initState();
// 注册生命周期监听
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
// 取消监听 + 释放 secure 资源,两者缺一不可
WidgetsBinding.instance.removeObserver(this);
ScreenCaptureService.dispose();
super.dispose();
}

/// App 生命周期回调
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// 从后台切回前台 + 之前已启用防截屏 → 重新激活
if (state == AppLifecycleState.resumed && _isSecure) {
ScreenCaptureService.syncScreenCapture(shouldBlock: true);
}
}

/// 开启防截屏
Future<void> enableBlockScreenCapture() async {
await ScreenCaptureService.syncScreenCapture(shouldBlock: true);
setState(() => _isSecure = true);
}

/// 关闭防截屏
Future<void> disableBlockScreenCapture() async {
await ScreenCaptureService.syncScreenCapture(shouldBlock: false);
setState(() => _isSecure = false);
}

@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
);
}
}

3. 按条件动态切换

实际场景中,防截屏通常是有条件的——比如某些页面需要、某些状态需要。只要在任何时机调用 syncScreenCapture(shouldBlock: true/false) 即可随时切换。

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
/// 示例:登录成功后根据用户角色开启防截屏
Future<void> onLoginSuccess(UserInfo user) async {
// 假设:vip 等级 < 2 的用户需要防截屏
final bool needSecure = user.vipLevel < 2;
await ScreenCaptureService.syncScreenCapture(shouldBlock: needSecure);
}

/// 示例:进入特定页面时开启,离开时关闭
class SecretPage extends StatefulWidget {
@override
State<SecretPage> createState() => _SecretPageState();
}

class _SecretPageState extends State<SecretPage> {
@override
void initState() {
super.initState();
// 进入页面开启防截屏
ScreenCaptureService.syncScreenCapture(shouldBlock: true);
}

@override
void dispose() {
// 离开页面恢复截屏
ScreenCaptureService.syncScreenCapture(shouldBlock: false);
super.dispose();
}

@override
Widget build(BuildContext context) => /* ... */;
}

踩坑

iOS 后台切回黑屏

secure 视图层与 Flutter 渲染管线存在竞争,切回前台时可能短暂黑屏。

解决:在 didChangeAppLifecycleStateresumed 回调中,延迟几百毫秒再执行页面操作(特别是 Navigator 跳转),等 secure_display 稳定即可。

1
2
3
4
5
6
7
8
9
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// 延迟 550ms 等 secure_display 稳定
Future.delayed(const Duration(milliseconds: 550), () {
if (_isSecure) ScreenCaptureService.syncScreenCapture(shouldBlock: true);
});
}
}

状态泄漏

dispose() 时忘记释放 SecureScreen,会导致退出页面后仍无法截屏。

解决:务必在 dispose() 中调用 ScreenCaptureService.dispose(),放在 removeObserver 旁边,避免遗漏。

Android 初次激活时机

secure_display.activate() 底层调用 FLAG_SECURE,需要在 Activity 完全创建后才能生效。如果在 initState 或页面刚进入时立即调用,标志可能被后续 Activity 初始化覆盖。

解决:Android 端首次激活建议加 200ms 延迟,或放在 addPostFrameCallback 中:

1
2
3
WidgetsBinding.instance.addPostFrameCallback((_) async {
await ScreenCaptureService.syncScreenCapture(shouldBlock: true);
});

总结

关注点 做法
开启 secure_display.activate()
关闭 secure_display.dispose()
生命周期 WidgetsBindingObserver.resumed 重新激活
释放 dispose() 必须调用 dispose()
Web 端 kIsWeb 直接 return
Android 延迟 首次激活建议放 addPostFrameCallback

参考