鸿蒙 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;
}

这样页面就不需要理解原生事件名字,只需要关心 visiblehidden 两个状态。

二、用 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;  // 页面已销毁或状态已变,跳过
  }
  // ...
});

这两次检查确保了:

  1. 同一状态不会重复激活

  2. 页面销毁后不会执行过期的操作

  3. 异步回调时状态不会冲突

七、完整的 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 页面有没有接住状态变化。食界探味当前的做法:

  1. Channel 层翻译事件 --- 原生 HIDE/PASS → Flutter hidden/visible

  2. ValueNotifier 暴露状态 --- 极简,只有两个值

  3. ValueListenableBuilder 监听 --- 状态变化时自动 rebuild

  4. 页面级激活/取消 --- 跟随 Tab 切换,不全局常驻

  5. dispose 时取消 --- 确保不泄漏

  6. 防重复激活 --- _lastCollectionProtectionTarget + addPostFrameCallback

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

相关推荐
天天开发1 小时前
Flutter状态管理新宠:RiverPod全面解析与实战指南
android·flutter
小雨下雨的雨1 小时前
HarmonyOS ArkUI训练营入门-组件掌握系列-Grid 网格布局深度解析-PC版本
学习·华为·harmonyos·鸿蒙·鸿蒙系统
UXbot10 小时前
如何选择适合公司项目的UI设计工具?企业选型指南
前端·低代码·ui·团队开发·原型模式·设计规范·web app
「、皓子~11 小时前
海狸IM 2.0 正式发布:六端齐发,开源 IM 迈入新阶段
flutter·electron·开源软件·ai编程·交友·im
Davina_yu12 小时前
定时器与任务调度:setTimeout与setInterval的正确使用(19)
harmonyos·鸿蒙·鸿蒙系统
祭曦念13 小时前
【共创季稿事节】鸿蒙原生ArkTS布局深度解析_GridRow_Row_Column混合栅格布局实战
华为·harmonyos
kiros_wang13 小时前
鸿蒙 ArkUI:V1 与 V2 装饰器全面对比与迁移指南
ubuntu·华为·harmonyos
古德new13 小时前
鸿蒙PC迁移:Photoflare Qt 图片编辑器鸿蒙PC适配全记录
qt·编辑器·harmonyos
不羁的木木13 小时前
HarmonyOS 6.1.0 创新特性技术精讲之沉浸光感
华为·harmonyos