diff --git a/README.md b/README.md
index e87c522..3970b20 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Runtime docs are available in [Rive's help center](https://help.rive.app/runtime
 
 ```yaml
 dependencies:
-  rive: ^0.7.16
+  rive: ^0.7.17
 ```
 
 ## Quick Start
diff --git a/example/lib/example_animation.dart b/example/lib/example_animation.dart
deleted file mode 100644
index cd2eb06..0000000
--- a/example/lib/example_animation.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:rive/rive.dart';
-
-class ExampleAnimation extends StatefulWidget {
-  const ExampleAnimation({Key? key}) : super(key: key);
-
-  @override
-  _ExampleAnimationState createState() => _ExampleAnimationState();
-}
-
-class _ExampleAnimationState extends State<ExampleAnimation> {
-  void _togglePlay() {
-    if (_controller == null) {
-      return;
-    }
-    setState(() => _controller!.isActive = !_controller!.isActive);
-  }
-
-  /// Tracks if the animation is playing by whether controller is running.
-  bool get isPlaying => _controller?.isActive ?? false;
-
-  Artboard? _riveArtboard;
-  RiveAnimationController? _controller;
-  @override
-  void initState() {
-    super.initState();
-
-    // Load the animation file from the bundle, note that you could also
-    // download this. The RiveFile just expects a list of bytes.
-    rootBundle.load('assets/off_road_car.riv').then(
-      (data) async {
-        // Load the RiveFile from the binary data.
-        final file = RiveFile.import(data);
-
-        // The artboard is the root of the animation and gets drawn in the
-        // Rive widget.
-        final artboard = file.mainArtboard.instance();
-        // Add a controller to play back a known animation on the main/default
-        // artboard. We store a reference to it so we can toggle playback.
-        artboard.addController(_controller = SimpleAnimation('idle'));
-        setState(() => _riveArtboard = artboard);
-      },
-    );
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: AppBar(
-        title: const Text('Animation Example'),
-      ),
-      body: Center(
-        child: _riveArtboard == null
-            ? const SizedBox()
-            : Rive(artboard: _riveArtboard!),
-      ),
-      floatingActionButton: FloatingActionButton(
-        onPressed: _togglePlay,
-        tooltip: isPlaying ? 'Pause' : 'Play',
-        child: Icon(
-          isPlaying ? Icons.pause : Icons.play_arrow,
-        ),
-      ),
-    );
-  }
-}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 945415a..32ee5fe 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,5 +1,5 @@
 import 'package:flutter/material.dart';
-import 'package:rive_example/example_animation.dart';
+import 'package:rive_example/play_pause_animation.dart';
 import 'package:rive_example/example_state_machine.dart';
 import 'package:rive_example/liquid_download.dart';
 import 'package:rive_example/little_machine.dart';
@@ -42,7 +42,7 @@ class Home extends StatelessWidget {
                 Navigator.push(
                   context,
                   MaterialPageRoute<void>(
-                    builder: (context) => const ExampleAnimation(),
+                    builder: (context) => const PlayPauseAnimation(),
                   ),
                 );
               },
