鸿蒙 Flutter 页面怎么感知防窥状态并调整 UI 可见性
适合谁看
-
想把原生事件接回 Flutter UI 的人
-
想用简单状态对象承接系统能力的人
-
正在做收藏页、隐私页防窥处理的人
问题背景
很多项目把系统能力接完之后,Flutter 页面几乎没有变化。这意味着能力虽然"接入成功",但用户其实感受不到。
防窥保护不是这样。它要求页面必须真正响应两种状态:
-
可见(PASS) — 正常展示内容
-
隐藏(HIDE) — 需要隐藏敏感内容
如果 Flutter 侧只是"收到了事件但页面没变化",那防窥保护就还没真正落地。
项目中的真实场景
食界探味当前 Flutter 侧的防窥实现主要在:
-
app/lib/core/platform/anti_peep_protection_channel.dart— 事件接收和状态管理 -
app/lib/app.dart— 页面级激活/取消防窥
使用场景:收藏页的隐私保护。当用户在收藏页时,如果有旁人偷看,系统会触发防窥,Flutter 侧需要隐藏收藏内容。
核心实现
一、事件翻译——把原生事件变成 Flutter 可消费状态
鸿蒙侧回传的是原生事件字符串:
'HIDE' → 需要隐藏
'PASS' → 恢复可见
'DEACTIVATE' → 防窥已取消
Flutter 侧把它们翻译成简洁的枚举:
enum AntiPeepVisibilityState { visible, hidden }
翻译逻辑:
switch (event) {
case 'HIDE':
visibilityState.value = AntiPeepVisibilityState.hidden;
break;
case 'PASS':
case 'DEACTIVATE':
visibilityState.value = AntiPeepVisibilityState.visible;
break;
}
这样页面就不需要理解原生事件名字,只需要关心 visible 和 hidden 两个状态。
二、用 ValueNotifier 保持实现轻量
当前选择 ValueNotifier 很合适,因为这里的状态很简单——只有可见和隐藏两个主要值。
class AntiPeepProtectionChannel {
static final ValueNotifier<AntiPeepVisibilityState> visibilityState =
ValueNotifier(AntiPeepVisibilityState.visible);
}
不需要为了这件事额外引入 Riverpod Provider 或 Bloc。ValueNotifier 的优势:
|
维度 |
ValueNotifier |
Riverpod/Bloc |
|---|---|---|
|
复杂度 |
极低 |
较高 |
|
适合场景 |
2-3 个状态值 |
复杂状态机 |
|
监听方式 |
ValueListenableBuilder |
Consumer/BlocBuilder |
|
生命周期 |
自动 |
需要手动管理 |
对于"可见/隐藏"这种二元状态,ValueNotifier 是最轻量的选择。
三、页面如何监听状态变化
Flutter 页面通过 ValueListenableBuilder 监听防窥状态:
ValueListenableBuilder<AntiPeepVisibilityState>(
valueListenable: AntiPeepProtectionChannel.visibilityState,
builder: (context, state, child) {
if (state == AntiPeepVisibilityState.hidden) {
// 防窥模式:隐藏敏感内容
return Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.visibility_off, size: 48),
),
);
}
// 正常模式:展示原始内容
return child!;
},
child: const OriginalContentWidget(), // 原始内容
)
当 visibilityState 变化时,ValueListenableBuilder 会自动 rebuild,页面 UI 随之切换。
四、页面级激活和取消防窥
防窥不是全局常驻的,而是和页面生命周期绑定。在 app.dart 的 _ScaffoldWithNavBarState 中:
void _scheduleCollectionProtectionSync(bool shouldProtect) {
if (_lastCollectionProtectionTarget == shouldProtect) {
return; // 状态没变,不重复操作
}
_lastCollectionProtectionTarget = shouldProtect;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _lastCollectionProtectionTarget != shouldProtect) {
return; // 页面已销毁或状态已变,不执行
}
if (shouldProtect) {
AntiPeepProtectionChannel.activateCollectionProtection();
} else {
AntiPeepProtectionChannel.deactivateCollectionProtection();
}
});
}
调用时机:
// 在 build() 中根据当前 Tab 判断
_scheduleCollectionProtectionSync(
isLoggedIn && widget.navigationShell.currentIndex == 2, // index 2 = 收藏页
);
这意味着:
-
用户切到收藏页 → 激活防窥
-
用户切离收藏页 → 取消防窥
-
用户退出页面 → 取消防窥
五、页面退出时必须取消防窥
这是一个必须处理的边界情况:
@override
void dispose() {
if (_lastCollectionProtectionTarget == true) {
AntiPeepProtectionChannel.deactivateCollectionProtection();
}
super.dispose();
}
如果不处理,用户退出应用后鸿蒙侧可能还在监听防窥事件,导致资源泄漏。
六、防重复激活——_lastCollectionProtectionTarget
bool? _lastCollectionProtectionTarget;
这个变量的作用是防止重复激活/取消防窥。在 build() 中每次都会调用 _scheduleCollectionProtectionSync,但只有状态真正变化时才会执行 activate/deactivate。
void _scheduleCollectionProtectionSync(bool shouldProtect) {
if (_lastCollectionProtectionTarget == shouldProtect) {
return; // 状态没变,跳过
}
// ...
}
同时在 addPostFrameCallback 中还要再检查一次:
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _lastCollectionProtectionTarget != shouldProtect) {
return; // 页面已销毁或状态已变,跳过
}
// ...
});
这两次检查确保了:
-
同一状态不会重复激活
-
页面销毁后不会执行过期的操作
-
异步回调时状态不会冲突
七、完整的 Flutter 侧防窥流程
应用启动
│
├─ AntiPeepProtectionChannel.initialize() ← 初始化事件监听
│
▼
用户切到收藏页(index == 2)
│
├─ _scheduleCollectionProtectionSync(true)
│ → activateCollectionProtection() ← 通知鸿蒙激活
│ → 鸿蒙:检查开关 → 订阅事件 → 获取初始状态
│
▼
旁人偷看(鸿蒙检测到 HIDE 事件)
│
├─ 鸿蒙:emitEvent('HIDE') ← 回传 Flutter
│
├─ Flutter:visibilityState.value = hidden ← 状态变化
│
├─ ValueListenableBuilder rebuild ← 页面重建
│ → 显示占位 UI(灰色背景 + 图标)
│
▼
旁人离开(鸿蒙检测到 PASS 事件)
│
├─ 鸿蒙:emitEvent('PASS') ← 回传 Flutter
│
├─ Flutter:visibilityState.value = visible ← 状态恢复
│
├─ ValueListenableBuilder rebuild ← 页面重建
│ → 恢复显示原始内容
│
▼
用户切离收藏页
│
├─ _scheduleCollectionProtectionSync(false)
│ → deactivateCollectionProtection() ← 通知鸿蒙取消
│ → 鸿蒙:cleanup() → 取消订阅
│
▼
页面退出
│
├─ dispose()
│ → deactivateCollectionProtection() ← 确保取消
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
事件接收 + 状态管理 |
|
|
页面级激活/取消防窥 |
|
|
鸿蒙原生插件 |
两层防窥的分工
防窥保护实际上有两层,它们各司其职:
|
层 |
负责什么 |
实现方式 |
|---|---|---|
|
鸿蒙系统层 |
检测偷看 + 设置系统蒙层 |
|
|
Flutter 页面层 |
隐藏敏感 UI 内容 |
|
两层的关系:
鸿蒙系统蒙层
└─ 系统级遮罩,覆盖整个窗口
└─ 用户看到的是一层半透明遮罩
Flutter 页面内容
└─ 应用级隐藏,只隐藏特定内容
└─ 收藏列表、菜品卡片等敏感内容
两层同时工作时,用户既看不到系统蒙层下的内容,也看不到 Flutter 页面的敏感数据。即使有人绕过了系统蒙层,Flutter 侧的内容也已经被隐藏了。
常见坑
-
收到 HIDE 事件后页面什么也不变 — 没有 ValueListenableBuilder 监听,事件被忽略
-
直接在页面里解析原生事件字符串 — 应该在 Channel 层翻译成枚举,页面不碰原生细节
-
不区分"系统蒙层"和"Flutter 视图可见性" — 两层各司其职,不能只做一层
-
页面退出后还保持隐藏态 — dispose 时必须 deactivate
-
重复激活防窥 — 用
_lastCollectionProtectionTarget防重复 -
build() 中重复调用 activate — 用
addPostFrameCallback+ 状态检查避免 -
MissingPluginException 不处理 — 非鸿蒙平台没有这个插件,需要 catch 后忽略
可复用模板
Channel 层模板
enum VisibilityState { visible, hidden }
class ProtectionChannel {
static final ValueNotifier<VisibilityState> visibilityState =
ValueNotifier(VisibilityState.visible);
static void initialize() {
_channel.setMethodCallHandler((call) async {
if (call.method == 'onEvent') {
final event = call.arguments['event'] as String?;
switch (event) {
case 'HIDE':
visibilityState.value = VisibilityState.hidden;
break;
case 'PASS':
visibilityState.value = VisibilityState.visible;
break;
}
}
});
}
}
页面监听模板
ValueListenableBuilder<VisibilityState>(
valueListenable: ProtectionChannel.visibilityState,
builder: (context, state, child) {
if (state == VisibilityState.hidden) {
return const SafePlaceholder(); // 隐藏时的安全占位
}
return child!;
},
child: const SensitiveContent(), // 原始敏感内容
)
页面级激活模板
bool? _lastProtectionTarget;
void syncProtection(bool shouldProtect) {
if (_lastProtectionTarget == shouldProtect) return;
_lastProtectionTarget = shouldProtect;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _lastProtectionTarget != shouldProtect) return;
if (shouldProtect) {
ProtectionChannel.activate();
} else {
ProtectionChannel.deactivate();
}
});
}
@override
void dispose() {
if (_lastProtectionTarget == true) {
ProtectionChannel.deactivate();
}
super.dispose();
}
本篇总结
防窥能力是否好用,关键在 Flutter 页面有没有接住状态变化。食界探味当前的做法:
-
Channel 层翻译事件 — 原生
HIDE/PASS→ Flutterhidden/visible -
ValueNotifier 暴露状态 — 极简,只有两个值
-
ValueListenableBuilder 监听 — 状态变化时自动 rebuild
-
页面级激活/取消 — 跟随 Tab 切换,不全局常驻
-
dispose 时取消 — 确保不泄漏
-
防重复激活 —
_lastCollectionProtectionTarget+addPostFrameCallback
只有插件没有页面响应,这项能力就还没真正落地。用简单状态对象先把事件翻译成 UI 可消费模型,是很实用的做法。
更多推荐




所有评论(0)