A comprehensive Flutter package for managing Rive animations with bidirectional data binding, interactive controls, image replacement, font replacement, GPU thumbnail capture, and global state management capabilities.
Headless RenderTexture Mode 🎨
- Render Rive animations to a GPU texture without a visible widget — ideal for broadcast compositors and zero-copy IOSurface pipelines
- New
RiveRenderMode.texturemode withtextureWidth/Height,onTextureReady, andonNativeTexturePointercallbacks
Font Replacement API ✨
- Dynamic font swapping at runtime, mirroring the image replacement pattern
updateFontFromBytes(),updateFontFromAsset(),updateFontFromUrl()— supports .ttf and .otf- FontAsset interception with
enableImageReplacement: true
Thumbnail / Snapshot API 📸
- GPU-direct frame capture via
RenderTexture.toImage()— noRepaintBoundaryneeded captureSnapshot()returnsui.Image,captureSnapshotAsPng()returns PNG bytescaptureAnimationThumbnail()on controller for one-liner thumbnail generation
Complete DataType Coverage
- All 13 Rive DataType variants supported:
string,number,integer,boolean,color,trigger,enumType,image,font,list,artboard,viewModel,symbolListIndex DataBindstrategy parameter for ViewModel binding
See the full examples: Run
dart pub unpack rive_animation_managerand check the/examplefolder andEXAMPLES.mdfor fully documented usage patterns.
Managing Rive animations in Flutter can be complex when you need to:
- ❌ Manually track dozens of state machine inputs across multiple animations
- ❌ Manually define every property, input, and callback for each animation instance
- ❌ Duplicate code when managing multiple Rive files with similar interactions
- ❌ Handle image replacements without built-in caching or optimization
- ❌ Debug issues without visibility into animation state and property values
This library solves all of this with:
✅ Global Controller - One centralized singleton manages all animations app-wide
✅ Automatic Discovery - ViewModel properties are automatically detected and exposed
✅ Type-Safe Updates - Update any property (string, number, boolean, color, trigger) with one method
✅ Animation ID System - Unique IDs let you control multiple .riv files independently
✅ Built-in Caching - Image and property path caching for optimal performance
✅ Comprehensive Logging - Debug with full visibility into all animation interactions
The animationId is the key differentiator that makes this library powerful for complex apps:
// ✅ Load multiple animations independently
RiveManager(
animationId: 'heroAnimation', // Unique ID
riveFilePath: 'assets/hero.riv',
)
RiveManager(
animationId: 'backgroundAnimation', // Different ID
riveFilePath: 'assets/background.riv',
)
// ✅ Control them independently from anywhere
final controller = RiveAnimationController.instance;
// Update hero animation
await controller.updateDataBindingProperty('heroAnimation', 'progress', 0.75);
// Update background animation
await controller.updateDataBindingProperty('backgroundAnimation', 'opacity', 0.5);Without this library, you'd need to:
- Store separate references to each animation's state
- Manually expose each property update method
- Track all state machine inputs yourself
- Handle image replacements manually for each instance
With this library, you get:
- One global controller for all animations
- Access any animation by its ID from anywhere
- Automatic property discovery and type handling
- Built-in caching and optimization
- Global Animation Controller: Centralized singleton for managing all Rive animations across your app
- State Machine Management: Handle inputs (triggers, booleans, numbers) and state transitions
- Data Binding Support: Full support for ViewModels with automatic property discovery
- Interactive Controls: Automatic generation of type-specific UI controls (string, number, boolean, color, trigger, enum)
- Bidirectional Updates: Real-time sync between UI controls and animation properties
- Flexible Color Support: 8 color formats with automatic detection (hex, RGB, Maps, Lists, named colors)
- Image Replacement: Dynamically update images from assets, URLs, or raw bytes
- Font Replacement: Dynamically swap fonts from assets, URLs, or raw bytes (.ttf/.otf)
- Thumbnail Capture: GPU-direct animation frame capture as
ui.Imageor PNG bytes - Headless RenderTexture Mode: Render to GPU texture without widget display for broadcast pipelines
- Image Caching: Preload and cache images for instant switching without decode overhead
- Text Run Management: Update and retrieve text values from animations
- Input Callbacks: Real-time callbacks for input changes, triggers, and hover actions
- Event Handling: Listen to Rive events with state context
- Property Caching: Optimized nested property path caching for performance
- Responsive Layouts: Automatic layout adaptation for desktop and mobile screens
- Comprehensive Logging: Debug logging with configurable log manager
Add to your pubspec.yaml:
dependencies:
rive_animation_manager: ^1.0.17Then run:
flutter pub getTo unpack the package source code and run the enhanced interactive example:
# Unpack the package source code and example app
dart pub unpack rive_animation_manager
# Navigate to the example folder
cd rive_animation_manager/example
# Create the platform folders (if not already created)
flutter create .
# Fetch dependencies
flutter pub get
# Run the example app
flutter runThis will launch the interactive example showcasing:
- Desktop View: Side-by-side layout with Rive animation on left and controls on right
- Mobile View: Stacked layout optimized for smaller screens
- Property Controls: Auto-generated controls based on your Rive file's ViewModel properties
- Event Logging: Real-time event tracking and debugging
import 'package:rive_animation_manager/rive_animation_manager.dart';
RiveManager(
animationId: 'myAnimation', // 🔑 Unique ID to control this animation
riveFilePath: 'assets/animations/my_animation.riv',
animationType: RiveAnimationType.stateMachine,
onInit: (artboard) {
print('Animation loaded: $artboard');
},
onInputChange: (index, name, value) {
print('Input changed: $name = $value');
},
)Use the global controller to manipulate animations:
RiveAnimationController controller = RiveAnimationController.instance;
// Update boolean input
controller.updateBool('myAnimation', 'isHovered', true);
// Update number input
controller.updateNumber('myAnimation', 'scrollPosition', 0.5);
// Trigger an input
controller.triggerInput('myAnimation', 'playAnimation');
// Update text
controller.setTextRunValue('myAnimation', 'myText', 'Hello World');
// Get current values
final value = controller.getDataBindingPropertyValue('myAnimation', 'propertyName');Update colors with 8 different formats - all automatically detected!
final controller = RiveAnimationController.instance;
// Hex format
await controller.updateDataBindingProperty('myAnimation', 'color', '#3EC293');
// RGB/RGBA strings
await controller.updateDataBindingProperty('myAnimation', 'color', 'rgb(62, 194, 147)');
await controller.updateDataBindingProperty('myAnimation', 'color', 'rgba(62, 194, 147, 0.8)');
// Flutter Color objects
await controller.updateDataBindingProperty('myAnimation', 'color', Colors.teal);
await controller.updateDataBindingProperty('myAnimation', 'color', Color(0xFF3EC293));
// Maps (standard 0-255)
await controller.updateDataBindingProperty('myAnimation', 'color', {'r': 62, 'g': 194, 'b': 147});
// Maps (Rive normalized 0.0-1.0) - Auto-detected!
await controller.updateDataBindingProperty('myAnimation', 'color', {'r': 0.2431, 'g': 0.7608, 'b': 0.5764});
// Lists (standard 0-255)
await controller.updateDataBindingProperty('myAnimation', 'color', [62, 194, 147]);
// Lists (Rive normalized 0.0-1.0) - Auto-detected!
await controller.updateDataBindingProperty('myAnimation', 'color', [0.2431, 0.7608, 0.5764]);
// Named colors
await controller.updateDataBindingProperty('myAnimation', 'color', 'teal');Discover and update data binding properties:
RiveManager(
animationId: 'myAnimation',
riveFilePath: 'assets/animations/my_animation.riv',
onViewModelPropertiesDiscovered: (properties) {
for (var prop in properties) {
print('Property: ${prop['name']} (${prop['type']})');
}
},
onDataBindingChange: (propertyName, propertyType, value) {
print('Property changed: $propertyName = $value');
},
)Update data binding properties:
final controller = RiveAnimationController.instance;
// Update various property types
await controller.updateDataBindingProperty('myAnimation', 'text', 'New Text');
await controller.updateDataBindingProperty('myAnimation', 'count', 42);
await controller.updateDataBindingProperty('myAnimation', 'isVisible', true);
await controller.updateDataBindingProperty('myAnimation', 'color', Color(0xFF00FF00));Enable image replacement for dynamic image updates:
RiveManager(
animationId: 'myAnimation',
riveFilePath: 'assets/animations/my_animation.riv',
enableImageReplacement: true,
)Update images programmatically:
final controller = RiveAnimationController.instance;
final state = controller.getAnimationState('myAnimation');
if (state != null) {
// Update from asset
await state.updateImageFromAsset('assets/images/new_image.png');
// Update from URL
await state.updateImageFromUrl('https://example.com/image.png');
// Update from bytes
await state.updateImageFromBytes(imageBytes);
// Update from pre-decoded RenderImage (fastest)
state.updateImageFromRenderedImage(renderImage);
}Enable font replacement for dynamic font updates (uses the same enableImageReplacement flag):
RiveManager(
animationId: 'myAnimation',
riveFilePath: 'assets/animations/my_animation.riv',
enableImageReplacement: true, // Enables both image AND font interception
)Update fonts programmatically:
final controller = RiveAnimationController.instance;
// Update from asset bundle
await controller.updateFontFromAsset('myAnimation', 'assets/fonts/CustomFont.ttf');
// Update from URL
await controller.updateFontFromUrl(
'myAnimation',
'https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuI6fMZ.ttf',
);
// Update from raw bytes
await controller.updateFontFromBytes('myAnimation', fontFileBytes);
// Or via direct state access
final state = controller.getAnimationState('myAnimation');
await state?.updateFontFromUrl('https://example.com/fonts/Brand.ttf');Capture any animation frame as PNG bytes or ui.Image using GPU-direct rendering — no RepaintBoundary needed:
final controller = RiveAnimationController.instance;
// Capture PNG bytes (most common)
final pngBytes = await controller.captureAnimationThumbnail(
'myAnimation',
width: 512,
height: 512,
);
// Save to file, upload, display, etc.
if (pngBytes != null) {
await File('thumbnail.png').writeAsBytes(pngBytes);
}For a trigger-then-capture workflow:
// 1. Trigger the animation
controller.updateDataBindingProperty('myAnimation', 'Show', true);
// 2. Wait for it to settle
await Future.delayed(Duration(milliseconds: 1000));
// 3. Capture the current visual state
final pngBytes = await controller.captureAnimationThumbnail(
'myAnimation',
width: 512,
height: 512,
);Render Rive animations to a GPU texture without displaying them in the widget tree — ideal for broadcast compositors:
RiveManager(
animationId: 'broadcast_overlay',
riveFilePath: 'assets/animations/lower_third.riv',
renderMode: RiveRenderMode.texture, // GPU texture mode
textureWidth: 1920,
textureHeight: 1080,
onTextureReady: (texture) {
print('GPU texture ready: textureId=${texture.textureId}');
},
onNativeTexturePointer: (address) {
// MTLTexture* pointer on macOS for FFI/IOSurface integration
print('Native pointer: 0x${address.toRadixString(16)}');
},
)
// Get pointer later via controller
final pointer = RiveAnimationController.instance
.getNativeTexturePointer('broadcast_overlay');Preload images for instant switching:
final controller = RiveAnimationController.instance;
await controller.preloadImagesForAnimation(
'myAnimation',
[
'https://example.com/image1.png',
'https://example.com/image2.png',
'https://example.com/image3.png',
],
Factory.rive,
);
// Switch to cached image instantly
controller.updateImageFromCache('myAnimation', 0);Handle different input types:
RiveManager(
animationId: 'myAnimation',
riveFilePath: 'assets/animations/my_animation.riv',
onInputChange: (index, inputName, value) {
print('Input changed: $inputName = $value');
},
onTriggerAction: (triggerName, value) {
print('Trigger fired: $triggerName');
},
onHoverAction: (hoverName, value) {
print('Hover state: $hoverName = $value');
},
)Listen to Rive events:
RiveManager(
animationId: 'myAnimation',
riveFilePath: 'assets/animations/my_animation.riv',
onEventChange: (eventName, event, currentState) {
print('Event: $eventName in state: $currentState');
},
)Update nested properties using path notation:
final controller = RiveAnimationController.instance;
// Using '/' separator
await controller.updateNestedProperty(
'myAnimation',
'parent/child',
'newValue',
);
// Using '.' separator
await controller.updateNestedProperty(
'myAnimation',
'parent.child',
'newValue',
);Monitor animation manager cache usage:
final controller = RiveAnimationController.instance;
final stats = controller.getCacheStats();
print('Active animations: ${stats['animations']}');
print('Cached images: ${stats['totalCachedImages']}');
print('Cached property paths: ${stats['totalCachedPropertyPaths']}');Add and retrieve logs for debugging:
import 'package:rive_animation_manager/rive_animation_manager.dart';
// Add a single log
LogManager.addLog('Animation loaded successfully');
// Add a log with error flag
LogManager.addLog('Failed to load animation', isExpected: false);
// Get all logs as strings
List<String> allLogs = LogManager.logs;
// Get last N logs
final recentLogs = LogManager.getLastLogsAsStrings(10);
// Search logs by keyword
List<Map<String, dynamic>> results = LogManager.searchLogs('animation');
for (var log in results) {
print('${log['timestamp']}: ${log['message']}');
}
// Export logs as JSON
String json = LogManager.exportAsJSON();Global singleton for managing all Rive animations.
Key Methods:
register(String id, RiveManagerState state)- Register an animationupdateBool(String id, String name, bool value)- Update boolean inputupdateNumber(String id, String name, double value)- Update number inputtriggerInput(String id, String name)- Fire a triggersetTextRunValue(String id, String textRunName, String value)- Update textupdateDataBindingProperty(String id, String name, dynamic value)- Update data binding propertyupdateNestedProperty(String id, String path, dynamic value)- Update nested propertyupdateFontFromUrl(String id, String url)- Update font from URLupdateFontFromBytes(String id, Uint8List bytes)- Update font from bytesupdateFontFromAsset(String id, String assetPath)- Update font from asset bundlecaptureAnimationThumbnail(String id, {required int width, required int height})- Capture frame as PNGgetNativeTexturePointer(String id)- Get native GPU texture pointer (texture mode)preloadImagesForAnimation(String id, List<String> urls, Factory factory)- Cache imagesupdateImageFromCache(String id, int index)- Use cached imagegetCacheStats()- Get cache statistics
Flutter widget for displaying Rive animations.
Constructor Parameters:
animationId- Unique identifier for this animation instance (enables global control)riveFilePath- Path to .riv file in assetsexternalFile- External Rive file (alternative to riveFilePath)fileLoader- Custom file loaderenableImageReplacement- Enable dynamic image and font updatesrenderMode-RiveRenderMode.widget(default) orRiveRenderMode.texturetextureWidth/textureHeight- GPU texture resolution (texture mode)- Various display properties:
fit,alignment,hitTestBehavior, etc.
Callbacks:
onInit- Called when animation is loadedonInputChange- Called when input value changesonTriggerAction- Called when trigger firesonViewModelPropertiesDiscovered- Called when data binding properties foundonDataBindingChange- Called when data binding property changesonTextureReady- Called when GPU texture is ready (texture mode)onNativeTexturePointer- Called with native texture pointer address (texture mode)
- Use Unique Animation IDs: Always provide unique identifiers for each animation instance
- Cache Images: For animations with many image updates, preload and cache images
- Handle Errors: Check return values and use logging to debug issues
- Dispose Properly: The package handles cleanup automatically in dispose()
- Nested Properties: Use path caching for frequently updated nested properties
- Enable Logging in Debug: Use LogManager to debug issues during development
Animation not loading?
- Check file path is correct
- Enable logging:
LogManager.enabled = true - Check logs for specific error messages
Image replacement not working?
- Ensure
enableImageReplacement: trueis set - Check that animation actually has image assets
- Verify image format is supported (PNG, JPEG, etc.)
Performance issues?
- Use image caching for frequent updates
- Enable property path caching (automatic)
- Monitor cache stats with
getCacheStats()
For issues, feature requests, or questions:
- GitHub Repository: https://github.com/unspokenlanguage/RiveAnimation-Manager
- GitHub Issues: https://github.com/unspokenlanguage/RiveAnimation-Manager/issues
- pub.dev: https://pub.dev/packages/rive_animation_manager
- Check existing issues: Search GitHub issues first
- Review documentation: See README.md, EXAMPLES.md, and QUICK_REFERENCE.md in the repository
- Run the example: Use
dart pub unpack rive_animation_managerto explore the fully documented example - Create new issue: If not found, create a detailed issue with:
- Flutter version (
flutter --version) - Package version
- Error logs or stack trace
- Minimal reproducible example (MRE)
- Flutter version (
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes with clear commit messages
- Add tests for new functionality
- Update documentation as needed
- Push to your branch (
git push origin feature/amazing-feature) - Open a Pull Request with detailed description
- Flutter 3.0.0 or higher
- Dart 3.0.0 or higher
- rive_native package dependency
This package is licensed under the MIT License. See LICENSE file for details.
- Headless RenderTexture Mode for zero-copy GPU pipeline integration
- Font Replacement API —
updateFontFromBytes/Asset/Urlmirroring image replacement - Thumbnail / Snapshot API — GPU-direct frame capture via
RenderTexture.toImage() - Complete DataType Coverage — All 13 Rive DataType variants supported
- DataBind Strategy Parameter —
DataBind.auto(),byName(),byIndex(),empty() - List, Artboard, Integer, SymbolListIndex property types
- Updated to stable Rive runtimes:
rive_native ^0.1.2,rive ^0.14.2
- Enhanced interactive example with side-by-side responsive layout
- Automatic UI control generation from ViewModel properties
- Bidirectional data binding demonstrations
Made with ❤️ for the Flutter community