diff --git a/example/lib/play_pause_animation.dart b/example/lib/play_pause_animation.dart
new file mode 100644
index 0000000..64ff1b8
--- /dev/null
+++ b/example/lib/play_pause_animation.dart
@@ -0,0 +1,50 @@
+import 'package:flutter/material.dart';
+import 'package:rive/rive.dart';
+
+class PlayPauseAnimation extends StatefulWidget {
+  const PlayPauseAnimation({Key? key}) : super(key: key);
+
+  @override
+  _PlayPauseAnimationState createState() => _PlayPauseAnimationState();
+}
+
+class _PlayPauseAnimationState extends State<PlayPauseAnimation> {
+  // Controller for playback
+  late RiveAnimationController _controller;
+
+  // This will toggle between play and pause states for the animation
+  void _togglePlay() {
+    setState(() => _controller.isActive = !_controller.isActive);
+  }
+
+  /// Tracks if the animation is playing by whether controller is running.
+  bool get isPlaying => _controller.isActive;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = SimpleAnimation('idle');
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Animation Example'),
+      ),
+      body: Center(
+        child: RiveControllerAnimation.network(
+          'https://cdn.rive.app/animations/vehicles.riv',
+          controllers: [_controller],
+        ),
+      ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: _togglePlay,
+        tooltip: isPlaying ? 'Pause' : 'Play',
+        child: Icon(
+          isPlaying ? Icons.pause : Icons.play_arrow,
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/rive.dart b/lib/rive.dart
index b7920ce..2c5ee87 100644
--- a/lib/rive.dart
+++ b/lib/rive.dart
@@ -22,3 +22,4 @@ export 'package:rive/src/rive_file.dart';
 export 'package:rive/src/runtime_artboard.dart';
 export 'package:rive/src/state_machine_controller.dart';
 export 'package:rive/src/widgets/rive_animation.dart';
+export 'package:rive/src/widgets/rive_controller_animation.dart';
diff --git a/lib/src/rive_file.dart b/lib/src/rive_file.dart
index 3dde563..c8e17a9 100644
--- a/lib/src/rive_file.dart
+++ b/lib/src/rive_file.dart
@@ -2,6 +2,8 @@ import 'dart:collection';
 import 'dart:typed_data';
 
 import 'package:collection/collection.dart';
+import 'package:flutter/services.dart';
+import 'package:http/http.dart' as http;
 import 'package:rive/src/core/core.dart';
 import 'package:rive/src/core/field_types/core_field_type.dart';
 import 'package:rive/src/generated/animation/animation_state_base.dart';
@@ -219,6 +221,19 @@ class RiveFile {
     return RiveFile._(reader, RuntimeHeader.read(reader));
   }
 
+  /// Imports a Rive file from an asset bundle
+  static Future<RiveFile> asset(String bundleKey) async {
+    final bytes = await rootBundle.load(bundleKey);
+    return RiveFile.import(bytes);
+  }
+
+  /// Imports a Rive file from a url over http
+  static Future<RiveFile> network(String url) async {
+    final res = await http.get(Uri.parse(url));
+    final bytes = ByteData.view(res.bodyBytes.buffer);
+    return RiveFile.import(bytes);
+  }
+
   /// Returns all artboards in the file
   List<Artboard> get artboards => _artboards;
 
diff --git a/lib/src/widgets/rive_animation.dart b/lib/src/widgets/rive_animation.dart
index 6857c48..8dc557a 100644
--- a/lib/src/widgets/rive_animation.dart
+++ b/lib/src/widgets/rive_animation.dart
@@ -1,10 +1,6 @@
-import 'dart:typed_data';
-
-import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:rive/rive.dart';
 import 'package:rive/src/rive_core/artboard.dart';
-import 'package:http/http.dart' as http;
 
 enum _Source {
   asset,
@@ -79,38 +75,16 @@ class _RiveAnimationState extends State<RiveAnimation> {
   @override
   void initState() {
     super.initState();
-    // Load the Rive file from the asset bundle
-    switch (widget.src) {
-      case _Source.asset:
-        _loadAsset();
-        break;
-      case _Source.network:
-        _loadNetwork();
-        break;
+
+    if (widget.src == _Source.asset) {
+      RiveFile.asset(widget.name).then(_init);
+    } else if (widget.src == _Source.network) {
+      RiveFile.network(widget.name).then(_init);
     }
   }
 
-  /// Loads a Rive file from an asset bundle and configures artboard, animation,
-  /// and controller.
-  void _loadAsset() {
-    rootBundle.load(widget.name).then(
-      (data) async {
-        _init(data);
-      },
-    );
-  }
-
-  /// Loads a Rive file from an HTTP source and configures artboard, animation,
-  /// and controller.
-  Future<void> _loadNetwork() async {
-    final res = await http.get(Uri.parse(widget.name));
-    final data = ByteData.view(res.bodyBytes.buffer);
-    _init(data);
-  }
-
   /// Initializes the artboard, animation, and controller
-  void _init(ByteData data) {
-    final file = RiveFile.import(data);
+  void _init(RiveFile file) {
     final artboard = widget.artboard != null
         ? file.artboardByName(widget.artboard!)
         : file.mainArtboard;
diff --git a/lib/src/widgets/rive_controller_animation.dart b/lib/src/widgets/rive_controller_animation.dart
new file mode 100644
index 0000000..0066a25
--- /dev/null
+++ b/lib/src/widgets/rive_controller_animation.dart
@@ -0,0 +1,115 @@
+import 'package:flutter/widgets.dart';
+import 'package:rive/rive.dart';
+import 'package:rive/src/rive_core/artboard.dart';
+
+enum _Source {
+  asset,
+  network,
+}
+
+/// High level widget that plays an animation from a Rive file. If artboard or
+/// animation are not specified, the default artboard and first animation fonund
+/// within it are used.
+class RiveControllerAnimation extends StatefulWidget {
+  /// The asset name or url
+  final String name;
+
+  /// The type of source used to retrieve the asset
+  final _Source src;
+
+  /// The name of the artboard to use; default artboard if not specified
+  final String? artboard;
+
+  /// List of Rive controllers to attach
+  final List<RiveAnimationController> controllers;
+
+  /// Fit for the animation in the widget
+  final BoxFit? fit;
+
+  /// Alignment for the animation in the widget
+  final Alignment? alignment;
+
+  /// Enable/disable antialiasing when rendering
+  final bool antialiasing;
+
+  /// Widget displayed while the rive is loading
+  final Widget? placeHolder;
+
+  /// Creates a new RiveControllerAnimation from an asset bundle
+  const RiveControllerAnimation.asset(
+    this.name, {
+    this.artboard,
+    this.controllers = const [],
+    this.fit,
+    this.alignment,
+    this.placeHolder,
+    this.antialiasing = true,
+  }) : src = _Source.asset;
+
+  const RiveControllerAnimation.network(
+    this.name, {
+    this.artboard,
+    this.controllers = const [],
+    this.fit,
+    this.alignment,
+    this.placeHolder,
+    this.antialiasing = true,
+  }) : src = _Source.network;
+
+  @override
+  _RiveControllerAnimationState createState() =>
+      _RiveControllerAnimationState();
+}
+
+class _RiveControllerAnimationState extends State<RiveControllerAnimation> {
+  /// Rive controller
+  final _controllers = <RiveAnimationController>[];
+
+  /// Active artboard
+  Artboard? _artboard;
+
+  @override
+  void initState() {
+    super.initState();
+
+    if (widget.src == _Source.asset) {
+      RiveFile.asset(widget.name).then(_init);
+    } else if (widget.src == _Source.network) {
+      RiveFile.network(widget.name).then(_init);
+    }
+  }
+
+  /// Initializes the artboard, animation, and controller
+  void _init(RiveFile file) {
+    final artboard = widget.artboard != null
+        ? file.artboardByName(widget.artboard!)
+        : file.mainArtboard;
+
+    if (artboard == null) {
+      throw const FormatException('Unable to load artboard');
+    }
+    if (artboard.animations.isEmpty) {
+      throw FormatException('No animations in artboard ${artboard.name}');
+    }
+
+    // Attach each controller to the artboard
+    widget.controllers.forEach(artboard.addController);
+    setState(() => _artboard = artboard);
+  }
+
+  @override
+  void dispose() {
+    _controllers.forEach((c) => c.dispose());
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) => _artboard != null
+      ? Rive(
+          artboard: _artboard!,
+          fit: widget.fit,
+          alignment: widget.alignment,
+          antialiasing: widget.antialiasing,
+        )
+      : widget.placeHolder ?? const SizedBox();
+}