rive-app / rive-app/rive-flutter
[rive_native]: ArtboardWidgetPainter leaked on controller swap (didUpdateWidget skips dispose)
Nobody has claimed this yet.
- Dominant language
- Dart
- Stars
- 1.5k
- Forks
- 240
- PR merge metrics
- No merged PRs in 30d
Description
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.6rive: 0.14.6- Flutter: 3.41.9
- Platform: iOS / macOS / Android (any — the bug is in pure Dart)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in rive_native/lib/src/rive_widget.dart at _ArtboardWidgetRiveRendererState.didUpdateWidget and compare its painter replacement with initState and dispose. Reproduce a controller swap with leak_tracker, then verify that outgoing ArtboardWidgetPainter instances are disposed and no longer reported as notDisposed after garbage collection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart, flutter
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100