Zero to One with Flutter
It was late summer 2016, and my first task as a new hire at the Google office in Aarhus, Denmark was to implement animated charts in an Android/iOS app using Flutter and Dart. Besides being a “Noogler”, I was new to Flutter, new to Dart, and new to animations. In fact, I had never done a mobile app before. My very first smartphone was just a few months old — bought in a fit of panic that I might fail the phone interview by answering the call with my old Nokia...
I did have some prior experience with charts from desktop Java, but that wasn’t animated. I felt… weird. Partly a dinosaur, partly reborn.
TL;DR Discovering the strength of Flutter’s widget and tween concepts by writing chart animations in Dart for an Android/iOS app.
Moving to a new development stack makes you aware of your priorities. Near the top of my list are these three:
- Strong concepts deal effectively with complexity by providing simple, relevant ways of structuring thoughts, logic, or data.
- Clear code lets us express those concepts cleanly, without being distracted by language pitfalls, excessive boilerplate, or auxiliary detail.
- Fast iteration is key to experimentation and learning — and software development teams learn for a living: what the requirements really are, and how best to fulfill them with concepts expressed in code.
Flutter is a new platform for developing Android and iOS apps from a single codebase, written in Dart. Since our requirements spoke of a fairly complex UI including animated charts, the idea of building it only once seemed very attractive. My tasks involved exercising Flutter’s CLI tools, some pre-built widgets, and its 2D rendering engine — in addition to writing a lot of plain Dart code to model and animate charts. I’ll share below some conceptual highlights of my learning experience, and provide a starting point for your own evaluation of the Flutter/Dart stack.
This is part one of a two-part introduction to Flutter and its ‘widget’ and ‘tween’ concepts. I’ll illustrate the strength of these concepts by using them to display and animate charts like the one shown above. Full code samples should provide an impression of the level of code clarity achievable with Dart. And I’ll include enough detail that you should be able to follow along on your own laptop (and emulator or device), and experience the length of the Flutter development cycle.
The starting point is a fresh installation of Flutter. Run
$ flutter doctor
to check the setup:
$ flutter doctor
[✓] Flutter (on Mac OS, channel master)
• Flutter at /Users/mravn/flutter
• Framework revision 64bae978f1 (7 hours ago), 2017-02-18 21:00:27
• Engine revision ab09530927
• Tools Dart version 1.23.0-dev.0.0
[✓] Android toolchain - develop for Android devices
(Android SDK 24.0.2)
• Android SDK at /Users/mravn/Library/Android/sdk
• Platform android-25, build-tools 24.0.2
• Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
[✓] iOS toolchain - develop for iOS devices (Xcode 8.2.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 8.2.1, Build version 8C1002
• ios-deploy 1.9.1
[✓] IntelliJ IDEA Community Edition (version 2016.3.4)
• Dart plugin version 163.13137
• Flutter plugin version 0.1.10
[✓] Connected devices
• iPhone SE • 664A33B0-A060-4839-A933-7589EF46809B • ios •
iOS 10.2 (simulator)
With enough check marks, you can create a Flutter app. Let’s call it
$ flutter create charts
That should give you a directory of the same name:
About fifty files have been generated, making up a complete sample app that can be installed on both Android and iOS. We’ll do all our coding in
main.dart and sibling files, with no pressing need to touch any of the other files or directories.
You should verify that you can launch the sample app. Start an emulator or plug in a device, then execute
$ flutter run
charts directory. You should then see a simple counting app on your emulator or device. It uses Material Design widgets, which is nice, but optional. As the top-most layer of the Flutter architecture, those widgets are completely replaceable.
Let’s start by replacing the contents of
main.dart with the code below, a simple starting point for playing with chart animations.
Save the changes, then restart the app. You can do that from the terminal, by pressing
R. This ‘full restart’ operation throws away the application state, then rebuilds the UI. For situations where the existing application state still makes sense after the code change, one can press
r to do a ‘hot reload’, which only rebuilds the UI. There is also a Flutter plugin for IntelliJ IDEA providing the same functionality integrated with a Dart editor:
Once restarted, the app shows a centered text label saying
Data set: null and a floating action button to refresh the data. Yes, humble beginnings.
To get a feel for the difference between hot reload and full restart, try the following: After you’ve pressed the floating action button a few times, make a note of the current data set number, then replace
Icons.add in the code, save, and do a hot reload. Observe that the button changes, but that the application state is retained; we’re still at the same place in the random stream of numbers. Now undo the icon change, save, and do a full restart. The application state has been reset, and we’re back to
Data set: null.
Our simple app shows two central aspects of the Flutter widget concept in action:
- The user interface is defined by a tree of immutable widgets which is built via a foxtrot of constructor calls (where you get to configure widgets) and
buildmethods (where widget implementations get to decide how their sub-trees look). The resulting tree structure for our app is shown below, with the main role of each widget in parentheses. As you can see, while the widget concept is quite broad, each concrete widget type typically has a very focused responsibility.
ChartPage (state management)
FloatingActionButton (user interaction)
- With an immutable tree of immutable widgets defining the user interface, the only way to change that interface is to rebuild the tree. Flutter takes care of that, when the next frame is due. All we have to do is tell Flutter that some state on which a subtree depends has changed. The root of such a state-dependent subtree must be a
StatefulWidget. Like any decent widget, a
StatefulWidgetis not mutable, but its subtree is built by a
Stateobject which is. Flutter retains
Stateobjects across tree rebuilds and attaches each to their respective widget in the new tree during building. They then determine how that widget’s subtree is built. In our app,
State. Whenever the user presses the button, we execute some code to change
ChartPageState.We’ve demarcated the change with
setStateso that Flutter can do its housekeeping and schedule the widget tree for rebuilding. When that happens,
ChartPageStatewill build a slightly different subtree rooted at the new instance of
Immutable widgets and state-dependent subtrees are the main tools that Flutter puts at our disposal to address the complexities of state management in elaborate UIs responding to asynchronous events such as button presses, timer ticks, or incoming data. From my desktop experience I’d say this complexity is very real. Assessing the strength of Flutter’s approach is — and should be — an exercise for the reader: try it out on something non-trivial.
Our charts app will stay simple in terms of widget structure, but we’ll do a bit of animated custom graphics. First step is to replace the textual representation of each data set with a very simple chart. Since a data set currently involves only a single number in the interval
0..100, the chart will be a bar chart with a single bar, whose height is determined by that number. We’ll use an initial value of
50 to avoid a
CustomPaint is a widget that delegates painting to a
CustomPainter strategy. Our implementation of that strategy draws a single bar.
Next step is to add animation. Whenever the data set changes, we want the bar to change height smoothly rather than abruptly. Flutter has an
AnimationController concept for orchestrating animations, and by registering a listener, we’re told when the animation value — a double running from zero to one — changes. Whenever that happens, we can call
setState as before and update
For reasons of exposition, our first go at this will be ugly:
Ouch. Complexity already rears its ugly head, and our data set is still just a single number! The code needed to set up animation control is a minor concern, as it doesn’t ramify when we get more chart data. The real problem is the variables
endHeight which reflect the changes made to the data set and the animation value, and are updated in three different places.
We are in need of a concept to deal with this mess.
Enter tweens. While far from unique to Flutter, they are a delightfully simple concept for structuring animation code. Their main contribution is to replace the imperative approach above with a functional one. A tween is a value. It describes the path taken between two points in a space of other values, like bar charts, as the animation value runs from zero to one.
Tweens are generic in the type of these other values, and can be expressed in Dart as objects of the type
lerp comes from the field of computer graphics and is short for both linear interpolation (as a noun) and linearly interpolate (as a verb). The parameter
t is the animation value, and a tween should thus lerp from
t is zero) to
t is one).
The Flutter SDK’s
Tween<T> class is very similar to the above, but is a concrete class that supports mutating
end. I’m not entirely sure why that choice was made, but there are probably good reasons for it in areas of the SDK’s animation support that I have yet to explore. In the following, I’ll use the Flutter
Tween<T>, but pretend it is immutable.
We can clean up our code using a single
Tween<double> for the bar height:
Tween for packaging the bar height animation end-points in a single value. It interfaces neatly with the
CustomPainter, avoiding widget tree rebuilds during animation as the Flutter infrastructure now marks
CustomPaint for repaint at each animation tick, rather than marking the whole
ChartPage subtree for rebuild, relayout, and repaint. These are definite improvements. But there’s more to the tween concept; it offers structure to organize our thoughts and code, and we haven’t really taken that seriously. The tween concept says,
Ts by tracing out a path in the space of all
Ts as the animation value runs from zero to one. Model the path with a
In the code above,
T is a
double, but we do not want to animate
doubles, we want to animate bar charts! Well, OK, single bars for now, but the concept is strong, and it scales, if we let it.
(You may be wondering why we don’t take that argument a step further and insist on animating data sets rather than their representations as bar charts. That’s because data sets — in contrast to bar charts which are graphical objects — generally do not inhabit spaces where smooth paths exist. Data sets for bar charts typically involve numerical data mapped against discrete data categories. But without the spatial representation as bar charts, there is no reasonable notion of a smooth path between two data sets involving different categories.)
Returning to our code, we’ll need a
Bar type and a
BarTween to animate it. Let’s extract the bar-related classes into their own
bar.dart file next to
I’m following a Flutter SDK convention here in defining
BarTween.lerp in terms of a static method on the
Bar class. This works well for simple types like
Rect and many others, but we’ll need to reconsider the approach for more involved chart types. There is no
double.lerp in the Dart SDK, so we’re using the function
lerpDouble from the
dart:ui package to the same effect.
Our app can now be re-expressed in terms of bars as shown in the code below; I’ve taken the opportunity to dispense of the
The new version is longer, and the extra code should carry its weight. It will, as we tackle increased chart complexity in part two. Our requirements speak of colored bars, multiple bars, partial data, stacked bars, grouped bars, stacked and grouped bars, … all of it animated. Stay tuned.