This chapter covers
I imagine, because you’re reading this, that you’re at least intrigued by Flutter. By the end of this chapter, I hope you’ll be excited about it. In this chapter, I’ll walk you through the ins and outs of Flutter. I’ll show you how to use it and how it works under the hood. The goal of this chapter is to build a foundation. This is the plan for doing so:
If Flutter isn’t installed on your machine yet, you can find installation instructions in the appendix. If you have trouble setting it up, look for additional help in the docs at https://flutter.dev/get-started.
Getting started with Flutter (after you have it installed on your machine) is as easy as running a command in your terminal. Anytime you start a new Flutter project, you’ll do so by running flutter create in your terminal. This generates the starting code for your project.
If you didn’t notice the note in the previous section, please make sure your environment is set up before proceeding! You can find instructions for downloading Flutter and all its dependencies in appendix A of this book or at https://flutter.dev/get-started.
Let’s fire up that first Flutter app. Navigate in your terminal to the location you want this app to live:
$ cd ~/Desktop/flutter_in_action/ $ flutter create counter_app $ cd counter_app && flutter pub get 1 $ flutter run
Now run your app. Figure 3.1 shows what you should see in your simulator.
You can press that button, and the counter will increase. It’s a hoot. The app doesn’t do much else, but it’s worth noting how easy it is to get started with Flutter!
This is called the “counter app” around the internet. It’s the “hello world” equivalent in Flutter. Anytime you start a new project, you’ll have this as your starting point.
A Flutter project, when first created, is a big directory. The good news is that most of it doesn’t matter to us right now. In fact, much of it won’t ever matter to you. This is what your directory should look like:
counter_app |- android 1 |- ios 2 |- lib 3 |- main.dart 4 |- test 5 |- widget_test.dart .gitignore pubspec.yaml 6 pubspec.lock 7 README.md
At this point, the main takeaway is that lib is where you’ll be adding code for a Flutter project, and main.dart is the app’s entry point.
The majority of the counter app lives inside the main.dart file. The generated starting code in main.dart is beautifully commented by the Flutter team, and you can get a ton of information out of those comments alone. But I want to break down the most important parts and tease out those comments. Starting from the top of the file:
import 'package:flutter/material.dart'; 1
A large portion of Flutter that we interact with is just a Dart library. This library includes everything you need to write a Flutter app, including all the widgets included with the SDK by default. In this case, the app is importing the material library, which includes all the base widgets, plus the ones that follow Google’s Material Design system. There’s also flutter/cupertino.dart, which provides iOS-styled components. We’ll use Material in this book.
The Material and Cupertino libraries come with the same core features and differ only in the widgets that are included by default. The Material library is more robust at this point, and most examples, documentation, and tutorials on the internet use it. I’ll use it only for that reason. Neither Material nor Cupertino is better or worse.
At the top of the counter app, you’ll see a main function:
void main() => runApp(MyApp());
Like all Dart programs, a Flutter app uses the main function as the entry point. In Flutter, you wrap your top-level widget in a method called runApp. At the least, your app will contain a line like this one. In a more robust app, you might do more in your main function, but you must call runApp with your top-level widget passed as an argument.
Remember, everything is a widget. That includes the root of your application! There is no special object or class for this.
In Flutter, (nearly) everything is a widget, and widgets are just Dart classes that know how to describe their view. They’re blueprints that Flutter will use to paint elements on the screen. The widget class is the only view model that Flutter knows about. There aren’t separate controllers or views.
As I’ve stated before and will go into in more depth soon, there are other object types in Flutter. But widgets are the model class that tell those other objects what to do. As developers, all we care about is writing models that Flutter knows how to turn into a UI. Widgets are declarative in nature, which is nice. We don’t have to worry about actually rendering the screen. We don’t (often) care about individual pixels. Widgets abstract those pain points away for us. This is one reason that we say “everything is a widget”: because everything we care about is a widget.
In most other frameworks, especially on the web, widgets are called components, and the mental model is similar. A widget (or component) is a class that defines a specific piece of your UI. To build an app, you make a ton of widgets (or components) and put them together in different ways to gradually compose larger widgets.
A difference, though, between components and other frameworks (like ReactJS) and widgets is that a widget can define any aspect of an application’s view. Some widgets, such as Row, define aspects of layout. Some are less abstract and define structural elements, like Button and TextField. The theme that defines colors and fonts in your app is a widget. Animations are defined by widgets. In a component-based framework from the web, you can build a component that has a singular job of adding padding to a child widget, but you don’t have to. You could use CSS to add padding to whichever component you want. In Flutter you can only style widgets with other widgets. To add padding, you use a Padding widget.
The point is that every piece of your UI is a widget. Even the root of your app is a widget. There isn’t a special object called App. You define your own widget, such as MyApp, which returns yet another widget in its build method.
The Flutter library is full to the brim with built-in widgets. When you start creating your own widgets, you’ll do so by composing these built-in widgets together. These are some of the most common widgets:
Every widget that you create must have a build method, and that method must return another widget. In most cases, it can be any widget. Here’s a bare minimum StatelessWidget:
Back in the app, take a look at this top-level widget, MyApp. MyApp in this counter app example is your top-level widget, but it’s not special—it’s a widget like anything else:
class MyApp extends StatelessWidget { 1 @override 2 Widget build(BuildContext context) { 3 return MaterialApp( 4 title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), 5 ); } }
In Flutter, you’ll create many instances of the same widgets. Many built-in widgets have both regular constructors and const constructors. Immutable instances of widgets are more performant, so you should always use const when you can. Flutter makes this easy by letting you omit the new and const keywords altogether. The framework will infer which one to use and always use const when it can:
Widget build(BuildContext context) { return Button( 1 child: Text("Submit"), ); } // compared to Widget build(BuildContext context) { return new Button( 2 child: new Text("Submit"), ); }
In practice, this means you don’t have to consider which widgets can be constant and which can’t. Flutter will take care of that for you. A bonus benefit is that your build methods look cleaner. Also, you can leave off the new keyword anywhere in your app anytime you create an instance of any class. It doesn’t have to be a widget. As of Dart 2.3, this feature can be used in any Dart environment, not just in Flutter.
Hot reload is one of Flutter’s greatest selling points to native mobile developers. If this section doesn’t excite you, I don’t know how to help you.
A fun fact about Dart is that is has both an ahead-of-time (AOT) compiler and a just-in-time (JIT) compiler. In Flutter, when you’re developing on your machine, it uses JIT. It’s called “just in time” because it compiles and runs code as it needs to. When you deploy the app in production, Flutter uses the AOT compiler. For us developers, that means you can develop and recompile code quickly in development, but you don’t sacrifice non-native performance in production.
Let’s test out how hot the hot reload really is. In the counter app, on line ~15, change the text passed in to the MyHomePage title argument:
// chapter_3/counter_app/lib/main.dart -- line ~15 home: MyHomePage(title: 'Flutter Home PageDemo'); // old home: MyHomePage(title: 'Hot Reload Demo'); // updated
Depending on your environment, you can trigger a hot reload a number of ways:
Fire that hot reload. You should see the change happen instantly. And that’s just a tiny example. You could have added new widgets and changed the theme color, and it would have reloaded just as quickly. Let’s check out one more example.
On line ~12, in the ThemeData constructor, update the primarySwatch argument to a different color:
// chapter_3/counter_app/lib/main.dart -- line ~12 theme: ThemeData( primarySwatch: Colors.blue, // old ), theme: ThemeData( primarySwatch: Colors.indigo, // updated ),
Hit that hot reload again. If everything went okay, your top app bar and the button should have changed colors in subsecond time. Pretty amazing stuff.
In the Flutter library, there are a ton of built-in widgets. Almost all of them are made from two different widget types: StatelessWidget and StatefulWidget. There are a couple more high-level widget types, which we’ll see throughout the book. But 95+% of the time, you’ll be using these two.
The general goal when developing a UI with Flutter is to compose a ton of widgets together to build the widget tree. A Flutter app is represented by a widget tree, similar to how the DOM on the browser is a tree structure. The widget tree is an actual tree data structure in code built behind the scenes by Flutter, but it’s also a useful way to talk about the structure of your Flutter app. While learning Flutter, whether throughout this book or elsewhere, you’ll encounter the widget tree quite often.
In short, the tree is a collection of nodes, where each node is a widget. Every time you add a widget in a build method you’re adding a new node the tree. The nodes are connected by their parent-child relationship.
Figure 3.2 shows a simplified visual representation of the widget tree for the counter app. In reality, there are a few more widgets in there, but don’t worry about specifics right now. And don’t get bogged down in the different widgets in that tree. For now, you only need to know that the widget tree is how Flutter apps are structured.
The process of composing widgets together into this tree is done by telling widgets that their child(ren) are more widgets. A simple example is styling some text:
return Container( child: Padding( 1 padding: EdgeInsets.all(8.0), child: Text("Padded Text") 2 ), );
In the widget tree, Container is the parent of Padding, which is the parent of the Text widget.
Not every widget has a child property, though. Other common properties in Flutter that allow you to pass widgets into widgets are children and builder, both of which we’ll see later (and often) in this book.
The difference between StatefulWidget and a StatelessWidget is right in the name. A StatefulWidget tracks its own internal state. A StatelessWidget doesn’t have any internal state that changes during the lifetime of the widget. It doesn’t care about its configuration or what data it’s displaying. It could be passed configuration from its parent, or the configuration could be defined within the widget, but it cannot change its own configuration. A stateless widget is immutable.
When it comes to learning about widgets, you’ll see the word configuration often. It’s kind of vague, but basically it encapsulates everything within your widget: the variables passed in and its size constraints, as well as meta information used by Flutter internally.
Imagine you’ve created a custom button widget in your app. Perhaps it will always say Submit, as shown in the next listing.
class SubmitButton extends StatelessWidget { Widget build(context) { return Button( child: Text('Submit'), ); } }
This is fine, but perhaps you want the button to say Submit in some cases and Update in others. In order to make the button class more usable, you can tell Flutter to render the button based on its configuration and data, as the following listing shows.
class SubmitButton extends StatelessWidget { final String buttonText; 1 SubmitButton(this.buttonText); 2 Widget build(context) { return Button( child: Text(buttonText); 3 ); } }
Either way, this widget is static and void of logic because it can’t update itself. It doesn’t care what the button says. Its configuration relies on parent widgets. It doesn’t know how to ask to be rebuilt, unlike a stateful widget (which we’ll see soon).
When I say “void of logic,” I don’t mean a stateful widget can’t have methods and properties like any other class. It can. You can have methods for your stateless widget, but a stateless widget is destroyed entirely when Flutter removes it from the widget tree. We’ll talk more about the widget tree and context later in this chapter, but it’s important to understand that a stateless widget shouldn’t be responsible for any data you don’t want to lose. Once it’s gone, it’s gone.
A stateful widget has internal state and can manage that state. All stateful widgets have corresponding state objects. Figure 3.3 shows the simplified widget tree again.
Notice the MyHomePage tree node is connected to the MyHomePageState tree node. I designed this to visually represent that all StatefulWidget instances actually have two classes involved. This is the anatomy of every stateful widget in code:
class MyHomePage extends StatefulWidget { 1 @override 2 _MyHomePageState createState() => _MyHomePageState(); 3 } class _MyHomePageState extends State<MyHomePage> { 4 @override Widget build(BuildContext context) { 5 // .. } }
In the previous example, notice that the class name is _MyHomePageState. It begins with an underscore, which is used to mark the class as private. All statements can be private. A top-level value that’s private, such as this class, is only available within the current file. If a class member, such as a variable or function, is marked private, it’s only available to use within that class itself.
Consider this Cat class:
class Cat { String name; String _color; void meow() => print("meow"); void _pur() => print("prrrr"); }
Then, consider interacting with it:
Cat nora = Cat(); nora.name = "Nora"; // Okay nora._color = "Orange"; // Invalid! nora.meow(); // Okay nora._pur(); // Invalid!
Private variables and class members are used quite a bit in Dart programming, solely to make your class APIs more readable.
If you remember, earlier I said that every widget class must have a build method. As you can see, the StatefulWidget class doesn’t have a build method. But every stateful widget has an associated state object, which does have a build method. You can think of the pair of StatefulWidget and State as the same entity. In fact, stateful widgets are immutable (just like stateless widgets), but their associated state objects are smart, mutable, and can hold onto state even as Flutter re-renders the widgets.
MyHomePage is a stateful widget because it manages the state of the counter in the center of the app. When you tap that button, it fires a method called _incrementCounter:
void _incrementCounter() { setState(() { 1 _counter++; }); }
setState is the third important Flutter method you have to know, after build and createState. It exists only for the state object. It more or less says, “Hey Flutter, execute the code in this callback (in this case, increase the counter variable by one), and then repaint all the widgets that rely on this state for configuration (in this case, the number on the screen in the middle of the app)” (see figure 3.4). This method takes one argument: a VoidCallback.
In this example, the state object is the _MyHomePageState widget, and its children interact and rely on the state. When you press the button, it calls a method passed to it from _MyHomePageState. That method calls setState, which in turn calls the _MyHomePageState.build method again, repainting the widgets whose configurations have changed (see figure 3.5).
There isn’t much more to setState than that, but it’s worth noting that setState can’t execute async code. Any async work should be done before calling setState, because you don’t want Flutter to repaint something before the data it’s meant to display has resolved. For example, if you’re fetching a GIF from a GIF API on the internet, you don’t want to call setState before the image is ready to be displayed.
The state object also has a method called initState, which is called as soon as the widget is mounted in the tree. State.initState is the method in which you initialize any data needed before Flutter tries to paint it the screen. For example, you could subscribe to streams or compute some data into a human-friendly format.
When Flutter builds a stateful widget and its state object, the first thing it’s going to do is whatever logic is in the initState function. For example, you may want to tell ensure that a String is formatted properly before the widget’s build method is called and anything is rendered:
class FirstNameTextState extends State<FirstNameText> { String name; FirstNameTextState(this.name); @override initState() { super.initState(); 1 name = name.toUpperCase(); } Widget build(BuildContext context) { return Text(name); } }
There are a few other lifecycle methods on the state object, and in a later chapter I’ll discuss a widget’s lifecycle in depth, including more on initState. Figure 3.6 shows all the methods and the order in which they’re called.
There is a lot in this figure, and you shouldn’t get bogged down in it. I’ll spend a ton of time on it later! For now, though, it’s important to know that initState and setState exist, and when to use them. initState is called once every time a state object is built. setState is called by you, the developer, whenever you want Flutter to re-render.
BuildContext is another concept in Flutter that’s crucial to building apps, and it has everything to do with tracking the entire widget tree—specifically, where widgets are located in the tree. When you update the theme in your ThemeData, as we did to change the color of the counter app, it updates child widgets throughout the widget tree. How does this work? It’s tied to the idea of BuildContext.
Every build method in a widget takes one argument, BuildContext, which is a reference to a widget’s location in the widget tree. Remember, build is called by the framework itself, so you don’t have to manage the build context yourself, but you will want to interact with it often.
A concrete example is the Theme.of method, a static method on the Theme class. When called, Theme.of takes a BuildContext as an argument and returns information about the theme at that place in the widget tree. This is why, in the counter app, we can call Theme.of(buildContext).primaryColor to color widgets. That gets the Theme information for this point in the tree and then returns the data saved at the variable primaryColor in the Theme class.
Every widget has its own build context, which means that if you had multiple themes dispersed throughout your tree, getting the theme of one widget could return different results than another. In the specific case of the theme in the counter app, or other of methods, you’ll get the nearest parent in the tree of that type (in this case, Theme; see figure 3.7).
The build context is used in various ways to tell Flutter exactly where and how to render certain widgets. For example, Flutter uses the build context to display modals and routes. If you wanted to display a new modal, Flutter needs to know where in the tree that modal should be inserted. This is accomplished by passing in BuildContext to a method that creates modals. We’ll see this in depth in the chapter on routing. The important point about the build context, for now, is that it contains information about a widget’s place in the widget tree, not about the widget itself.
Widgets, state, and context are arguably the three cornerstones of the foundation for developing a basic app in Flutter. Let’s put them in action now.
The default counter app isn’t useful right now. You can’t even reset your count. In this section, we’ll extend the functionality of the counter app and explore some of the most important widgets in Flutter. According to the documentation, the absolute basic widgets are the following:
Of these widgets, Column, Text, Icon, Scaffold, and AppBar are already in the counter app. We’ll add the rest to make the counter app a bit more fun. Your improved counter app will look like figure 3.8 in the end.
First, let’s add a button to decrease the counter. All of this functionality will live in the _MyHomePageState class. To decrease the counter, we need
RaisedButton is one of the Material Design-based buttons, and it appears slightly elevated. Raised buttons are used to add dimension to your layout, as opposed to a FlatButton. To add the button, let’s start in the build method of _MyHomePageState in the next listing.
// _MyHomePageState Widget build(BuildContext context) { return Scaffold( // ... body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ // ... RaisedButton( 1 child: Text("Decrement Counter"), onPressed: _decrementCounter, 2 ), ])), // ...
To finish up that functionality, we need to write the _decrementCounter method:
void _decrementCounter() { setState(() => _counter--); 1 }
Interaction is largely handled by callbacks in Flutter, like onPressed. In widgets provided by Flutter and that you’ll write, you’ll use callbacks to execute some function when a user interacts with your app. In built-in widgets, you’ll see onPressed, onTapped, onHorizontalDrag, and many more. Chapter 5 is devoted to user interaction, where I’ll cover these further.
Designing object-oriented software is hard, and designing reusable object-oriented software is even harder.
This is the opening line of Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma et al., published in 1994. In all object-oriented programming, one of the hardest design issues is establishing relationships between your classes. There are two ways to create relationships between classes. The first, inheritance, establishes an “is a” relationship. Composition establishes a “has a” relationship. For example, a Cowboy is a Human, and a Cowboy has a Voice. Inheritance tends to have you designing objects around what they are, and composition around what they do.
What if you wanted to make a game where you’re a cowboy and you have to protect the wild west from aliens? Maybe you have classes like this:
Human .rideHorse() Cowboy .chaseOutlaws() .fightAliens() Rancher .herdCattle() Alien .flySpaceship() .invadeEarth()
Great. You got to reuse the rideHorse method because cowboys and ranchers both, in fact, ride horses. You’re well into making this killer game when you have the wild idea that the aliens learn the ancient art of horse riding. Well, that’s a problem. An alien isn’t a human, so you shouldn’t have Alien inherit from Human. But you also don’t want to rewrite rideHorse.
This could have been avoided by using composition from the beginning, rather than inheritance. You could have a HorseRiding class, which could be added as a member to any class. It’d look more like this:
HorseRiding .rideHorse Cowboy HorseRidingInstance.rideHorse() .chaseOutlaws() .fightAliens() Rancher HorseRidingInstance.rideHorse() .herdCattle() Alien HorseRidingInstance.rideHorse() .flySpaceship() .invadeEarth()
This is great. No matter how many objects need to ride a horse, you have an easy, decoupled way to add that functionality.
The curious among you might be asking, “Why not just make all the actions into their own classes and inherit everything?” Well, that’s not a bad idea. Maybe the rancher learned how to fly a spaceship. So now how do we think about our objects that have these methods?
Well, a Cowboy is a HorseRider and AlienFighter and OutlawChaser. Similarly, the alien and rancher are combinations of what they can do:
Alien = HorseRider + SpaceShipFlyer + EarthInvader Rancher = HorseRider + CattleHerder Cowboy = HorseRider + OutlawChaser + AlienFighter
If you made classes that represent HorseRider, EarthInvader, and the like, then you could implement those actions into your classes. This is what composition is.
(If you’re thinking, “That sounds a lot like the idea behind abstract classes,” you’re correct. We’ll explore those deeply in the part 3 of this book.)
The example in listing 3.3 with a RaisedButton uses composition:
//... RaisedButton( child: Text("Decrement Counter"), onPressed: () => _decrementCounter(), ), //...
To make a button that says Decrement Counter, you pass in another widget (Text) that handles the responsibility of setting text.
In Flutter, always favor composition (over inheritance) to create reusable and decoupled widgets. Most widgets don’t know their children ahead of time. This is especially true for widgets like text blocks and dialogs, which are basically containers for content.
A more robust example of a button may look like this:
class PanicButton extends StatelessWidget { final Widget display; final VoidCallback onPressed; PanicButton({this.display, this.onPressed}); 1 Widget build(BuildContext context) { RaisedButton( color: Colors.red, 2 child: display, 3 onPressed: onPressed, 4 ); } }
Here, using composition, I’m saying “This button has text,” rather than “This text is a button.” What if you want the button to display an icon instead of text? It’s already set up to do that. All you need to do is pass in an Icon instead of Text. The button doesn’t care about its child, it only knows that it has one.
You could kick that up a notch and pass in the color if you wanted to. The button doesn’t even care about that, only that it will be told what color it is.
Anyway, back in your app, you should now have a button that you can use to decrement the counter by one. Next, we’ll keep adding more to the app.
The most common questions from those working in Flutter for the first time are about layout. Flutter’s rendering engine is unique in that it doesn’t use one specific layout system. Way down on the low level, it doesn’t consider the screen a Cartesian graph (at first). It doesn’t force the developer to use flex layout, a Cartesian graph, or other common systems like width in, height out (WIHO). It leaves that up to us. And often, we mix and match those systems to achieve the layout we want.
Widgets, as we now know, are high-level classes that describe a view. There are lower-level objects that know how to paint these widgets onto the screen. In practice, that means the layout system is abstracted away for the developer, which opens up the possibility of using several different paradigms together. There are widgets that use the flexible layout, commonly known as FlexBox on the web. And there are widgets that allow us to explicitly place widget on the screen at given coordinates. In this section, I want to explore some of the most common layout widgets.
Besides layout widgets, I’ll also talk about constraints in Flutter. Constraints are a core part of understanding layout. In a nutshell, though, constraints tell widgets how much space they can take up, and then the widgets decide what they will take up. In section 3.6.2, I talk about constraints in depth.
The most commonly used layout style in Flutter is known as the flexible layout, just like FlexBox. You can use flex layouts with Column and Row widgets. The counter app already has a Column widget in it, as shown in the next listing.
// _MyHomePageState body: Center( child: Column( 1 mainAxisAlignment: MainAxisAlignment.center, 2 children: <Widget>[ 3 Text('You have pushed the button this many times:'), Text( '$_counter', style: Theme.of(context).textTheme.display1, ), RaisedButton( child: Text("Decrement Counter"), onPressed: _decrementCounter, ), ], ), ),
The Row widget behaves like the Column but on a horizontal axis. It will take all its children and lay them out, one by one, next to each other, from left to the right.
In some languages (as in speaking languages, not programming languages), words are written right-to-left. In this case, Flutter supports RTL settings and would change the behavior of the Row widget. This is outside the scope of this chapter. If you aren’t developing an app that will be localized to one of these RTL languages, then this shouldn’t concern you.
I want to wrap the decrement button in Row in the example app so I can add a second button beside it. In that same code block, start by adding Row around RaisedButton.
// _MyHomePageState.build Row( // new children: <Widget>[ // new RaisedButton( color: Colors.red, child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, ), ], // new ), // new
When you hot-reload your app, the Decrement button is now aligned to the left side of the screen (see figure 3.9). This is because flexible widgets try to take up as much space as they can on their main axis. The Row widget expands as much as it can horizontally, which in this case is as wide as the whole screen, constrained by its parent (the column).
Layout and constraints are monumentally important in Flutter. Flutter is, after all, mainly a UI library and a rendering engine. Understanding how widgets determine their sizes will save you headaches in the future. You will certainly, at some point, get some errors when you’re using Row and Column and other layout widgets. These are layout-constraint errors. When developers are learning Flutter for the first time, they’ll certainly see a flutter layout infinite size error.
This is an error that can be a headache to correct, unless you know how constraints work. I need to take a conceptual aside to discuss how Flutter knows what pixels to paint on the screen, thanks to constraints.
I’ve said many times that there are a couple of objects in Flutter other than widgets. One of the most important to understand is RenderObject. This class is mainly used internally. You’ll rarely have to use it directly.
Render objects are responsible for the actual painting to the screen done by Flutter. They are made internally by the framework, and all the render objects make up the render tree, which is separate from the widget tree. The render tree is made up of classes that implement RenderObject. And render objects have corresponding widgets.
As developers, we write widgets, which provide data (such as constraints) to a render object. The render object has methods on it like performLayout and paint. These methods are responsible for painting the pixels on the screen. They’re concerned with exact bits of information for controlling pixels. All styling and layout work done in widgets is largely an abstraction over the render objects.
These render objects are also without any state or logic. By design, they know some basic data about their parent render object, and they have the ability to visit their children, but they don’t coordinate with each other on the scale of the whole app. They don’t have the ability to make decisions—they only follow orders.
Importantly, widgets build child widgets in their build method, which create more widgets, and so on down the tree until it bottoms out at a RenderObjectWidget (or a collection of RenderObjectWidgets). These are the widgets that create render objects that paint to the screen.
Consider a Column widget, which would not be a leaf RenderObjectWidget in a widget tree. A column is an abstract layout idea; it isn’t an actual thing you can see. Text and colors are concrete objects that can be painted. The job of a column is to provide constraints, not to paint anything on the screen.
RenderObjects aren’t of much concern to us as developers, but they’re an important piece of the relationship between your widgets and how Flutter actually works. The render object API is exposed to us, but it’s unlikely you’ll need to use it.
Render objects are closely tied to layout constraints. While you can set your own constraints on widgets using constraint widgets, render objects are ultimately responsible for telling the framework a widget’s true, physical size. Constraints are passed to a render object, and that object eventually decides, “Okay, given these constraints, I will be this size and in this exact location.”
In other words, constraints are concerned with minWidth, minHeight, maxWidth, and maxHeight. Size, on the other hand, is concerned with actual width and height. When a render box is given its constraints, it then decides how much of that allotted space it will actually take up (its size).
Different render objects behave differently. The most common render object subclass, by far, is RenderBox, which calculates a widget’s size using a Cartesian coordinate system. In general, there are three kinds of render boxes:
Thus far in the discussion about render objects, those three styles are the most important thing to remember. At this point in the book, I won’t discuss actually writing render objects; I’m only giving a foundational explanation. It’s important to remember, though, that different widget’s RenderObjects behave in one of those three ways.
Back to the original layout problem: the flutter layout infinite size error. This error happens when a widget’s constraints tell it that it can be infinitely large on either the horizontal or vertical access. This has everything to do with the constraints that are passed to it, and the way its render object behaves.
Sometimes the constraints that are given to a box are unbounded. This happens when either the maxHeight or maxWidth given to a render box is double.INFINITY. Unbounded constraints are found in Row, Column, and widgets that are scrollable. That makes sense, because a row can be—in theory—infinitely wide (depending on its children). But the render engine can’t actually paint an infinitely wide widget, because we’re human beings constrained by time and the computer is constrained by processing power and memory.
Row and Column are special because they’re flex boxes. Their render objects don’t, by default, fit into one of those three categories of render object behavior that I mentioned earlier. They behave differently based on the constraints passed by their parents. If they have bounded constraints, they try to be as big as possible within those bounded constraints. If they have unbounded constraints, they try to fit their children in the direction of their main axis. For example, a column full of images that has unbounded constraints will try to be as tall as the combined height of all the images.
The constraint passed to the column’s children is determined by the constraints passed to the column. If you don’t know to look for it, this can lead to a pesky error. Let me try to make this more concrete with an example. Here’s how Columns within Columns can cause infinite height:
child: Column( 1 children: <Widget>[ Column( 2 mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Expanded( 3 child: Text( 'You have pushed the button this many times:', ), ), ], ), ], ),
In this case, the inner column is going to try to be whatever size its child tries to be, and is unbounded by its own parent. The Expanded will say, “Great! I have no height constraint, and it’s in my nature to try and be as big as possible, so I’m going to expand forever.” That’ll throw an error.
Also, it’s worth noting that flexible widgets, as well as some scrolling widgets, always try to take up as much space as possible on their cross axis. A column will always try to be as wide as its parent, and a row will always try to be as tall as its parent.
Because widgets pass constraints down the tree, there can be some degrees of separation between nested flex boxes, and you’ll end up with an infinitely expanding child somewhere. This often leads to that pesky error mentioned before. This is often solved by ensuring that you aren’t using a widget that tries to be as big as possible in nested flexible widgets.
It’s quite common to have nested flexible widgets, such as rows of widgets within a column. There isn’t one go-to fix for this problem because the constraints vary depending on what widgets you’re using. In general, though, if you know how flexible widgets behave, this problem is easier to tackle.
Now that you have a bit of information about constraints and dealing with flexible widgets, let’s put it into practice in the following listing. Back in the app, let’s add this second button to the row, which will increment the counter (see figure 3.10).
Row( children: <Widget>[ RaisedButton( color: Colors.red, child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, ), RaisedButton( 1 color: Colors.green, child: Text( "Increment", style: TextStyle(color: Colors.white), ), onPressed: _incrementCounter, ), ], ),
Now there are two buttons, both aligned to the left side. To make that a little more pleasing to look at, we need to add an alignment to the Row. Flexible widgets can be told how to space their children with a few different alignment options that can be passed to the mainAxisAlignment property:
Row( mainAxisAlignment: MainAxisAlignment.spaceAround, 1 children: <Widget>[ RaisedButton( color: Colors.red, // ...
If you come from the web, spaceAround may look familiar (see figure 3.11). The Axis Alignment options are the same as the FlexBox and CSS Grid properties for justification and alignment. Figure 3.12 shows all the flexible layout alignments from a real-life example.
That’s the basics of using multi-child Row and Column widgets. You’ll find yourself using a lot of them, so it’s worth understanding the basics.
In Flutter, all the Material Design icons are built in and available as constants. Compared to my experience on the web, this is a real blessing. Icons are a core part of building mobile interfaces where space is limited. In Flutter, you don’t have to find an external library or upload images if you’re happy using the Material Design style icons. There are several icons, and they can all be seen and searched through at https://material.io/tools/icons. Table 3.1 lists the icons I use most often.
Const name |
|
---|---|
Icons.add | |
Icons.arrow_drop_down | |
Icons.arrow_forward | |
Icons.chevron_left | |
Icons.close | |
Icons.favorite | |
Icons.check | |
Icons.arrow_drop_up | |
Icons.arrow_back | |
Icons.chevron_right | |
Icons.menu | |
Icons.refresh |
In the counter app right now, an Icon is used in the FloatingActionButton: the Add icon. Since Icons is a constant, you can access Icons anywhere in your app (passed in to the Icon widget). The Icon widget is, as you probably guessed, a widget. You can use it anywhere.
The FloatingActionButton button is a prime example of a widget that Flutter gives you, styled and all, for free. According to the FloatingActionButton documentation (http://mng.bz/079E), “A floating action button is a circular icon button that hovers over content to promote a primary action in the application. Floating action buttons are most commonly used in the Scaffold.floatingActionButton field.” When used in a Scaffold (as it is in our app), it’s placed where it needs to be, with no work necessary on your part. You can use it anywhere you’d like, though, if you want a circular button that has styles that make it look “elevated” with a box shadow.
Back in the app, we want the FloatingActionButton (FAB) to reset the counter, not increase it. Easy enough! What are the steps to get this done?
First, let’s write the method. All we want to do is set _counter back to 0. Also, don’t forget that we need to tell Flutter to repaint:
void _resetCounter() { setState(() => _counter = 0); }
That’s a simple enough method. Now we have to update the FAB itself.
Step 1 is choosing the right Icon. I choose Icons.refresh, which will be passed into the FAB. In the FAB, change the icon and the function passed into the onPressed callback:
floatingActionButton: FloatingActionButton( onPressed: _resetCounter, 1 tooltip: 'Reset Counter', 2 child: Icon(Icons.refresh), 3 ),
When you hot-reload the app, it should now reflect the changes with the different icon and functionality.
Flutter makes it easy to add images to your app via the Image widget. The Image widget has different constructors, depending on the source of your image: that is, whether the image is saved locally in the project or you’re fetching it from the internet.
The quickest way to add an image is the with Image.network constructor. You pass it a URL as a String, and it takes care of everything for you. An example is Image.network("https://funfreegifs.com/panda-bear"). Any URL that resolves to an image can be passed.
In your app, though, it’s more likely that you’ll need some images hosted locally. In this case, you use the Image.asset constructor. This constructor works the same way: you pass in a path to an image in your project, and it resolves it for you. However, you have to tell Flutter about it in your pubspec.yaml file first.
In this counter app, let’s put a Flutter logo at the top of the app. There’s already a Flutter logo image in the GitHub repository for book. The image also needs to be added to the pubspec.yaml file. I’ll take this opportunity to briefly walk through the pubspec.yaml of a basic Flutter app. (If you start a new Flutter project, the pubspec.yaml file has in-depth comments, which you may find helpful.)
name: counter_app 1 description: A new Flutter project. 1 version: 1.0.0+1 1 environment: 2 sdk: ">=2.0.0-dev.68.0 <3.0.0" 2 dependencies: 3 flutter: sdk: flutter cupertino_icons: ^0.1.2 4 dev_dependencies: 5 flutter_test: sdk: flutter flutter: 6 uses-material-design: true 7 assets: 8 - flutter_logo_1080.png
Any assets that you need in your app must be listed under the assets header in your spec file. It must follow this format. YAML is sensitive to whitespace. The assets themselves should be listed as a path from the lib folder in your project. I just happened to put mine directly under lib, but if it was in a folder called images, that line would say images/flutter_logo_1080.png.
Now that you’ve added flutter_logo_1080.png to your assets, you’re going to have to restart your app. Hot reload doesn’t work if you change your spec files. But hot restart does work, and it should be much faster than stopping and starting your app. You can perform a hot restart by typing R into the terminal in which you ran flutter run.
Now you can access that image in the counter app by adding an Image widget to the Column widget’s children, as shown in the next listing.
children: <Widget>[ Image.asset( 'flutter_logo_1080.png', 1 width: 100.0, 2 ), Text( 'You have pushed the button this many times:', ),
Now, if you hot reload, you’ll see an image in your app.
The image doesn’t look great right now. It’s just sitting there on top of the text, without any spacing. Let’s clean it up with the Container widget. Figure 3.13 shows what we’re going for, and it can all be done with the Container.
Some widgets, such as Container, vary in the way they size themselves based on their constructor arguments and children. In the case of Container, it defaults to trying to be as big as possible; but if you give it a width, for instance, it tries to honor that and be that particular size.
The Container widget is a “convenience” widget that provides a slew of properties that you would otherwise get from individual widgets. For example, there is a Padding widget that solely adds padding to its child. But the Container widget has a padding property (among others).
You will likely get a lot of use out of the Container widget. Look at all these optional properties you can take advantage of (and these aren’t all) from the constructor of the Container widget:
// From Flutter source code Container({ Key key, this.alignment, this.padding, Color color, Decoration decoration, 1 this.foregroundDecoration, double width, double height, BoxConstraints constraints, this.margin, this.transform, this.child, }) ...
We’ll explore all of these in time, but the point is that if you need to style a widget in some way, you should reach for a Container. Wrap your Image.asset in a Container, and then add the following properties to it.
Container( margin: EdgeInsets.only(bottom: 100.0), 1 padding: EdgeInsets.all(8.0), 2 decoration: BoxDecoration( 3 color: Colors.blue.withOpacity(0.25), 4 borderRadius: BorderRadius.circular(4.0), 5 ), child: Image.asset( 6 'flutter_logo_1080.png', width: 100.0, ), ),
Hot-reload one more time. Your app should look like the original goal from figure 3.14. And, more importantly, you’ve learned about all of the foundational concepts of wrangling UI in Flutter.
Now that you’ve seen a handful of widgets, I’d like to take one last opportunity to explore Flutter under the surface. If you’ve tried messing with Flutter before this book, you’ve likely seen the graphic floating around that discusses the “layers” of the framework. It looks like figure 3.15.
About 99.99% of the time, we developers get to live in the top layers of that table: in the Widgets layer and Material/Cupertino layer. Below those layers are the Rendering layer and the dart:ui library. Dart UI is the most low-level piece of the framework written in Dart. This library exposes an API to talk directly with the rendering engine on device. Dart UI allows us to paint directly on the screen using the canvas API, and lets us listen for user interaction with hit testing.
For the rest of this section, I’m going to explain what Element objects are and how they are relevant to you, the developer. That said, you’ll rarely use elements directly. This section is meant to give you an understanding of how the framework operates. It is a tough concept to grok, but it doesn’t have any bearing on your ability to move through this book.
Understanding the inner workings of Flutter comes in handy in a few “gotcha” situations. This understanding may help you debug your apps in the future, but you shouldn’t get hung up on it. It’s good to be aware of now, but it’s not necessary for you to completely understand it. That will come with time.
The long and short of that library is that it’d be an extreme slog of a process to write an app with it, but you could. You’d have to calculate coordinates for every single pixel on the screen and update the screen with every single frame. That’s why Flutter has widgets, the high-level abstractions that give us a declarative approach to building a UI. We don’t have to worry about working with pixels or low-level device hit testing.
Looking at figure 3.15, this just leaves the layer in between the widgets and dart:ui: rendering. It turns out that there’s yet another tree in your Flutter app: the element tree. The element tree represents the structure of your app, in much the same way the widget tree does. In fact, there’s an element in the element tree for every widget in the widget tree.
Earlier in the book, I described widgets as blueprints that Flutter will use to paint elements on the screen. When I said elements in that sentence, I literally meant the Flutter Element class. Widgets are configurations for elements. Elements are widgets that have been made real and mounted into the tree. Elements are what are actually displayed on your device at any given moment when running a Flutter app.
Each Element also has a RenderObject. All of these render objects make up the render tree. Render objects are the interface between our high-level code and the low-level dart:ui library. With that in mind, you can think of the element as the glue between the widget and render trees (see figure 3.16).
You can use render objects directly as a Flutter developer, but I doubt you’ll ever want to. Render objects do the actual painting to the screen and are therefore quite complex and expensive. That’s why there are three trees in Flutter. They give the framework the ability to internally be smart about reusing expensive render objects while being careless about destroying inexpensive widgets.
With this in mind, I’d like to talk more about elements, but we shouldn’t worry about the render tree anymore. The relationship between elements and widgets, however, is worth investigating.
Elements are created by widgets. When a new widget is built, the framework calls Widget.createElement(this). This widget is the initial configuration for the element, and the element has a reference to the widget that built it. The elements that are created are their own tree, as well. The element tree is, in the simplest explanation, like the skeleton of your app. It holds the structure of the app, but none of the details that widgets provide. It can look up configuration details via those references to its corresponding widget.
Elements are different than widgets because they aren’t rebuilt; they’re updated. When a widget is rebuilt, or a different widget is inserted at some place in the tree by an ancestor, an element can change its reference to the widget, rather than being re-created. Elements can be created and destroyed, of course, and will be as a user navigates around the app. But consider an animation. The animation calls build after every frame change—which is a lot! (Up to 60 times per second, in fact.) During an animation, the widget at every frame is the same type but varies slightly in configuration. (That is, some display properties have changed. For example, a widget’s color might be a slightly different shade in each frame of the animation.) In this case, the element itself doesn’t have to rebuild, because the tree is still structurally the same. The widget gets rebuilt on every frame, but the element only updates its reference to that widget.
And this is how Flutter gets away with rebuilding widgets constantly, but remains performant. Widgets are just blueprints, and it’s cheap to replace widgets in the tree without disturbing the tree that’s actually displayed on the screen, because it’s handled by elements.
There’s one last important detail I’d like to touch on: state objects are actually managed by elements, not widgets. In fact, under the hood, Flutter renders based on elements and state objects and isn’t as concerned with widgets. In the next section, we’ll look deeper into how the state object interacts with elements and widgets.
This is all necessary (and optimal), because widgets are immutable. Because they’re immutable, they can’t change their relationships with other widgets. They can’t get a new parent. They have to be destroyed and rebuilt. Elements, however, are mutable, but we don’t have to update them ourselves. We get the speed of mutable elements, but the safety of writing immutable code (via widgets).
Again, elements, like render objects, are rarely of concern to the developer, but you can create your own. You’ll likely always stick to writing widgets.
To add a bit more flair to the app (and demonstrate how the element tree works), I want to swap the increment and decrement buttons each time the Reset button is pressed. To start, let me point out some relevant code in the counter app.
class _MyHomePageState extends State<MyHomePage> { int _counter = 0; bool _reversed = false; 1 List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()]; 2 // ... rest of class }
To make this easier, I made a widget called FancyButton, shown in the next listing. This is a stateful widget that manages its own background color, as well as calling a callback passed into it when the button is pressed.
class FancyButton extends StatefulWidget { final VoidCallback onPressed; final Widget child; const FancyButton({Key key, this.onPressed, this.child}) : super(key: key); @override _FancyButtonState createState() => _FancyButtonState(); } class _FancyButtonState extends State<FancyButton> { @override Widget build(BuildContext context) { return Container( child: RaisedButton( color: _getColors(), 1 child: widget.child, onPressed: widget.onPressed, ), ); } Color _getColors() { return _buttonColors.putIfAbsent(this, () => colors[next(0, 5)]); 2 } } Map<_FancyButtonState, Color> _buttonColors = {}; 3 final _random = Random(); 3 int next(int min, int max) => min + _random.nextInt(max - min); 3 List<Color> colors = [ 3 Colors.blue, 3 Colors.green, 3 Colors.orange, 3 Colors.purple, 3 Colors.amber, 3 Colors.lightBlue, 3 ]; 3
The getColors method manages color for all the fancy buttons by using the putIfAbsent method on Dart Map objects. This method says, “If this button is already in the map, tell me its color. Otherwise, put this in the map with this new color, and return that color.”
The FancyButton widget is used in the _MyHomePageState.build method (shown in the next listing). The buttons are first created as variables and will be used in the widget tree in the returned portion of this build method.
// _MyHomePageState.build @override Widget build(BuildContext context) { final incrementButton = FancyButton( 1 child: Text( 1 "Increment", 1 style: TextStyle(color: Colors.white), 1 ), 1 onPressed: _incrementCounter, 1 ); 1 final decrementButton = FancyButton( 2 child: Text( 2 "Decrement", 2 style: TextStyle(color: Colors.white), 2 ), 2 onPressed: _decrementCounter, 2 ); 2 List<Widget> _buttons = <Widget>[incrementButton, decrementButton]; 3 if (_reversed) { 4 _buttons = _buttons.reversed.toList(); 4 } }
For both fancy buttons, the configuration resembles the configuration you’d need for a RaisedButton in Flutter.
I’ll cover the rest of the build method (and in turn, using keys) next. First, I’d like you to press the Reset button and swap the buttons. The Reset button, when pressed, calls the method _resetCounter:
void _resetCounter() { setState(() => _counter = 0); _swap(); 1 } void _swap() { 2 setState(() { _reversed = !_reversed; }); }
You may notice that it isn’t behaving the way we wanted. If your code is the same as mine, then when you press the Reset button, the buttons do indeed swap places, but the button background colors don’t swap. That is, the button on the left has the same background color it did before the swap, even though the button itself is different. This is the result of elements, state objects, and widgets and how they all work together.
A few things to keep in mind as I explain what’s happening:
The relationship between a single stateful widget, an element, and a state object is shown in figure 3.17.
It’s helpful for me if I consider the element the brains of the operation. Elements are simple in that they only contain meta information and a reference to a widget, but they also know how to update their own reference to a different widget if the widget changes.
Anytime Flutter is rebuilding, the element’s reference points to the new widget in the exact location in the widget tree of the element’s old reference. When Flutter is deciding what to rebuild and re-render after build is called, an element is going to look at the widget in the exact same place as the previous widget it referenced (see figure 3.18). Then, it’ll decide if the widget is the same (in which case it doesn’t need to do anything), or if the widget has changed, or it’s a different widget altogether (in which case it needs to re-render).
So, when you swap those two buttons, they replace each other in the widget tree, but the element’s reference points to the same location. Each element is going to look at its widget and say, “Has this widget changed? Or is it a new widget altogether?” So, we’d expect the element to see that the widget’s color property has changed, so it should in fact update its reference to the new widget.
The problem is what elements look at to decipher what’s updated. They only look at a couple of properties on the widget:
In this example, the colors of these widgets aren’t in the widget configuration; they’re in the state objects. The element is pointing to the updated widgets and displaying the new configuration, but still holding on to the original state object. So, the element is seeing the new widget that’s been inserted into this place in the tree and thinking, “There’s no key, and the runtime type is still FancyButton, so I don’t need to update my reference. This is the correct widget to match my state object.” (See figure 3.19).
This issue presents another feature of Flutter: keys, which can be used by the framework to explicitly identify widgets.
If you aren’t convinced that you won’t have to deal with elements directly, consider this anecdote: in my two years of developing production apps for Flutter, I’ve only written an element once. The problem I just discussed isn’t fixed by diving into elements—it’s solved using a common feature in Flutter, keys, which I’ll discuss next.
I can’t stress enough that having a basic understanding of elements will arm you with a greater understanding of Flutter under the hood than almost everyone out there, but that’s all you need: the basics.
Continuing with the problem of State and Element, let me present the easiest solution to the problem: keys. When working with widgets in collections, giving them keys helps Flutter know when two widgets of the same type are actually different. This is particularly useful for the children of multi-child widgets. Often, as in our example case, all the children in a row or column are of the same type, so it’s ideal to give Flutter an extra piece of information to differentiate the children. In our app, let’s solve the problem with a UniqueKey:
_buttons = <Widget>[ FancyButton( key: _buttonKeys.first, 1 child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, ), FancyButton( key: _buttonKeys.last, 2 child: Text( "Increment", style: TextStyle(color: Colors.white), ), onPressed: _incrementCounter, ), ];
There is quite a bit more to explore with keys, and they are extremely useful. Next, I’ll talk about the different types.
There are several types of keys: ValueKey, ObjectKey, UniqueKey, GlobalKey, and PageStorageKey. They’re all related in a some way. PageStorageKey is a subclass of ValueKey<T>, which is a subclass of LocalKey. And that is a subclass of Key. ObjectKey and UniqueKey also implement LocalKey. GlobalKey is a subclass of Key. They’re all related, and they’re all a Key type.
Those relationships may have been hard to follow. Luckily, there’s no reason to memorize all of them—I’m just making a point, which is that the familial relationship of all the keys doesn’t matter. They’re all keys at the end of the day. The difference is that they’re all used for specific cases. All that said, you can start organizing keys by putting them all into two camps: global and local.
Using keys, especially global keys, generally is not necessary or recommended. Global keys can almost always be replaced with some sort of global state management. The exceptions to that rule are the issue we saw earlier, and using some specialized key like PageStorageKey.
Global keys are used to manage state and move widgets around the widget tree. For example, you could have a GlobalKey in a single widget that displays a checkbox, and use the widget on multiple pages. This key tells the framework to use the same instance of that widget. So, when you navigate to different pages to see that checkbox, its checked state will remain the same. If you check it on page A, it’ll be checked on page B.
It’s important to note that using global keys to manage state is not advised (by me or by the Flutter team). You’ll likely want a more robust way to manage state, which I’ll discuss throughout this book. Global keys aren’t used often, and they impact performance; using local keys is more common. Later in the book, I’ll show you how to use a global key when the time is right.
Local keys are all similar in that they’re scoped to the build context in which you created the key. Deciding which one to use comes down to the case:
Key key = ObjectKey({ "seller": product.seller, "product": product.title })
If you’re experimenting with Flutter for the first time, this chapter may have been a lot to take in. I just introduced many concepts that are practical, and that you will use day to day when writing Flutter apps, and concepts that are more conceptual, about how Flutter works.
I would encourage you not to get too bogged down in the details. From this point forward, the book is completely action-based. That is, you’ll learn by writing code. With that in mind, I don’t think it’s necessary that you understand 100% of how constraints work, or the element tree, or most of the other concepts. I wrote this chapter to expose you to concepts that will come up over and over again, which means you’ll understand them better the more you see them.
I think if you are comfortable with the basic syntax and anatomy of writing widgets, you’re in a good spot. You’ll see many more columns and flexible widgets. You’ll have plenty of chances to create UIs. This chapter represents the foundation, so that when we dive into other topics, like routing, we can focus specifically on routing. But you’ll have lots of opportunities to practice and revisit what was covered in this chapter.
18.224.95.38