问题】
DraggableScrollableSheet 初始化时传入了DraggableScrollableController实例。但是在拖动手势触发时,总是提示_sheetController.isAttached == false
。
原因】
DraggableScrollableSheet 的 builder返回了 SizedBox.shrink()
SizedBox.shrink() 这是一个零尺寸的空组件,所以DraggableScrollableSheet 的内容区域实际上是空的。更重要的是,在某些情况下,特别是当 initialChildSize 和 minChildSize 非常小(接近于0)时,Flutter 的 Widget 树优化机制可能会认为这个 DraggableScrollableSheet 在视觉上没有尺寸,或者不需要渲染。
当一个 Widget 不需要被渲染时,它的 State 可能不会被创建,或者被销毁(dispose)。这就导致了 DraggableScrollableSheet 没有机会去“关联”那个 _sheetController。
解决方案】
把 return const SizedBox.shrink(); 替换成了一个包含 ListView 的 SizedBox。
正因为 SizedBox.shrink() 会被 Flutter 的优化机制认为是一个可以被“忽略”的空组件,从而导致 DraggableScrollableSheet 无法正确关联其控制器。
ListView 和 scrollController
这是最关键的一步。DraggableScrollableSheet 被设计用来管理一个可滚动的子组件。它的 builder 方法会提供一个 scrollController,并期望这个 controller 被设置给其内部的可滚动组件(如 ListView、GridView、SingleChildScrollView 等)。
即使 ListView 的 children 是空的,height 是0,但将 builder 提供的 scrollController 传递给了 ListView。这个动作建立了一个明确的连接,告诉 DraggableScrollableSheet:“嘿,这就是你要管理的那个滚动视图。”
这个连接一旦建立,DraggableScrollableSheet 就会成功地将自己附着(attach)到它的控制器 _sheetController 上,从而使得 _sheetController.isAttached 变为 true。
总结】
controller.isAttached == false 的本质原因是:控制器(Controller)实例已经存在,但它还没有和任何一个界面上的 Widget 状态(State)建立起双向绑定关系。控制器本身不知道要控制谁,直到一个 Widget 告诉它:“由你来控制我”。
isAttached == false 的常见情况
- 时机过早:在 Widget 构建完成前使用 Controller
- 描述:这是最常见的原因。在 State 的 initState 方法中,Widget 的 build 方法还没有被调用,意味着你要控制的那个 Widget(比如 ListView)此刻还不存在于 Widget 树中。此时尝试调用需要“附着”才能使用的方法(如 scrollController.jumpTo())就会失败。
- 例子:在 initState 里直接调用 _scrollController.jumpTo(100)。
- Widget 已被移除:Widget 不在当前的 Widget 树中
- 描述:由于条件渲染(例如 if 语句或 Visibility Widget),你要控制的 Widget 可能被从树中移除了。当它被移除时,它与 Controller 的关联就会被解除(detach)。
- 例子:一个 TextField 包在 if (_showTextField) 中,当 _showTextField 变为 false 时,TextField 被销毁,它的 TextEditingController 就会变为 isAttached == false。
- Widget 被优化:Widget 因内容或尺寸问题被 Flutter 忽略
- 描述:这是您上次遇到的情况。即使 Widget 在代码层面存在于树中,但如果它的尺寸为零(如 SizedBox.shrink())或其内容不满足渲染要求,Flutter 的渲染引擎可能会为了优化性能而“跳过”它,不为其创建完整的 State 或 RenderObject。没有 State,自然就无法附着 Controller。
- 例子:DraggableScrollableSheet 的 builder 返回一个 SizedBox.shrink()。
- 忘记关联:Controller 从未被传递给 Widget
- 描述:这是一个简单的逻辑错误。你创建了 Controller 实例,但在构建 Widget 时忘记了将它赋值给 Widget 的 controller 属性。
- 例子:final _scrollController = ScrollController(); 但在 ListView 中忘记写 controller: _scrollController。
- 生命周期不匹配:Widget 被销毁后仍在使用 Controller
- 描述:当一个页面被 Navigator.pop(),或者一个 StatefulWidget 被销毁(disposed)后,它所关联的 Controller 也会被解除附着。如果在此时(例如在一个异步回调 then 中)尝试使用这个 Controller,isAttached 也会是 false。