The Rive engineering team’s a fan of Flutter; so much so that we used it to rebuild the Rive editor, which we currently deploy to web and MacOS, with more platforms on the way.

While most of our runtimes use a shared C++ core, the Flutter runtime does not. It shares the editor’s rendering core, allowing us to maintain a single Flutter-based renderer written in Dart. It also has a different API style, more in keeping Flutter’s widget pattern. The runtime’s available in pub and Github.

Let’s dive into the runtime, explore how to play animations, and how to mix them in code.

Playing Rive animations in Flutter

In this first example we’ll take a spin in a monster truck. The truck.riv file contains 4 animations: idle, bouncing, windshield_wipers, and broken. These are designed to be mixed together to create sophisticated behavior, but let’s start simple and animate the truck in its idle state.

We’ll add the rive package, and truck.riv to pubspec.yaml:

dependencies:
  rive: ^0.6.2

flutter:
  assets:
    - assets/truck.riv
pubspec.yaml

Here’s the code in its entirety. It loads a Rive file, initializes and plays the idle animation.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Rive Flutter Demo',
      home: Scaffold(
        body: MyRiveAnimation(),
      ),
    );
  }
}

class MyRiveAnimation extends StatefulWidget {
  @override
  _MyRiveAnimationState createState() => _MyRiveAnimationState();
}

class _MyRiveAnimationState extends State<MyRiveAnimation> {
  final riveFileName = 'assets/truck.riv';
  Artboard _artboard;

  @override
  void initState() {
    _loadRiveFile();
    super.initState();
  }

  // loads a Rive file
  void _loadRiveFile() async {
    final bytes = await rootBundle.load(riveFileName);
    final file = RiveFile();

    if (file.import(bytes)) {
      // Select an animation by its name
      setState(() => _artboard = file.mainArtboard
        ..addController(
          SimpleAnimation('idle'),
        ));
    }
  }

  /// Show the rive file, when loaded
  @override
  Widget build(BuildContext context) {
    return _artboard != null
        ? Rive(
            artboard: _artboard,
            fit: BoxFit.cover,
          )
        : Container();
  }
}
main.dart

We’ve created a widget, MyRiveAnimation, which is stateful because we load in the Rive file directly inside the widget, and we only want to load once on initialization. Note that we load raw bytes. This gives you flexibility in where you source your Rive data: as assets, over a network, from a local database, or by other means.

Once the data is imported into a RiveFile, we can access any of the file’s artboards and play the animations they contain. In our truck example, there’s one artboard, so we can fetch that with the file.mainArtboard property.

We attach RiveAnimationControllers to artboards that will give us control of how the artboard’s animations are played and mixed. Here we use a basic animation controller -- SimpleAnimation -- that comes with the rive package. The controller auto-plays the specified animation.

We use the Rive widget to render animations. This widget takes an artboard and an optional fit parameter.

And that’s it; the idle animation should now be playing in your app. If you want to see the other animations, simply swap out idle for bouncing, or windshield_wipers.

This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.

Mixing Rive animations

What if you wanted to play multiple animations at the same time?

One of Rive’s strengths is its ability to mix animations together and to independently control animation playback through code. Let’s see how we can get both idle and windshield_wipers animating concurrently:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Rive Flutter Demo',
      home: Scaffold(
        body: MyRiveAnimation(),
      ),
    );
  }
}

class MyRiveAnimation extends StatefulWidget {
  @override
  _MyRiveAnimationState createState() => _MyRiveAnimationState();
}

class _MyRiveAnimationState extends State<MyRiveAnimation> {
  Artboard _artboard;
  RiveAnimationController _wipersController;
  // flag to turn on and off the wipers
  bool _wipers = false;

  @override
  void initState() {
    _loadRiveFile();
    super.initState();
  }

  /// Loads a Rive file
  void _loadRiveFile() async {
    final bytes = await rootBundle.load('assets/truck.riv');
    final file = RiveFile();
    if (file.import(bytes)) {
      setState(() => _artboard = file.mainArtboard
        ..addController(SimpleAnimation('idle')));
    }
  }

