Rive Animations: Interactive State Machine Animations
Rive Animations: Interactive State Machine Animations
Rive is a real-time interactive design and animation tool that produces compact .riv files consumed directly by Flutter. Unlike traditional sprite sheets or Lottie JSON files, Rive animations embed a state machine — a finite-state automaton that defines states, transitions, and inputs. This lets you drive complex, branching animations entirely from Dart code without shipping hundreds of frame images.
Why Use Rive Over Other Animation Approaches?
- State machines replace conditional animation logic in Dart with designer-controlled graph nodes.
- File size — a rich character animation typically weighs 20–80 KB in
.rivformat versus several MB of sprite frames. - Real-time interpolation — Rive's renderer interpolates between states at runtime, producing silky-smooth transitions without pre-baking frames.
- Two-way binding — Boolean, trigger, and number inputs flow from Dart into the state machine; the machine's active state can be read back.
Step 1 — Add the Dependency and Asset
Add rive to pubspec.yaml and declare your .riv file as a Flutter asset:
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
rive: ^0.13.0 # use the latest stable version
flutter:
assets:
- assets/animations/button.riv
Run flutter pub get to fetch the package. Place button.riv inside assets/animations/.
Step 2 — Load the .riv File and Attach a State Machine Controller
The recommended approach is to load the asset bytes in initState, parse the RiveFile, locate the artboard you want, and attach a StateMachineController. The controller exposes named inputs that you store as fields so you can fire them later.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
class RiveButtonDemo extends StatefulWidget {
const RiveButtonDemo({super.key});
@override
State<RiveButtonDemo> createState() => _RiveButtonDemoState();
}
class _RiveButtonDemoState extends State<RiveButtonDemo> {
// Artboard reference used by RiveAnimation.direct
Artboard? _artboard;
// Inputs retrieved from the state machine
SMITrigger? _pressTrigger;
SMIBool? _isHovered;
@override
void initState() {
super.initState();
_loadRive();
}
Future<void> _loadRive() async {
// 1. Read the asset bytes
final data = await rootBundle.load('assets/animations/button.riv');
// 2. Parse the Rive file
final file = RiveFile.import(data);
// 3. Grab the default (or named) artboard
final artboard = file.mainArtboard;
// 4. Find the state machine by name and attach it
final controller = StateMachineController.fromArtboard(
artboard,
'ButtonMachine', // must match the name in the Rive editor
onStateChange: _onStateChange,
);
if (controller != null) {
artboard.addController(controller);
// 5. Cache the inputs so we can fire them later
_pressTrigger = controller.findInput<SMITrigger>('Press');
_isHovered = controller.findInput<SMIBool>('Hovered');
}
// 6. Trigger a rebuild with the loaded artboard
setState(() => _artboard = artboard);
}
void _onStateChange(String machineName, String stateName) {
debugPrint('State machine "$machineName" entered state "$stateName"');
}
@override
Widget build(BuildContext context) {
if (_artboard == null) return const CircularProgressIndicator();
return GestureDetector(
onTapDown: (_) => _pressTrigger?.fire(),
onTapUp: (_) => _isHovered?.value = false,
child: SizedBox(
width: 200,
height: 80,
child: RiveAnimation.direct(_artboard!),
),
);
}
}
controller != null before using it. StateMachineController.fromArtboard returns null when the machine name does not match — a silent failure that is easy to miss during development.State Machine Input Types
Rive exposes three input primitives that map to Dart types:
SMITrigger— one-shot pulse; call.fire()to send a transient event (e.g., button press, jump).SMIBool— persistent Boolean; set.value = true/false(e.g., hover state, active flag).SMINumber— persistent floating-point; set.value = 0.75(e.g., health percentage, speed blend).
Step 3 — Triggering Transitions from User Input
Wiring Flutter gestures to Rive inputs is straightforward. The example below shows a toggle switch whose idle, hover, and toggle states are all handled by the state machine — your Dart code only manages the SMIBool value:
class RiveToggle extends StatefulWidget {
const RiveToggle({super.key});
@override
State<RiveToggle> createState() => _RiveToggleState();
}
class _RiveToggleState extends State<RiveToggle> {
Artboard? _artboard;
SMIBool? _isOn;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
final bytes = await rootBundle.load('assets/animations/toggle.riv');
final file = RiveFile.import(bytes);
final board = file.mainArtboard;
final ctrl = StateMachineController.fromArtboard(board, 'ToggleMachine');
if (ctrl != null) {
board.addController(ctrl);
_isOn = ctrl.findInput<SMIBool>('IsOn');
}
setState(() => _artboard = board);
}
void _toggle() {
if (_isOn != null) {
_isOn!.value = !_isOn!.value; // flip the Boolean input
}
}
@override
Widget build(BuildContext context) {
if (_artboard == null) {
return const SizedBox(width: 80, height: 40);
}
return GestureDetector(
onTap: _toggle,
child: SizedBox(
width: 80,
height: 40,
child: RiveAnimation.direct(_artboard!),
),
);
}
}
setState() inside the onStateChange callback to trigger a heavy UI rebuild — the callback fires on the render thread. Use it for lightweight side-effects (analytics, audio cues). If you must update Flutter state, dispatch the call via WidgetsBinding.instance.addPostFrameCallback.Using RiveAnimation.network vs RiveAnimation.asset vs RiveAnimation.direct
RiveAnimation.asset('assets/anim.riv', stateMachines: ['Machine'])— simplest; no controller needed for basic playback, but you cannot drive inputs.RiveAnimation.network(url)— streams a remote.riv; same limitation.RiveAnimation.direct(_artboard!)— required when you need aStateMachineControllerfor input-driven animations.
Summary
Integrating Rive state machine animations in Flutter requires four coordinated steps: adding the rive package and asset, loading and parsing the .riv file at runtime, attaching a StateMachineController by machine name, and caching typed inputs (SMITrigger, SMIBool, SMINumber) to fire or set in response to user gestures. This architecture cleanly separates animation design (Rive editor) from interaction logic (Dart), yielding expressive, designer-driven motion without inflating your codebase.