Description
_ArtboardWidgetRiveRendererState.didUpdateWidget replaces its internal ArtboardWidgetPainter without disposing the outgoing one. Each time the painter prop of ArtboardWidgetRiveRenderer changes, the previous ArtboardWidgetPainter (a ChangeNotifier) is dropped on the floor — dispose() is never called, and leak_tracker reports it as notDisposed.
Package: rive_native 0.1.6 (also reproduces against rive 0.14.6, which is what triggers the swap by passing the controller through as the painter).
Source
rive_native/lib/src/rive_widget.dart, lines 583–589:
@override
void didUpdateWidget(covariant ArtboardWidgetRiveRenderer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.painter != widget.painter) {
setState(() {
_painter = ArtboardWidgetPainter(widget.painter); // ← old _painter dropped, never disposed
});
}
}
State.initState / State.dispose are paired correctly — only didUpdateWidget is missing the disposal of the outgoing painter.
Reproduction
Any flow that swaps the RiveWidgetController passed to RiveWidget will trigger it, since _RiveWidgetState.build passes widget.controller straight through as the painter of RiveArtboardWidget → ArtboardWidgetRiveRenderer. Each controller swap leaks one ArtboardWidgetPainter.
Minimal repro:
class Demo extends StatefulWidget {
const Demo({super.key});
@override
State<Demo> createState() => _DemoState();
}
class _DemoState extends State<Demo> {
RiveWidgetController? _controller;
late RiveFile _file;
@override
void initState() {
super.initState();
File.asset('assets/anim.riv').then((f) {
setState(() {
_file = f!;
_controller = RiveWidgetController(_file);
});
});
}
void _swap() {
final old = _controller;
setState(() => _controller = RiveWidgetController(_file));
WidgetsBinding.instance.addPostFrameCallback((_) => old?.dispose());
}
@override
Widget build(BuildContext context) {
if (_controller == null) return const SizedBox.shrink();
return Column(children: [
Expanded(child: RiveWidget(controller: _controller!)),
ElevatedButton(onPressed: _swap, child: const Text('swap controller')),
]);
}
}
Enable leak_tracker in main, tap the swap button N times, force a GC — leak_tracker reports N ArtboardWidgetPainter<ProceduralPainter> instances as notDisposed, with creation stacks pointing at either _RiveNativeView.createRenderObject (first painter, then replaced by a later swap) or _RiveNativeView.updateRenderObject (subsequent painters created in didUpdateWidget).
Expected
didUpdateWidget should dispose the outgoing _painter before replacing it, matching the initState / dispose lifecycle.
Suggested fix
@override
void didUpdateWidget(covariant ArtboardWidgetRiveRenderer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.painter != widget.painter) {
final old = _painter;
setState(() {
_painter = ArtboardWidgetPainter(widget.painter);
});
old?.dispose();
}
}
(Could also be deferred via addPostFrameCallback if there's any concern about disposing mid-build, but in practice the painter isn't referenced by the render object yet at this point.)
Environment
rive_native: 0.1.6
rive: 0.14.6
- Flutter: 3.41.9
- Platform: iOS / macOS / Android (any — the bug is in pure Dart)
Description
_ArtboardWidgetRiveRendererState.didUpdateWidgetreplaces its internalArtboardWidgetPainterwithout disposing the outgoing one. Each time thepainterprop ofArtboardWidgetRiveRendererchanges, the previousArtboardWidgetPainter(aChangeNotifier) is dropped on the floor —dispose()is never called, andleak_trackerreports it asnotDisposed.Package:
rive_native0.1.6 (also reproduces againstrive0.14.6, which is what triggers the swap by passing the controller through as the painter).Source
rive_native/lib/src/rive_widget.dart, lines 583–589:State.initState/State.disposeare paired correctly — onlydidUpdateWidgetis missing the disposal of the outgoing painter.Reproduction
Any flow that swaps the
RiveWidgetControllerpassed toRiveWidgetwill trigger it, since_RiveWidgetState.buildpasseswidget.controllerstraight through as the painter ofRiveArtboardWidget→ArtboardWidgetRiveRenderer. Each controller swap leaks oneArtboardWidgetPainter.Minimal repro:
Enable
leak_trackerinmain, tap the swap button N times, force a GC —leak_trackerreports NArtboardWidgetPainter<ProceduralPainter>instances asnotDisposed, with creation stacks pointing at either_RiveNativeView.createRenderObject(first painter, then replaced by a later swap) or_RiveNativeView.updateRenderObject(subsequent painters created indidUpdateWidget).Expected
didUpdateWidgetshould dispose the outgoing_painterbefore replacing it, matching theinitState/disposelifecycle.Suggested fix
(Could also be deferred via
addPostFrameCallbackif there's any concern about disposing mid-build, but in practice the painter isn't referenced by the render object yet at this point.)Environment
rive_native: 0.1.6rive: 0.14.6