auto_dispose_guard 1.0.5 copy "auto_dispose_guard: ^1.0.5" to clipboard
auto_dispose_guard: ^1.0.5 copied to clipboard

Safe automatic disposal for Flutter controllers, streams, timers, services, and custom resources.

auto_dispose_guard #

CI coverage pub package License: MIT

Safe automatic disposal for Flutter controllers, streams, timers, and custom resources — across every state-management pattern.

AutoDisposeGuard removes repetitive dispose(), close(), and cancel() boilerplate while keeping cleanup idempotent, fail-safe, and crash-free.


What's New in 1.0.4 #

  • Flutter-Recommended Teardown Ordering — Reordered mixin cleanup to release registered resources before framework teardown (super.dispose()), ensuring ticker-dependent resources like AnimationController dispose cleanly.
  • Dynamic Duck-Type Detection — Probes standard teardown methods (dispose(), close(), cancel()) and status indicators (isDisposed, isClosed) using dynamic member access (reflection-free).
  • Sink Support — Native auto-disposal for standard Sink interfaces, mapped automatically to close().
  • Mounted Callback Guards — Adds safeExecute() and safeExecuteAsync() to AutoDisposeMixin to protect async callbacks (e.g. setState) post-unmount.
  • Retryable Disposals — Gracefully handles individual resource disposal errors via try/catch, allowing later retries without marking the resource as disposed on failure.

Installation #

dependencies:
  auto_dispose_guard: ^1.0.4
import 'package:auto_dispose_guard/auto_dispose_guard.dart';

API At a Glance #

API Best for
AutoDisposeMixin State classes — register resources inline
AutoDisposeScope Widget-tree scope for shared cross-widget resources
AutoDispose.of(context) Imperative registration from any widget
.autoDispose(context) Fluent one-liner registration
AutoDisposeBag Plain Dart classes, repositories, services
AutoDisposeBagMixin GetX, Provider / ChangeNotifier lifecycle owners
BlocAutoDisposeMixin Bloc, Cubit, Riverpod StateNotifier
AutoDisposeChangeNotifier Provider models, Riverpod ChangeNotifier notifiers

Widget State (AutoDisposeMixin) #

class _ProfileScreenState extends State<ProfileScreen>
    with SingleTickerProviderStateMixin, AutoDisposeMixin {

  late final name      = register(TextEditingController());
  late final focus     = register(FocusNode());
  late final animation = register(
    AnimationController(vsync: this, duration: kThemeAnimationDuration),
  );
  late final stream    = register(StreamController<String>.broadcast());

  @override
  Widget build(BuildContext context) => const SizedBox();

  // No dispose() override needed.
}

Extra shortcuts on AutoDisposeMixin #

// Dispose one resource early (e.g. when a tab closes):
disposeOf(animation);

// Remove from tracking without disposing (take back ownership):
unregister(stream);

// Check before re-registering:
if (!isRegistered(stream)) register(stream);

// Be notified when this State's scope fully closes:
addDisposeListener(() => debugPrint('screen cleaned up'));

Bloc / Cubit (flutter_bloc) #

No flutter_bloc dependency is added to your project — BlocAutoDisposeMixin works with any version.

class SearchCubit extends Cubit<SearchState> with BlocAutoDisposeMixin {
  SearchCubit(this._repo) : super(const SearchState()) {
    _sub = register(
      _repo.stream.listen(_onData),
      isDisposed: () => isClosed,   // skip if cubit already closed
    );
    _debounce = register(
      Timer(const Duration(milliseconds: 300), () {}),
    );
  }

  final DataRepository _repo;
  late final StreamSubscription<Data> _sub;
  late final Timer _debounce;

  @override
  Future<void> close() {
    disposeAutoDispose();   // ← release registered resources first
    return super.close();
  }
}
class AuthBloc extends Bloc<AuthEvent, AuthState> with BlocAutoDisposeMixin {
  AuthBloc(this._auth) : super(AuthInitial()) {
    _tokenSub = register(_auth.tokenStream.listen(_onToken));
    on<LogoutEvent>(_onLogout);
  }

  final AuthService _auth;
  late final StreamSubscription<Token> _tokenSub;

  @override
  Future<void> close() {
    disposeAutoDispose();
    return super.close();
  }
}

Riverpod — StateNotifier #

