Repository navigation
Flutter: draw only when the artboard changed (didChange), like iOS and web #665
Description
Submission checklist
- I have confirmed the issue is present in the latest version of the
riveFlutter package - I have searched the documentation and forums and could not find an answer
- I have searched existing issues and this is not a duplicate
Description
With Factory.flutter, FlutterRiveRenderBox calls markNeedsPaint() after every ticker advance while advance returns true; whether the artboard changed is never checked. The C++ runtime already tracks it (Artboard::didChange()), and the other runtimes use it:
- iOS:
RiveView.drawOptimization: DrawOptimization { alwaysDraw, drawOnChanged }(default.drawOnChanged). - Web:
drawingOptions: DrawOptimizationOptions { AlwaysDraw, DrawOnChanged }(defaultDrawOnChanged).
Flutter doesn't even expose the flag, so any animation that keeps advancing redraws every frame, including frames where nothing changed.
Patch (ready to re-land)
rive_native has no public repo for a PR, so the series is on a fork, based on rive_native 0.2.0-dev.3 verbatim from pub.dev (4 commits, offered for re-landing with co-authorship as for #631):
What it does:
- Exports
riveArtboardDidChangeand addsArtboard.didChange(looked up optionally: binaries without it returntrue, i.e. always draw). - Adds
DrawOptimization { alwaysDraw, drawOnChanged }onProceduralPainter, same names as iOS. DefaultalwaysDraw= unchanged behaviour. - With
drawOnChanged,FlutterRiveRenderBoxstill advances every tick but only repaints whendidChangeis true or a redraw is forced (first frame, layout, painter swap,scheduleRepaint/fit/alignment, pointer, image decode, last advance before the ticker stops). The box becomes a repaint boundary. - Opt-in because a gap in dirt tracking (cf. #14701, opacity 0) would show a stale frame; the default can flip later like iOS/web.
Factory.riveisn't covered (texture cleared before advance).
Benchmark (Galaxy Z Flip6, profile, 60 Hz, at rest, 10 s windows)
| fps | build ms | raster ms | GPU | process CPU | |
|---|---|---|---|---|---|
tab bar, alwaysDraw |
59.3 | 3.30 | 6.92 | 55 % | 80 % |
tab bar, drawOnChanged |
59.8 | 1.77 | 6.78 | 55 % | 71 % |
player card, alwaysDraw |
59.5 | 1.63 | 3.57 | 17 % | 47 % |
player card, drawOnChanged |
59.8 | 1.01 | 2.87 | 18 % | 36 % |
Build time roughly halved and ~10 CPU points saved. GPU is unchanged because frames keep coming while the ticker runs (see the related issue).
Tests
New draw_optimization_test.dart 13/13 (fails without the gate); artboard_did_change_test.dart 1 pass / 2 skipped with the prebuilt library (no export; verified with llvm-nm on Android builds and on device); nearby existing suites 63/63 unchanged.
Reproduction steps / code
Any Rive animation that keeps advancing while holding still (e.g. a looping timeline with constant keys), shown with RiveWidget on Factory.flutter: DevTools shows a Rive repaint on every frame although Artboard::didChange() is false.
Rive Flutter package version
rive 0.14.11 (rive_native 0.1.11); patch based on rive_native 0.2.0-dev.3.
Flutter version
Flutter 3.47.3 (stable), Dart 3.13.3
Device
Samsung Galaxy Z Flip6 (SM-F741B)
OS version
Android 16
Additional context
Related: #664 (state machine never settles after the max-iterations cap).
🤖 Generated with Claude Code
Activity
HayesGordon commentedon Oct 20, 2026
The
didChangecheck is something we've been meaning to implement for the Flutter runtime. Looking into it.feat(flutter)!: separate advance from paint and redraw only on change…