A Flutter package that provides RAII (Resource Acquisition Is Initialization) pattern implementation for managing object lifecycles and ensuring proper resource cleanup.
- Automatic resource disposal through lifecycle management
- Fluent API for resource registration
- Debug logging support for lifecycle events
- Type-safe resource management
- Integration with Flutter's widget lifecycle
- Support for Material and Cupertino components
- Built-in support for common Flutter resources:
- Controllers (Animation, Text, Scroll, etc.)
- Notifiers and Listeners
- Streams
- App lifecycle
Add the package to your pubspec.yaml
:
dependencies:
raii: ^0.1.0
class MyWidgetState extends State<MyWidget>
// Add RaiiStateMixin
with SingleTickerProviderStateMixin, RaiiStateMixin {
// Resources are automatically disposed together with widget
late final controller = AnimationController(vsync: this)
.withLifecycle(this, debugLabel: 'MyAnimation');
late final textController = TextEditingController()
.withLifecycle(this, debugLabel: 'TextInput');
// Works like initState() but runs when widget is mounted and
// context is accesible, you are free to move it to `initState`
// if you don't need context access.
@override
void initLifecycle() {
super.initLifecycle();
// Register listeners with automatic cleanup
controller.addListenerWithLifecycle(
this,
() => setState(() {}),
debugLabel: 'AnimationListener',
);
}
}
// Tab Controller
late final tabController = TabController(length: 3, vsync: this)
.withLifecycle(this, debugLabel: 'TabBar');
// Search Controller
late final searchController = SearchController()
.withLifecycle(this, debugLabel: 'SearchBar');
// Data Table Source
final dataSource = MyDataSource()
.withLifecycle(this, debugLabel: 'TableDataSource');
// Cupertino Tab Controller
late final cupertinoTabs = CupertinoTabController(initialIndex: 0)
.withLifecycle(this, debugLabel: 'CupertinoTabs');
// Restorable Tab Controller
late final restorableTabs = RestorableCupertinoTabController(initialIndex: 0)
.withLifecycle(this, debugLabel: 'RestorableTabs');
class MyCustomResource {
void initialize() { /* ... */ }
void cleanup() { /* ... */ }
}
// Usage in a widget state
class MyWidgetState extends State<MyWidget> with RaiiStateMixin {
late final resource = RaiiBox.withLifecycle(
this,
instance: MyCustomResource(),
init: (r) => r.initialize(),
dispose: (r) => r.cleanup(),
debugLabel: 'CustomResource',
);
// The resource will be automatically initialized and disposed
// with the widget's lifecycle
}
// Alternative usage with RaiiManager
final raiiManager = RaiiManager();
final resource = RaiiBox.withLifecycle(
raiiManager,
instance: MyCustomResource(),
init: (r) => r.initialize(),
dispose: (r) => r.cleanup(),
debugLabel: 'CustomResource',
);
// When done
// Will properly clean up the resource
raiiManager.disposeLifecycle();
// Implementing lifecycle directly in your resource class
class ManagedResource with RaiiLifecycleMixin {
late final Socket _socket;
late final StreamSubscription _subscription;
// Named constructor that immediately attaches to a lifecycle manager
ManagedResource.withLifecycle(RaiiLifecycleAware lifecycleAware) {
lifecycleAware.registerLifecycle(this);
}
@override
void initLifecycle() {
super.initLifecycle();
// Initialize resources
_socket = Socket.connect(...);
_subscription = _socket.listen(...);
debugPrint('ManagedResource: Initialized socket and subscription');
}
@override
void disposeLifecycle() {
// Clean up resources
_subscription.cancel();
_socket.close();
debugPrint('ManagedResource: Cleaned up socket and subscription');
super.disposeLifecycle();
}
}
// Usage in a widget - cleaner syntax with .attach constructor
class MyWidgetState extends State<MyWidget> with RaiiStateMixin {
late final resource = ManagedResource.withLifecycle(this);
// The resource will be automatically initialized and disposed
// with the widget's lifecycle
}
// Alternative usage with RaiiManager
final raiiManager = RaiiManager();
final resource = ManagedResource.withLifecycle(raiiManager);
// When done
// Will properly clean up the resource
raiiManager.disposeLifecycle();
Choose RaiiBox
when:
- You need to add lifecycle to an existing class
- You don't control the resource's source code
Choose direct RaiiLifecycleMixin
implementation when:
- You're creating a new resource class
- You prefer explicit lifecycle management in your class
- You want a cleaner API with
.withLifecycle
constructor pattern
// Automatic stream subscription cleanup
myStream.listen(onData).withLifecycle(
this,
debugLabel: 'MyStreamSubscription',
);
class MyWidgetState extends State<MyWidget> with RaiiStateMixin {
late final valueNotifier = ValueNotifier<int>(0)
.withLifecycle(this, debugLabel: 'Counter');
@override
void onLifecycleAttach() {
// Simple listener
valueNotifier.addListenerWithLifecycle(
this,
setState(() {}),
debugLabel: 'ValueNotifierListener',
);
// Multiple listeners for the same listenable
valueNotifier.addListenerWithLifecycle(
this,
updateUI,
debugLabel: 'UIListener',
);
valueNotifier.addListenerWithLifecycle(
this,
saveToPreferences,
debugLabel: 'StorageListener',
);
// Listening to animation controller
final controller = AnimationController(vsync: this)
.withLifecycle(this, debugLabel: 'Animation');
controller.addListenerWithLifecycle(
this,
() => debugPrint('Animation value: ${controller.value}'),
debugLabel: 'AnimationListener',
);
}
}
class MyWidgetState extends State<MyWidget> with RaiiStateMixin {
@override
void initLifecycle() {
super.initLifecycle();
// Basic app lifecycle observer
final observer = AppStateObserver();
// raii addObserver alternative that allows you to
// attach to the state's lifecycle
WidgetsBinding.instance.addObserverWithLifeycle(
this,
observer,
debugLabel: 'AppLifecycle',
);
}
}
class AppStateObserver with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
debugPrint('App resumed - restore resources');
break;
case AppLifecycleState.inactive:
debugPrint('App inactive - pause updates');
break;
case AppLifecycleState.paused:
debugPrint('App paused - save state');
break;
case AppLifecycleState.detached:
debugPrint('App detached - cleanup');
break;
}
}
@override
void didChangePlatformBrightness() {
debugPrint('Brightness changed');
}
}
class TimerWidgetState extends State<TimerWidget> with RaiiStateMixin {
late final periodicTimer = RaiiBox.withLifecycle(
this,
instance: Timer.periodic(
Duration(seconds: 1),
(_) => debugPrint('Timer tick'),
),
dispose: (timer) => timer.cancel(),
debugLabel: 'PeriodicTimer',
);
}
// Resources that live for the entire application lifetime
final globalResource = MyGlobalResource.withLifecycle(
alwaysAliveRaiiManager,
);
When using RaiiStateMixin
with TickerProviderStateMixin
, the order matters:
// Correct order:
class MyWidgetState extends State<MyWidget>
with TickerProviderStateMixin, RaiiStateMixin {
// ...
}
// Incorrect order - will cause incorrect resources disposal:
class MyWidgetState extends State<MyWidget>
with RaiiStateMixin, TickerProviderStateMixin {
// ...
}
Debug labels help track lifecycle events in the console:
late final animationController = AnimationController(vsync: this)
.withLifecycle(this, debugLabel: 'MyAnimation');
late final textInput = EditingTextController()
.withLifecycle(this, debugLabel: 'TextInput');
Console output:
[RAII] Init lifecycle: MyAnimation
[RAII] Init lifecycle: TextInput
...
[RAII] Dispose lifecycle: TextInput
[RAII] Dispose lifecycle: MyAnimation
RaiiLifecycle
- Base interface for objects with manageable lifecyclesRaiiLifecycleAware
- Interface for objects that aware about other lifecycles typically used for managing multiple lifecyclesRaiiLifecycleMixin
- Mixin that helps add basic lifecycle implementationRaiiManagerMixin
- Mixin that helps implementat for managing multiple lifecycles, it implementsRaiiLifecycleAware
interface.RaiiBox
- Container for managing custom resources
Licensed under the MIT (LICENSE or https://mit-license.org/)