适合谁看

  • 想把原生事件接回 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 可消费模型,是很实用的做法。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