Building a Water Tracking App with Flare & Flutter
Diving into Flare’s Interactive Side
--
Getting Started
Flare is a powerful animation tool that you can use for both UI and character design in apps and games. By using Flare, you can easily improve the user experience of your mobile application by adding fluid animations that your users can interact with. Flare has runtimes that support an array of languages, and for this, we’ll be using the Flare-Flutter runtimes found here. In this tutorial, I’ll walk you through how to export a Flare file and build it into an app using Flutter. You’ll need to sign up for a 2Dimensions account here and set up your Flutter dev environment using these instructions.
In this tutorial, you’ll learn how to:
1) Mix Animations in Flare
2) Play multiple animations simultaneously
3) Build a Flutter app with interactive animations
The App
The Water Tracker app is a simple way to keep track of how many glasses of water you’ve consumed today. Decide how many eight-ounce glasses of water you want to drink and each time you have one, just click the plus button. When you start a new day, easily reset your stats. Here’s what it’ll look like when we are done! The Flare file can be found here, and it encompasses the entire UI that we’ll be using to create our water tracking mobile app. Let’s get started with exporting the file, and then we’ll dive into some code!
Exporting Fun!
Let’s jump right into the Flare here and export our file we’ll be using in Flutter! Flare works best when using Chrome or Firefox and if you have any issues, check out our FAQ here. Looking at the Editor, you’ll notice all of the animations at the bottom left corner that we’ll be using. As you play through each one, you see the animation name, length, and if it loops. These are also all parameters that are available to read in code. Feel free to fork this file and play around with the animations, bones, keyframes, etc.
At the top right, you’ll notice an export button with a few options available. For this tutorial, we’ll be using the “Export” feature and download it as a ‘Binary’ file with the toggle on for ‘Duration from last keyframe’. Save it in a place where you’ll remember, and we’ll drop it in our project directory in a bit!
Setting Up Our Project
First, let’s open Android studio and “Start a new Flutter application”. If you need help setting up your development environment for Flutter, you can find instructions here.
Now let’s set our “Project Name”, I called mine “water_tracker”, but feel free to set this to what you want, keeping in mind the naming convention that Android Studio requires. If you already set up your development environment, the “Flutter SDK path” should be set for you, but you can also navigate to it and set it. For the “Project location”, it is pre-filled in for you, but you may change this too. Finally, we have a short “Description” for our app. Once you decided on that, go ahead and click the next button. Next, we need to “Set the package name” that will be used in publishing our app. We’ll also include support for both iOS and Android since Flutter, and Flare can deploy to both! Hit the “Finish” button, and your app will be created.
The first thing we need to do once our app is created will be to update the “pubspec.yaml” file to include the packages for Flare. To do this, simply find the latest version here and reference it in the dependencies as below. Once you have the version set, make sure to click the “Packages get” button in blue to download the Flare library to the project.
Flare: Imports & Exports
Now that we have our basic project set up with our Flare libraries, we can create a new ‘directory’ called ‘assets’ so that we can import our ‘.flr’ file that we exported from the editor.
In order to utilize this file in code, we’ll first need to update our ‘pubspec.yaml’ to have a new section called ‘assets’ and add the ‘WaterApp.flr’ file to it. That should be it for the ‘pubspec’, now on to the fun stuff!
Diving Into Code
The first thing we want to do is clear out all of the boilerplate code in the ‘main.dart’ file. We’ll make ourselves a little checklist of the things we want to include in the code, so we can track our progress and keep our focus. Let’s also remove the UI overlays that are visible once we run the app in the simulator (or on the device) so that we can have a full-screen view of our app.
import 'package:flutter/material.dart';
import 'tracking_input.dart';
import 'package:flutter/services.dart';void main() {
///let's remove the Android buttons. For the purpose of this app, we don't need/want em on screen!
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.pink,
),
home: TrackingInput(),);
}
}
/*
* We'll need:
* to import .flr
* controller for the water
* math for add and sub water and correlate to animation
* reset progress button
* set goal for how many cups
*/
Finally, you’ll notice a few errors pointing to the ‘tracking_input.dart’, so let’s fix that now by adding a new Dart file to your project with the name ‘tracking_input’. Majority of the app will live in this file.
Let’s add the necessary code to our new file and import the Flare files we’ll be needing.
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_controls.dart';
import 'flare_controller.dart';class TrackingInput extends StatefulWidget {@override
TrackingState createState() => new TrackingState();}class TrackingState extends State<TrackingInput> {
void initState() {
super.initState();
}@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
}
}
You’ll notice there’s an error on line 4, we need to create another class! The imported file ‘flare_controller.dart’ will be the class where we handle all of our animations, so let’s also add that to the project now.
Let’s set up the controller class first since we’ll be using a lot of this code in our ‘tracking_input.dart’.
This new file that will handle the majority of our animations, mixing animations and our fill logic for the water and our cute little ice cube. Let’s drop in our basic boilerplate code required for extending FlareController and add the imports we’ll be using.
import 'dart:math';
import 'package:flare_dart/math/mat2d.dart';
import 'package:flare_flutter/flare.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_controller.dart';class AnimationControls extends FlareController {
void initialize(FlutterActorArtboard artboard) {
}void setViewTransform(Mat2D viewTransform) {}bool advance(FlutterActorArtboard artboard, double elapsed) {}
We can go ahead and declare all the variables we’ll need in this class, such as the artboard, the animations, and the doubles for animating that will be passed to this class.
class AnimationControls extends FlareController {
///so we can reference this any where once we declare it
FlutterActorArtboard _artboard;///our fill animation, so we can animate this each time we add/reduce water intake
ActorAnimation _fillAnimation;///our ice cube that moves on the Y Axis based on current water intake
ActorAnimation _iceboyMoveY;///used for mixing animations
final List<FlareAnimationLayer> _baseAnimations = [];///our overall fill
double _waterFill = 0.00;
///current amount of water consumed
double _currentWaterFill = 0;///time used to smooth the fill line movement
double _smoothTime = 5;
We can initialize some of our vars that we declared earlier and set up our ‘advance’ function that advances the animation of the artboard by the elapsed time.
void initialize(FlutterActorArtboard artboard) {
//get the reference here on start to our animations and artboard
_artboard = artboard;
_fillAnimation = artboard.getAnimation("water up");
_iceboyMoveY = artboard.getAnimation("iceboy_move_up");
}bool advance(FlutterActorArtboard artboard, double elapsed) {
//we need this separate from our generic mixing animations,
// b/c the animation duration is needed in this calculation
if(artboard.name.compareTo("Artboard") == 0){
_currentWaterFill += (_waterFill-_currentWaterFill) * min(1, elapsed *
_smoothTime);
_fillAnimation.apply( _currentWaterFill * _fillAnimation.duration, artboard, 1);
_iceboyMoveY.apply(_currentWaterFill * _iceboyMoveY.duration, artboard, 1);
}int len = _baseAnimations.length - 1;
for (int i = len; i >= 0; i--) {
FlareAnimationLayer layer = _baseAnimations[i];
layer.time += elapsed;
layer.mix = min(1.0, layer.time / 0.01);
layer.apply(_artboard); if (layer.isDone) {
_baseAnimations.removeAt(i);
}
}
}
return true;
Next, we need to set up our functions that will be called from our ‘tracking_input.dart’ class to manage the animations and status of the ‘water up’ animation.
///called from the 'tracking_input'
void playAnimation(String animName){
ActorAnimation animation = _artboard.getAnimation(animName); if (animation != null) {
_baseAnimations.add(FlareAnimationLayer()
..name = animName
..animation = animation
);
}
}
///called from the 'tracking_input'
///updates the water fill line
void updateWaterPercent(double amt){_waterFill = amt;}
///called from the 'tracking_input'
///resets the water fill line
void resetWater(){_waterFill = 0;
}
Now that we have the animations all setup, we can jump back to ‘tracking_input.dart’ and utilize these methods we’ve set up here!
Firstly, our import on line 3 should now be free of any errors, and we can now declare the variables that we’ll use.
///these get set when we build the widget
double screenWidth = 0.0;
double screenHeight = 0.0;
///this is the animation controller for the water and iceBoy
AnimationControls _flareController;
///an example of how to set up individual controllers
final FlareControls plusWaterControls = FlareControls();
final FlareControls minusWaterControls = FlareControls();
///the current number of glasses drunk
int currentWaterCount = 0;
///this will come from the selectedGlasses times ouncesPerGlass
/// we'll use this to calculate the transform of the water fill animation
int maxWaterCount = 0;
///we'll default at 8, but this will change based on user input
int selectedGlasses = 8;
///this doesn't change, hence the 'static const', we always count 8 ounces
///per glass (it's assuming)
static const int ouncePerGlass = 8;
We can now add the methods we’ll call from our buttons to change our water intake goals, add or decrease our current water intake, reset our data for the day and toggle our settings tray to open or close.
///this is a quick reset for the user, to reset the intake back to zero
void _resetDay() {
setState(() {
currentWaterCount = 0;
_flareController.resetWater();
});
}
///we'll use this to increase how much water the user has drunk, hooked
///via button
void _incrementWater() {
setState(() {
if (currentWaterCount < selectedGlasses) {
currentWaterCount = currentWaterCount + 1;
double diff = currentWaterCount / selectedGlasses;
plusWaterControls.play("plus press");
_flareController.playAnimation("ripple");
_flareController.updateWaterPercent(diff);
}
if (currentWaterCount == selectedGlasses) {
_flareController.playAnimation("iceboy_win");
}
});
}
///we'll use this to decrease our user's water intake, hooked to a button
void _decrementWater() {
setState(() {
if (currentWaterCount > 0) {
currentWaterCount = currentWaterCount - 1;
double diff = currentWaterCount / selectedGlasses;
_flareController.updateWaterPercent(diff);
_flareController.playAnimation("ripple");
} else {
currentWaterCount = 0;
}
minusWaterControls.play("minus press");
});
}
void calculateMaxOunces() {
maxWaterCount = selectedGlasses * ouncePerGlass;
}
void _incSelectedGlasses(StateSetter updateModal, int value) {
updateModal(() {
selectedGlasses = (selectedGlasses + value).clamp(0, 26).toInt();
calculateMaxOunces();
});
}
We can start getting things set up for our animations and initialize our var at the start of the application.
void initState() {
_flareController = AnimationControls(); super.initState();
}
We have our three classes set up now and without errors. But when you run the app, nothing happens! Let’s start building the fun widgets that our app is centered around.
Building Widgets
Finally, we get down to business with building out our widgets and seeing this app and IceBoy come to life! We’ll need a few buttons, a few containers for those buttons to place them properly, a bottom sheet for our tray, a text widget for changing our goal amount and most importantly our Flare widgets. Here’s what our main Button Widgets will look like:
Widget settingsButton() {
return RawMaterialButton(
constraints: BoxConstraints.tight(Size(95, 30)),
onPressed: _showMenu,
shape: Border(),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
elevation: 0.0,
child: FlareActor("assets/WaterArtboards.flr",
fit: BoxFit.contain,
sizeFromArtboard: true,
artboard: "UI Ellipse"),
);
}
Widget addWaterBtn() {
return RawMaterialButton(
constraints: BoxConstraints.tight(const Size(150, 150)),
onPressed: _incrementWater,
shape: Border(),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
elevation: 0.0,
child: FlareActor("assets/WaterArtboards.flr",
controller: plusWaterControls,
fit: BoxFit.contain,
animation: "plus press",
sizeFromArtboard: false,
artboard: "UI plus"),
);
}
Widget subWaterBtn() {
return RawMaterialButton(
constraints: BoxConstraints.tight(const Size(150, 150)),
onPressed: _decrementWater,
shape: Border(),
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
elevation: 0.0,
child: FlareActor("assets/WaterArtboards.flr",
controller: minusWaterControls,
fit: BoxFit.contain,
animation: "minus press",
sizeFromArtboard: true,
artboard: "UI minus"),
);
}
}/// Button with a Flare widget that automatically plays
/// a Flare animation when pressed. Specify which animation
/// via [pressAnimation] and the [artboard] it's in.
class FlareWaterTrackButton extends StatefulWidget {
final String pressAnimation;
final String artboard;
final VoidCallback onPressed;
const FlareWaterTrackButton(
{this.artboard, this.pressAnimation, this.onPressed});
@override
_FlareWaterTrackButtonState createState() => _FlareWaterTrackButtonState();
}
class _FlareWaterTrackButtonState extends State<FlareWaterTrackButton> {
final _controller = FlareControls();
@override
Widget build(BuildContext context) {
return RawMaterialButton(
constraints: BoxConstraints.tight(const Size(95, 85)),
onPressed: () {
_controller.play(widget.pressAnimation);
widget.onPressed?.call();
},
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: FlareActor("assets/WaterArtboards.flr",
controller: _controller,
fit: BoxFit.contain,
artboard: widget.artboard),
);
}
}
We will have these widgets split. The first three will be the children in our parent widget, and the rest will go in the bottom sheet. When we put it all together, here’s what it looks like:
@override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
screenHeight = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: const Color.fromRGBO(93, 93, 93, 1),
body: Container(
///Stack some widgets
color: const Color.fromRGBO(93, 93, 93, 1),
child: Stack(
fit: StackFit.expand,
children: [
///this is our main artboard with iceboy and the water fill
FlareActor(
"assets/WaterArtboards.flr",
controller: _flareController,
fit: BoxFit.contain,
animation: "iceboy",
artboard: "Artboard",
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Spacer(),
///Each widget on the main view
addWaterBtn(),
subWaterBtn(),
settingsButton(),
],
)
],
),
),
);
}///set up our bottom sheet menu
void _showMenu() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter updateModal) {
return Container(
decoration: BoxDecoration(
color: const Color.fromRGBO(93, 93, 93, 1),
),
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
"Set Target",
style: TextStyle(
fontWeight: FontWeight.normal,
color: Colors.white,
fontSize: 24.0),
textAlign: TextAlign.center,
),
// Some vertical padding between text and buttons row
const SizedBox(height: 20),
Row(
children: [
///our animated button that increases your goal
FlareWaterTrackButton(
artboard: "UI arrow left",
pressAnimation: "arrow left press",
onPressed: () => _incSelectedGlasses(updateModal, -1),
),
Expanded(
child: Text(
selectedGlasses.toString(),
style: TextStyle(
fontWeight: FontWeight.normal,
color: Colors.white,
fontSize: 40.0),
textAlign: TextAlign.center,
),
),
///our animated button that decreases your goal
FlareWaterTrackButton(
artboard: "UI arrow right",
pressAnimation: "arrow right press",
onPressed: () => _incSelectedGlasses(updateModal, 1),
),
],
),
// Some vertical padding between text and buttons row
const SizedBox(height: 20),
Text(
"/glasses",
style: TextStyle(
fontWeight: FontWeight.normal,
color: Colors.white,
fontSize: 20.0),
textAlign: TextAlign.center,
),
// Some vertical padding between text and buttons row
const SizedBox(height: 20),
///our Flare button that closes our menu
///TODO: Here is your challenge!
FlareWaterTrackButton(
artboard: "UI refresh",
onPressed: () {
_resetDay();
// close modal
Navigator.pop(context);
},
),
],
),
);
},
);
},
);
}
We should be able to run our app now and see IceBoy in all his cuteness moving about the screen as we change our water intake! If you press the ellipsis at the bottom of the screen, you can see our settings tray animate up, and we can adjust our daily goal or reset our stats. Congratulations, you’ve completed an interactive app using Flare in Flutter!
Where To Now?
Now that you’ve completed this tutorial, you should have a better understanding of how to integrate Flare into your Flutter apps! Adding Flare to our project not only brought the user experience to a new level, but it also gives designers more control over the look and feel of our app with little effort on the code side.
We have much more coming to Flare so stay tuned for the next tutorial in our series! You can view the full source code here. Please join in our conversation in Discord and follow us on social media for updates to Flare. If this tutorial helped you out, give us a clap (or 50!) and leave a comment below with any questions about this tutorial!