  void _wipersChange(bool wipersOn) {
    if (_wipersController == null) {
      _artboard.addController(
        _wipersController = SimpleAnimation('windshield_wipers'),
      );
    }
    setState(() => _wipersController.isActive = _wipers = wipersOn);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: _artboard != null
              ? Rive(
                  artboard: _artboard,
                  fit: BoxFit.cover,
                )
              : Container(),
        ),
        SizedBox(
          height: 50,
          width: 200,
          child: SwitchListTile(
            title: const Text('Wipers'),
            value: _wipers,
            onChanged: _wipersChange,
          ),
        ),
      ],
    );
  }
}
main.dart

Much of this code remains unchanged. The primary difference is that there are now two SimpleAnimation controllers: one for the idle animation and is always running, and one for windshield_wipers, that can be started and stopped.

The Switch widget fires _wipersChange, which creates the wipers controller the first time it's run, and starts or stops the animation by setting isActive on the controller. When both animations are playing, they’re mixed together; note how the wiper movement is blended with the bouncing motion of the truck.

This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.
Wipers

Customizing animation control

When playing the windshield_wipers animation, you can see how the truck’s wipers will pause as soon as you hit the switch. What if we don’t want our wipers to get in the way of our view when they’re not active and instead return to their start state?

Animation controllers let us customize animation behavior at runtime. windshield_wipers is a loop animation, meaning it will repeatedly play from beginning to end while active. There are two other animation modes: oneShot plays the animation from beginning to end, and then stops; pingPong will play an animation from beginning to end and then end to beginning repeatedly.

Lets introduce our own WiperAnimationController to control our animation:

import 'package:rive/rive.dart';

class WiperAnimation extends SimpleAnimation {
  WiperAnimation(String animationName) : super(animationName);

  start() {
    instance.animation.loop = Loop.loop;
    isActive = true;
  }

  stop() => instance.animation.loop = Loop.oneShot;
}
wiper_controller.dart

WiperAnimation extends SimpleAnimation and provides two new functions: stop flips the animation loop mode to oneShot, meaning that the animation will complete and stop, rather than looping indefinitely; start sets the loop mode back to loop and reactivates the animation.

But what disables the animation with stop? The SimpleAnimation controllers apply function takes care of that by deactivating animations that have completed.

Now that we have our WiperAnimation controller we can use it instead of the SimpleAnimation controller in our app:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Gone fishing.',
     home: Scaffold(
       body: MyRiveAnimation(),
     ),
   );
 }
}

class MyRiveAnimation extends StatefulWidget {
 @override
 _MyRiveAnimationState createState() => _MyRiveAnimationState();
}

class _MyRiveAnimationState extends State<MyRiveAnimation> {
 final riveFileName = 'assets/truck.riv';
 Artboard _artboard;
 WiperAnimation _wipersController;
 // Flag to turn wipers on and off
 bool _wipers = false;

 @override
 void initState() {
   _loadRiveFile();
   super.initState();
 }

 void _loadRiveFile() async {
   final bytes = await rootBundle.load(riveFileName);
   final file = RiveFile();

   if (file.import(bytes)) {
     setState(() => _artboard = file.mainArtboard
       ..addController(
         SimpleAnimation('idle'),
       ));
   }
 }

 void _wipersChange(bool wipersOn) {
   if (_wipersController == null) {
     _artboard.addController(
       _wipersController = WiperAnimation('windshield_wipers'),
     );
   }
   if (wipersOn) {
     _wipersController.start();
   } else {
     _wipersController.stop();
   }
   setState(() => _wipers = wipersOn);
 }

 @override
 Widget build(BuildContext context) {
   return Column(
     children: [
       Expanded(
         child: _artboard != null
             ? Rive(
                 artboard: _artboard,
                 fit: BoxFit.cover,
               )
             : Container(),
       ),
       SizedBox(
         height: 50,
         width: 200,
         child: SwitchListTile(
           title: const Text('Wipers'),
           value: _wipers,
           onChanged: _wipersChange,
         ),
       ),
     ],
   );
 }
}
main.dart

Take the truck for a spin and watch the wipers, you’ll notice that the wiper animation now completes its animations if it’s toggled to stop.

That’s it for part one; we’ve just scratched the surface of what can be done with animation controllers and mixing. In part 2 we’ll take our truck off-road and cover programmatically controlling animation speed and more advanced ways to mix animations.

This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.
Wipers