class CartNotifier extends StateNotifier<CartState> with BlocAutoDisposeMixin {
  CartNotifier(this._repo) : super(const CartState()) {
    _timer = register(
      Timer.periodic(const Duration(minutes: 5), (_) => _sync()),
    );
    _sub = register(_repo.cartStream.listen(_onCart));
  }

  final CartRepository _repo;
  late final Timer _timer;
  late final StreamSubscription<Cart> _sub;

  @override
  void dispose() {
    disposeAutoDispose();   // ← release registered resources first
    super.dispose();
  }
}

Provider / ChangeNotifier (AutoDisposeChangeNotifier) #

Extend AutoDisposeChangeNotifier instead of ChangeNotifier. All registered resources are disposed automatically — no override needed.

class CartModel extends AutoDisposeChangeNotifier {
  late final search = register(TextEditingController());
  late final scroll = register(ScrollController());
  late final _timer = register(
    Timer.periodic(const Duration(seconds: 30), (_) => _sync()),
  );

  void _sync() { /* fetch latest cart */ }

  // dispose() is handled automatically.
}
// With Provider:
ChangeNotifierProvider(create: (_) => CartModel())

Custom teardown is still possible:

@override
void dispose() {
  _socket.close();   // run before listeners are notified
  super.dispose();   // calls disposeAutoDispose() + ChangeNotifier.dispose()
}

GetX Controllers and Services #

class LoginController extends GetxController with AutoDisposeBagMixin {
  late final email    = register(TextEditingController());
  late final password = register(TextEditingController());
  late final timer    = register(
    Timer.periodic(const Duration(seconds: 1), (_) => _tick()),
  );

  @override
  void onClose() {
    disposeAutoDispose();
    super.onClose();
  }
}
class SocketService extends GetxService with AutoDisposeBagMixin {
  late final messages = register(StreamController<String>.broadcast());

  @override
  void onClose() {
    disposeAutoDispose();
    super.onClose();
  }
}

Plain Dart Classes (AutoDisposeBag) #

class DataRepository {
  DataRepository() : _bag = AutoDisposeBag(debugLabel: 'DataRepository');

  final AutoDisposeBag _bag;

  late final events  = _bag.register(StreamController<int>.broadcast());
  late final _timer  = _bag.register(
    Timer.periodic(const Duration(seconds: 10), (_) => _heartbeat()),
  );

  bool get isReady => !_bag.isDisposed;

  void dispose() => _bag.dispose();
}

Widget-Tree Scope (AutoDisposeScope) #

Wrap a route or subtree once, register from any descendant.

MaterialPageRoute<void>(
  builder: (_) => const AutoDisposeScope(
    debugLabel: 'ProfileRoute',
    child: ProfileScreen(),
  ),
);
// In any descendant widget's initState / didChangeDependencies:
final controller = TextEditingController().autoDispose(context);
AutoDispose.of(context).register(StreamController<int>());

Lifecycle Hooks #

class _HomeState extends State<HomeScreen> with AutoDisposeMixin {
  @override
  void initState() {
    super.initState();
    addDisposeListener(() {
      // Fires after every resource in this scope has been released.
      analytics.logEvent('home_screen_cleaned_up');
    });
  }
}

Already-Disposed Guards #

Pass an isDisposed probe to prevent double-dispose crashes.

late final socket = register(
  MySocket(),
  onDispose: () => socket.close(),
  isDisposed: () => socket.isClosed,
);

Implement DisposeState on your own types:

class MyCache implements Disposable, DisposeState {
  bool _closed = false;

  @override
  bool get isDisposed => _closed;

  @override
  void dispose() {
    if (_closed) return;
    _closed = true;
  }
}

Auto-Detection Table #

AutoDisposeGuard uses type checks first, then safe dynamic probing as a fallback — zero reflection overhead.

Type Auto-detected method Disposed probe
Disposable (our interface) dispose() —
Closeable (our interface) close() —
Cancellable (our interface) cancel() —
DisposeState (our interface) — isDisposed
ChangeNotifier dispose() dynamic isDisposed
StreamController close() isClosed
StreamSubscription cancel() —
Timer cancel() !isActive
Sink close() —
Duck-type fallback dynamic dispose() / close() / cancel() dynamic isDisposed / isClosed
Any other type pass onDispose callback pass isDisposed callback

For Bloc/Cubit (which have close() but do not implement our Closeable):

