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

High-performance Flutter package that automatically manages disposal of all disposable resources — zero boilerplate, zero memory leaks. Supports TextEditingController, AnimationController, StreamContr [...]

example/lib/main.dart

/// AutoDisposeGuard — full example app.
///
/// Demonstrates all three registration APIs:
///   1. AutoDisposeMixin  (FormScreen)
///   2. AutoDisposeScope  (ScopeScreen)
///   3. .autoDispose()    (inline extension, also in ScopeScreen)
///
/// Run with `flutter run` and watch the debug console when navigating away.
library;

import 'dart:async';

import 'package:auto_dispose_guard/auto_dispose_guard.dart';
import 'package:flutter/material.dart';

void main() => runApp(const AutoDisposeExampleApp());

// ─────────────────────────────────────────────────────────────────────────────
// App root
// ─────────────────────────────────────────────────────────────────────────────

class AutoDisposeExampleApp extends StatelessWidget {
  const AutoDisposeExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AutoDisposeGuard Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Home
// ─────────────────────────────────────────────────────────────────────────────

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AutoDisposeGuard')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            _DemoTile(
              icon: Icons.tune,
              title: 'AutoDisposeMixin',
              subtitle: 'State mixin — zero dispose() boilerplate',
              onTap: () => _push(context, const FormScreen()),
            ),
            const SizedBox(height: 12),
            _DemoTile(
              icon: Icons.view_in_ar,
              title: 'AutoDisposeScope',
              subtitle: 'InheritedWidget scope + .autoDispose() extension',
              onTap: () => _push(
                context,
                const AutoDisposeScope(
                  debugLabel: 'ScopeScreen',
                  child: ScopeScreen(),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _push(BuildContext ctx, Widget screen) {
    Navigator.push<void>(ctx, MaterialPageRoute<void>(builder: (_) => screen));
  }
}

class _DemoTile extends StatelessWidget {
  const _DemoTile({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.onTap,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 320,
      child: Card(
        child: ListTile(
          leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
          title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
          subtitle: Text(subtitle),
          trailing: const Icon(Icons.arrow_forward_ios, size: 14),
          onTap: onTap,
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Method 1: AutoDisposeMixin
// ─────────────────────────────────────────────────────────────────────────────

/// Demonstrates [AutoDisposeMixin].
///
/// Notice: no `@override void dispose()` anywhere in this class.
/// All controllers are declared as `late final` fields using `register()`.
class FormScreen extends StatefulWidget {
  const FormScreen({super.key});

  @override
  State<FormScreen> createState() => _FormScreenState();
}

class _FormScreenState extends State<FormScreen>
    with AutoDisposeMixin, SingleTickerProviderStateMixin {
  // ── Registered resources ── all auto-disposed when the route is popped ──

  late final _nameController = register(TextEditingController());
  late final _emailController = register(TextEditingController());
  late final _focusNode = register(FocusNode());

  // AnimationController requires vsync — initialize in the field directly.
  late final _animation = register(
    AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 900),
    )..repeat(reverse: true),
  );

  late final _counterStream = register(StreamController<int>.broadcast());

  // StreamSubscription auto-detected via `is StreamSubscription`.
  // Not stored — register() handles lifetime; the value is not needed elsewhere.
  // ignore: unused_field
  late final _sub = register(
    _counterStream.stream.listen(
      (v) => setState(() => _streamValue = v),
    ),
  );

  int _streamValue = 0;

  void _increment() => _counterStream.add(_streamValue + 1);

  // ── No dispose() override ─────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AutoDisposeMixin Demo'),
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(4),
          child: AnimatedBuilder(
            animation: _animation,
            builder: (_, __) => LinearProgressIndicator(
              value: _animation.value,
              backgroundColor: Colors.transparent,
            ),
          ),
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const _SectionLabel(
              '6 resources registered — pop this screen to auto-dispose all',
            ),
            const SizedBox(height: 20),
            TextField(
              controller: _nameController,
              focusNode: _focusNode,
              decoration: const InputDecoration(
                labelText: 'Name',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 32),
            _CounterCard(
              value: _streamValue,
              onIncrement: _increment,
              label: 'StreamController counter',
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Method 2: AutoDisposeScope + .autoDispose() extension
// ─────────────────────────────────────────────────────────────────────────────

/// Demonstrates [AutoDisposeScope] and the `.autoDispose(context)` extension.
///
/// The [AutoDisposeScope] wrapper is added in [HomeScreen._push], not here,
/// so this screen is a plain [StatefulWidget] — no mixin needed.
class ScopeScreen extends StatefulWidget {
  const ScopeScreen({super.key});

  @override
  State<ScopeScreen> createState() => _ScopeScreenState();
}

class _ScopeScreenState extends State<ScopeScreen>
    with SingleTickerProviderStateMixin {
  // Resources are registered via the inherited scope using the extension.
  // initState is the right place because getInheritedWidgetOfExactType
  // (used internally) is safe to call from initState.
  late final TextEditingController _nameController;
  late final AnimationController _pulseAnimation;
  late final StreamController<String> _logStream;
  // ignore: unused_field
  late final StreamSubscription<String> _logSub;
  final List<String> _logs = [];

  @override
  void initState() {
    super.initState();

    // Extension API — inline, type-safe, fluent.
    _nameController = TextEditingController().autoDispose(context);

    _pulseAnimation = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )
      ..repeat(reverse: true)
      ..autoDispose(context);

    _logStream = StreamController<String>.broadcast()..autoDispose(context);

    _logSub = _logStream.stream.listen((msg) {
      setState(() => _logs.insert(0, msg));
    })..autoDispose(context);

    // Alternatively use AutoDispose.of(context) for imperative registration:
    // AutoDispose.of(context).register(_nameController);
  }

  void _addLog() {
    _logStream.add('Event at ${DateTime.now().toIso8601String()}');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AutoDisposeScope Demo'),
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(4),
          child: AnimatedBuilder(
            animation: _pulseAnimation,
            builder: (_, __) => LinearProgressIndicator(
              value: _pulseAnimation.value,
              backgroundColor: Colors.transparent,
              color: Theme.of(context).colorScheme.secondary,
            ),
          ),
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const _SectionLabel(
              '4 resources via .autoDispose(context) — pop to auto-dispose',
            ),
            const SizedBox(height: 20),
            TextField(
              controller: _nameController,
              decoration: const InputDecoration(
                labelText: 'Type something',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: _addLog,
              icon: const Icon(Icons.add),
              label: const Text('Add stream event'),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: ListView.builder(
                itemCount: _logs.length,
                itemBuilder: (_, i) => ListTile(
                  dense: true,
                  leading: const Icon(Icons.circle, size: 8),
                  title: Text(_logs[i]),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Custom type example — implement Disposable for zero-config support
// ─────────────────────────────────────────────────────────────────────────────

/// A mock service that holds an expensive resource.
class AnalyticsService implements Disposable {
  AnalyticsService() {
    debugPrint('[AnalyticsService] initialized');
  }

  @override
  void dispose() {
    debugPrint('[AnalyticsService] disposed');
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared UI helpers
// ─────────────────────────────────────────────────────────────────────────────

class _SectionLabel extends StatelessWidget {
  const _SectionLabel(this.text);
  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.primaryContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        text,
        style: TextStyle(
          color: Theme.of(context).colorScheme.onPrimaryContainer,
          fontSize: 12,
        ),
      ),
    );
  }
}

class _CounterCard extends StatelessWidget {
  const _CounterCard({
    required this.value,
    required this.onIncrement,
    required this.label,
  });

  final int value;
  final VoidCallback onIncrement;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text(label, style: Theme.of(context).textTheme.labelMedium),
            const SizedBox(height: 8),
            Text(
              '$value',
              style: Theme.of(context).textTheme.displayMedium,
            ),
            const SizedBox(height: 8),
            FilledButton(
              onPressed: onIncrement,
              child: const Text('Increment via Stream'),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
0
points
222
downloads

Publisher

unverified uploader

Weekly Downloads

High-performance Flutter package that automatically manages disposal of all disposable resources — zero boilerplate, zero memory leaks. Supports TextEditingController, AnimationController, StreamController, FocusNode, custom types, and anything with dispose/close/cancel.

Repository (GitHub)
View/report issues

Topics

#dispose #memory-management #lifecycle #performance #controllers

License

unknown (license)

Dependencies

flutter

More

Packages that depend on auto_dispose_guard