Skip to content

fix: guard _runFocus against disposed controller (forward() after dispose) - #233

Open
thinklanguages wants to merge 1 commit into
RafaelBarbosatec:masterfrom
thinklanguages:fix-runfocus-disposed-controller-guard
Open

fix: guard _runFocus against disposed controller (forward() after dispose)#233
thinklanguages wants to merge 1 commit into
RafaelBarbosatec:masterfrom
thinklanguages:fix-runfocus-disposed-controller-guard

Conversation

@thinklanguages

Copy link
Copy Markdown

Problem

AnimatedFocusLightState._runFocus() is scheduled asynchronously — Future.delayed(Duration.zero, _runFocus) in initState, plus an await widget.beforeFocus(...) — and then calls _controller.forward() with no mounted check.

If the widget is disposed during that async gap (the host overlay is removed via removeOverlayEntry() / a route pop while the tutorial is mid-presentation), forward() runs on a disposed AnimationController. _ticker is null, and because assert(_ticker != null) inside AnimationController.stop() is stripped in release builds, it throws an uncaught Null check operator used on a null value (_TypeError) instead of a debug assertion.

We hit this in production (release) on a coach mark shown on a screen the user navigated away from before the focus animation started. In debug it surfaces as:

AnimationController.forward() called after AnimationController.dispose()
'package:flutter/src/animation/animation_controller.dart': Failed assertion: '_ticker != null'
  AnimatedFocusLightState._runFocus (animated_focus_light.dart)

Fix

Add a mounted guard immediately before forward() in _runFocus:

  safeSetState(() { ... });

+ if (!mounted) return;

  await _controller.forward();
  _isAnimating = false;

safeSetState above is already mounted-guarded, so this is the only remaining unguarded controller access on that path. Minimal, no behavior change when the widget is still mounted.

(The same pattern exists on the develop branch's restructured _runFocus; happy to mirror it there if you prefer.)

_runFocus runs asynchronously (Future.delayed(Duration.zero) in initState,
plus an optional `await widget.beforeFocus`), then calls _controller.forward()
with no mounted check. If the widget is disposed during that async gap (the
host overlay is removed via removeOverlayEntry()/route pop while the tutorial
is mid-presentation), forward() runs on a disposed AnimationController whose
_ticker is null.

In release builds the `assert(_ticker != null)` inside
AnimationController.stop() is stripped, so instead of a debug assertion the app
throws an uncaught "Null check operator used on a null value" (_TypeError).
Reproduced in production on a coach mark shown on a screen the user navigated
away from.

Add a `mounted` guard immediately before forward(). safeSetState above is
already mounted-guarded, so this is the only remaining unguarded controller
access on that path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant