欢迎光临
我们一直在努力

鸿蒙 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() ← 确保取消

    关键代码位置

    文件

    作用

    app/lib/core/platform/anti_peep_protection_channel.dart

    事件接收 + 状态管理

    app/lib/app.dart

    页面级激活/取消防窥

    app/ohos/entry/src/main/ets/plugins/AntiPeepProtectionPlugin.ets

    鸿蒙原生插件

    两层防窥的分工

    防窥保护实际上有两层,它们各司其职:

    负责什么

    实现方式

    鸿蒙系统层

    检测偷看 + 设置系统蒙层

    dlpAntiPeep.setAntiPeepMaskLayer()

    Flutter 页面层

    隐藏敏感 UI 内容

    ValueListenableBuilder 切换 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 → Flutter hidden/visible

  • ValueNotifier 暴露状态 — 极简,只有两个值

  • ValueListenableBuilder 监听 — 状态变化时自动 rebuild

  • 页面级激活/取消 — 跟随 Tab 切换,不全局常驻

  • dispose 时取消 — 确保不泄漏

  • 防重复激活 — _lastCollectionProtectionTarget + addPostFrameCallback

  • 只有插件没有页面响应,这项能力就还没真正落地。用简单状态对象先把事件翻译成 UI 可消费模型,是很实用的做法。

    赞(0)
    未经允许不得转载:171主机测评 » 鸿蒙 Flutter 页面怎么感知防窥状态并调整 UI 可见性
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址