SuperAPI AI 工具集

nested-flag-settings-auto-disable

编程与开发

查看项目

更新于 10-17 16:15

技能介绍

该条目来自 maxrave-dev/kotlin-footguns,讨论设置界面中“由父条件门控的子开关”这一常见缺陷及其三层防护方案。

用途与问题背景

设置项常出现嵌套开关:某个子开关只有在父条件成立时才有意义,例如凭证已存在、密钥已输入、某项能力可用。当父条件为 false 时,界面把该行绘制为禁用状态,问题随之而来:禁用意味着用户无法再将其关闭,因此父条件变为 false 时仍处于开启状态的子开关,只能通过代码来关闭。

三层防护

文中提出三层防护,分别覆盖不同情况:

  1. 行级:当行检测到门控关闭时,将自身子开关置为关闭。
  2. 状态持有者:在撤回父条件的任何路径上清除子开关。
  3. 消费方:拒绝单独依据子开关行动,需组合两个标志来门控。

行级自动禁用示例

文中给出适配后的 Compose 示例,SettingItem 接收 isEnable、switch、onDisable 等参数,并用 LaunchedEffect(isEnable) 在门控翻转为 false 时触发 onDisable。关键点是:以 isEnable 为 key 而非 Unit,这样会话中途父条件变为 false 时也会重新运行副作用,而不仅首次组合时运行一次。调用处对回调加守卫,仅当子开关当前为开时才写入:

SettingItem( switch = (childEnabled to { viewModel.setChildEnabled(it) }), isEnable = parentSatisfied, onDisable = { if (childEnabled) viewModel.setChildEnabled(false) }, )

陷阱

  • LaunchedEffect(Unit) 是缺陷所在:Unit 永不变化,副作用只在进入组合时运行一次;若屏幕打开期间父条件变为 false(例如用户在上一行退出登录),子开关保持开启且行已禁用,用户无法撤销。
  • 只有 isEnable 而没有清除回调是常见的“半修复”:isEnable 只改变行的外观(变暗、不再接受点击),标志值保持不变,于是恰好落入本想避免的状态。onDisable 是可选参数,省略它仍能编译并通过评审。
  • 回调不加守卫会导致写入循环:副作用在首次组合时也会运行,若 onDisable 无条件写入且设置存储写入后重新发出,写入触发重组又触发写入。用 if (childEnabled) 守卫使回调幂等。守卫应放在回调里,因为 onDisable 独立于该行的 switch,行本身无法知道该检查哪个标志。
  • 门控值必须被观察而非采样:LaunchedEffect(isEnable) 仅在 isEnable 实际变化时重启,这要求它来自订阅(如从存储收集的状态),而非构建屏幕时读取一次的值。对永不更新的值使用正确的 key 与 Unit 无异。
  • 应变灰而非隐藏:隐藏行会将其移出组合,自动禁用副作用根本不会运行,标志卡住且界面无恢复路径,同时用户也看不到解释。应保留行与副标题、变暗、禁用交互而不只是外观。文中给出 Box 修饰符示例,仅在 onClick 存在且 isEnable 为真时添加可点击修饰符,并在 !isEnable 时应用灰色。
  • 合法例外:隐藏整个构建不支持的部分(能力被编译排除、该风味缺少凭证),这是静态事实而非运行时门控,不存在卡住的标志。
  • 懒列表中离屏行不存在:设置在懒列表中时,行内副作用仅在该行滚动到可见时运行。在顶部退出登录,下方子开关从未组合、从未运行副作用,保持开启。因此行级自动禁用不能作为事实来源,只是用户在场时的便利。
  • 同一症状可能有三种原因:“开关卡在开启”可能由三层中任一层失败导致。在改副作用 key 前,应检查父条件撤回路径是否重置子开关、消费方是否单独读取子开关。只修行级会让不经过该屏幕的路径仍然卡住。

验证方法

文中给出排查命令:

grep -rn "LaunchedEffect(Unit)" --include=".kt" . | grep -v "/build/" grep -rn -A6 "isEnable = " --include=".kt" . | grep -v "/build/"

第二条保留尾随的 = 以返回声明门控的行而非所有读取。对第一条命中项,判断其主体是否读取屏幕打开期间可能变化的值,这些是需要改 key 的。

再将每个门控与其清除逻辑交错阅读:

grep -n -A6 'isEnable = ' <settings-screen> | grep -E 'isEnable|onDisable'

预期会看到缺口,按行判断:静态构建事实(如 isEnable = getPlatform() == … 或 isEnable = true)无需清除,其余缺口就是会变灰并保持设置的开关。

行为验证分三次运行,对应三层:

  1. 屏幕打开:打开子开关,然后从同屏某行撤回父条件。子开关必须在眼前翻转为关,行应变暗且不可交互。
  2. 屏幕打开、行已滚走:同上,但子开关行远在屏幕外。滚回后必须为关。若为开,说明行级副作用在做撤回路径应做的工作。
  3. 屏幕关闭:在应用其他位置撤回父条件,强制停止并重启,在不打开设置的情况下读取存储值。仍为关说明状态持有者正确。

最后独立确认消费方:手动在存储中将子开关设为开且父条件缺失,检查子系统从不启动——这是另外两层被绕过时仍能成立的防护。

适用场景与限制

该内容适用于 Kotlin/Compose 设置界面中由父条件门控的子开关场景。文中未提供具体库版本、平台、收费或效果数据。