interactive_viewer_vector 0.2.5 copy "interactive_viewer_vector: ^0.2.5" to clipboard
interactive_viewer_vector: ^0.2.5 copied to clipboard

Fork of InteractiveViewer with direct RenderTransform updates (markNeedsPaint) for zero widget rebuilds during pan/zoom interactions.

interactive_viewer_vector #

A drop-in replacement for Flutter's InteractiveViewer that eliminates widget rebuilds during pan and zoom — zero setState, zero rebuilds, just paint.

The problem #

When you pan or zoom a standard InteractiveViewer, Flutter rebuilds the entire widget subtree on every single frame of the gesture. Every CustomPaint, every RepaintBoundary, every child — all of it is rebuilt and re-laid-out dozens of times per second. On a heavy canvas (a mindmap with hundreds of nodes, a complex editor, a painted dashboard) this rebuild storm shows up as visible jank and dropped frames on mobile.

This is a known, long-standing Flutter framework limitation (#78543, #72066, #118434, #129150, #60550). The stock InteractiveViewer subscribes to its TransformationController and calls setState on every transformation change. It has not been fixed upstream because the fix is architectural.

The fix #

A single method change: _handleTransformation no longer calls setState. It writes the matrix straight to the RenderTransform via a GlobalKey, triggering markNeedsPaint() only. The widget tree is untouched during interactions.

Stock InteractiveViewer:   matrix change -> setState -> build() whole subtree -> layout/paint
InteractiveViewerVector:   matrix change -> RenderTransform.transform = m   -> markNeedsPaint only

The API, gesture behavior, and constructor parameters are unchanged.

Screenshots #

Android Desktop
Android demo Desktop demo

Usage #

Replace InteractiveViewer with InteractiveViewerVector and TransformationController with TransformationControllerVector — same parameters, same callbacks:

final _controller = TransformationControllerVector();

InteractiveViewerVector(
  transformationController: _controller,
  constrained: false,
  boundaryMargin: const EdgeInsets.all(2000),
  minScale: 0.1,
  maxScale: 3,
  onInteractionEnd: (details) { /* ... */ },
  child: MyHugeCanvas(),
)

Programmatic transforms work as usual:

_controller.value = Matrix4.identity();

All constructor variants are supported: InteractiveViewerVector(...), InteractiveViewerVector.builder(...), panEnabled, scaleEnabled, panAxis, trackpadScrollCausesScale, scaleFactor, alignment, clipBehavior, etc.

Tests #

Widget tests assert that a child with a build counter is built exactly once across 10 consecutive transformation updates. With the stock widget, each update triggers a build.

flutter test                              # unit + widget tests
cd example && flutter test integration_test  # integration (device required)

For the real-device performance testing protocol (DevTools profiling, frame timing), see README_GH.md.

Platforms #

Native only — CanvasKit/HTML rendering on the web has its own performance characteristics and negates the benefit.

Platform Status
Android Manually validated
Linux Manually validated
Windows Manually validated
iOS CI-compiled, no runtime tests
macOS CI-compiled, no runtime tests

Origin & license #

Forked from the Flutter SDK (packages/flutter/lib/src/widgets/interactive_viewer.dart, 1300+ lines) with a single behavioral change in _handleTransformation. BSD 3-Clause license, copyright notice of The Flutter Authors preserved inside LICENSE.


For detailed technical notes, contribution guidelines, and the performance testing protocol, see README_GH.md.

1
likes
160
points
453
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Fork of InteractiveViewer with direct RenderTransform updates (markNeedsPaint) for zero widget rebuilds during pan/zoom interactions.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter, vector_math

More

Packages that depend on interactive_viewer_vector