// Explicit callback — works with any framework type:
register(myCubit, onDispose: () => myCubit.close());

// Or use BlocAutoDisposeMixin directly in the Cubit class.

Core Guarantees #

  • O(1) identity-based registration and lookup.
  • LIFO disposal order — last registered, first released.
  • Idempotent — disposeAll() / disposeAutoDispose() are safe to call multiple times.
  • Fail-safe — one disposal error is logged; remaining resources still release.
  • Zero overhead in release builds — all debug logging is guarded by kDebugMode.

Production & Enterprise Semantics #

1. super.dispose() Teardown Order #

⚠️ DO NOT change this order. AutoDispose resources are released before super.dispose() to support AnimationController / Ticker lifecycles.

AutoDisposeMixin intentionally invokes _autoDisposeManager.release() before delegating to super.dispose(). This matches the recommended own-cleanup-first pattern in Flutter.

  • Why: Dart mixins evaluate right-to-left. By executing mixin cleanup first, resources like AnimationController can cleanly shut down while the vsync/ticker providers are still fully active.
  • Mixin ordering: Always place AutoDisposeMixin after SingleTickerProviderStateMixin / TickerProviderStateMixin in the with clause so the disposal chain runs in the correct order.
  • Future maintainers: Inverting this order will cause TickerProvider assertion errors with AnimationController. This is by design, not accidental.

2. Strong Reference Retention Design #

Resources are held via a LinkedHashMap<Object, TrackedResource> with identity equality — strong references by design.

  • Why not WeakReference? Weak references are garbage-collected non-deterministically. If the GC collects an active resource prematurely, its dispose() / close() hook is never called, causing native memory leaks. Strong retention guarantees deterministic cleanup.
  • Avoid memory growth: Only register resources inside lifecycle initialization methods (initState, constructor) and never inside build(). Long-lived scopes with accidental build-time registrations will leak.

3. Hot Reload & Duplicate Protection #

During development, Hot Reload cycles can trigger duplicate registration paths. AutoDisposeGuard uses identity checks (identical) in O(1) to make duplicate registration a fast, safe no-op. Stale closures or redundant controllers are avoided.

Note: Hot Reload validation requires manual QA — automated hot-reload testing is not reliably supported in flutter test.

4. Error Isolation & Retry Semantics #

If a resource's dispose() / close() throws:

  • The TrackedResource remains in a non-disposed state (_disposed = false), allowing retry.
  • The registry's disposeAll() catches the error, logs it, and continues disposing remaining resources — one failure never blocks the rest.
// TrackedResource internals:
try {
  _disposeFn();
  _disposed = true;
} catch (_) {
  // _disposed stays false — caller can retry
  rethrow;
}

5. Enterprise Leak Tracking #

For enterprise-scale validation, we recommend integrating manual QA or the official leak_tracker package during integration test suites:

testWidgets('no controller leaks', (tester) async {
  final controller = TextEditingController();
  // Register, use, and let AutoDispose handle cleanup.
  // Then assert controller state or use leak_tracker.
});

6. Benchmark Performance #

Performance is verified under high loads (10,000 resources). Measured on CI:

Operation Throughput Complexity
Registration < 0.2 μs/op O(1) identity hash
Disposal < 0.1 μs/op O(1) reverse LIFO
Duplicate check < 0.05 μs/op O(1) hash lookup
1K TextEditingControllers ~35 ms register, ~3 ms dispose Linear
1K bag lifecycles (10 resources each) ~15 ms Linear

Compatibility Matrix #

Environment Supported Version
Flutter SDK >= 3.10.0 (including Flutter 3.22+)
Dart SDK >= 3.0.0 < 4.0.0
Platforms Android, iOS, Web, macOS, Windows, Linux
State Management Bloc, Cubit, Riverpod, Provider, GetX, vanilla

CI Pipeline #

Every push and PR runs the full validation suite via GitHub Actions:

✅ dart format --set-exit-if-changed
✅ flutter analyze
✅ flutter test --coverage
✅ Coverage gate ≥ 80%

License #

MIT

1
likes
160
points
55
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Safe automatic disposal for Flutter controllers, streams, timers, services, and custom resources.

Repository (GitHub)
View/report issues

Topics

#dispose #memory-management #lifecycle #controllers #bloc

License

MIT (license)

Dependencies

flutter

More

Packages that depend on auto_dispose_guard