Chapter 3. Breaking into Flutter

This chapter covers

  • Dissecting Flutter basics via the Increment app
  • Flutter widget classes
  • BuildContext, the widget tree, and the element tree
  • Flutter development environment and tips

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:

  1. Take an in-depth look at the counter app, which is the app that’s generated when you start a new Flutter project with the CLI.
  2. Make the counter app more robust by adding some basic widgets.
  3. Spend some time talking about BuildContext, the widget tree, and elements. Understanding how this works is 90% of debugging Flutter errors.
  4. Learn tricks and tools that the Flutter team has built in to the SDK that makes development enjoyable.
Note

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.

3.1. Intro to the counter app

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.

Warning

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

  • 1 The pub get command gets package dependencies in Dart. It must follow flutter if you’re building a Flutter app.

Now run your app. Figure 3.1 shows what you should see in your simulator.

Figure 3.1. The Flutter counter app

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.

3.1.1. Flutter project structure

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

  • 1 Compiled android app (doesn’t matter right now)
  • 2 iOS app (also doesn’t matter right now)
  • 3 Where you’ll spend 99% of your time
  • 4 Entry point of the project: must exist, and must contain main()
  • 5 Where you should spend 50% of your time but will probably spend 0.1%
  • 6 Required in all Dart projects: manages dependencies and metadata
  • 7 Generated lock file that you should not edit. Updates when you update pubspec.yaml, and ensures that you don’t introduce incompatible versions of packages.

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.

3.1.2. Anatomy of a Flutter app

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

  • 1 Imports the material library

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.

Note

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.

Application entry point

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.

3.1.3. Again, everything is a widget

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.

Note

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:

  • LayoutRow, Column, Scaffold, Stack
  • StructuresButton, Toast, MenuDrawer
  • StylesTextStyle, Color, Padding
  • AnimationsFadeInPhoto, transformations
  • Positioning and alignmentCenter, Padding

3.1.4. The build method

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
    );
  }
}

  • 1 MyApp is a widget, like everything else in Flutter.
  • 2 An annotation (decorator, in other frameworks) that tells Dart this class’s superclass (StatelessWidget) has a build method, but this method should be called instead
  • 3 Every widget has a build method that returns another widget.
  • 4 MaterialApp (a built-in widget) wraps your app to pass Material Design-specific functionality to all the widgets in the app.
  • 5 Widgets are classes that have constructors that take arguments. MaterialApp takes optional, named parameters: title, theme, and home, which take a String, ThemeData, and Widget, respectively.

3.1.5. The new and const constructors in Flutter

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"),
    );
}

  • 1 Neither the Button class nor the Text class is created with the new keyword.
  • 2 Uses the new keyword

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.

3.1.6. Hot reload

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
Using hot reload

Depending on your environment, you can trigger a hot reload a number of ways:

  • In Intellij, Visual Studio Code, or Android Studio, there’s a Hot Reload button, and the shortcut is Cmd-S (Ctrl-S on machines running Windows or Linux). This comes as a feature of the Flutter plugin for these IDEs.
  • If you used flutter run in your terminal, type r in that terminal to hot reload.

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.

3.2. Widgets: The widget tree, widget types, and the State object

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.

Figure 3.2. Counter app widget tree

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
    ),
);

  • 1 The Container widget has a property called child, which takes another widget.
  • 2 The Padding widget also has a property called child, which takes a widget.

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.

3.2.1. Stateless widgets

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.

Note

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.

Listing 3.1. An example button widget
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.

Listing 3.2. A widget with configuration
class SubmitButton extends StatelessWidget {
  final String buttonText;                    1
  SubmitButton(this.buttonText);              2

  Widget build(context) {
    return Button(
      child: Text(buttonText);                3
    );
  }
}

  • 1 Any data passed into the widget is part of its configuration.
  • 2 You can omit useless constructors in Dart, so this constructor wasn’t in the last example. Now it’s needed so the button knows to expect an argument when it’s built.
  • 3 Passes in a variable rather than a string literal. Flutter now knows to re-render this button whenever the variable passed in is different.

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.

3.2.2. Stateful widgets

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.

Figure 3.3. Example widget tree

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
    // ..
  }
}

  • 1 Inherits from StatefulWidget
  • 2 Overrides the superclass method createState
  • 3 Every stateful widget must have a createState method that returns a State object.
  • 4 Your state class inherits from the Flutter State object.
  • 5 StatefulWidget’s required build method
Private values in Dart with an underscore

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++;
  });
}

  • 1 One of the methods a Flutter State object uses to manage internal state

3.2.3. setState

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.

Figure 3.4. setState tells Flutter to repaint.

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).

Figure 3.5. setState visual

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.

3.2.4. initState

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);
    }
}

  • 1 The State.initState method is marked as mustCallSuper in the superclass. So, you must call the superclass implementation of initState in your overridden method.

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.

Figure 3.6. StatefulWidget lifecycle

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.

3.3. BuildContext

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).

Figure 3.7. Using Theme in Flutter

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.

3.4. Enhancing the counter app with the most important widgets

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:

  • Container
  • Row
  • Column
  • Image
  • Text
  • Icon
  • RaisedButton
  • Scaffold
  • AppBar

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.

Figure 3.8. Finished counter app 2.0

3.4.1. RaisedButton

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

  • A button to click
  • A function that decrements _counter by one

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.

Listing 3.3. Adding a raised button to _MyHomePageState.build
// _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
            ),
          ])),
      // ...

  • 1 Adds a RaisedButton
  • 2 onPressed is a property on a button that expects a callback. By passing in a callback, we can manage state in this parent widget (another common pattern).

To finish up that functionality, we need to write the _decrementCounter method:

void _decrementCounter() {
  setState(() => _counter--);     1
}

  • 1 setState takes a callback as an argument, which should solely update pieces of state in the widget.

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.

3.5. Favor composition in Flutter (over inheritance)

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.

3.5.1. What is composition?

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.)

3.5.2. An example of composition in Flutter

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
    );
  }
}

  • 1 This widget’s configuration is passed in to it, including the widget to display. Imagine the display passed in is Text(“Panic”).
  • 2 Sets the button’s background color to red
  • 3 This text widget is passed in from the parent. This is key!
  • 4 The callback is passed in as well. This makes it as flexible as possible. It doesn’t care about the callback and isn’t tied to any certain functionality. All it cares about is displaying a button and telling its parent when that button is pressed (via the callback).

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.

3.6. Intro to layout in Flutter

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.

3.6.1. Row and Column

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.

Listing 3.4. Column widget in the counter app
// _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,
      ),
    ],
  ),
),

  • 1 Aptly named column that lays out all its children in a column
  • 2 Alignment property similar to FlexBox in CSS. It tells Flutter how to lay out the Column children in relationship to each other (“each other” is key!).
  • 3 Some widgets (mainly layout widgets) take a list of widgets as children, rather than a single child.

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.

Tip

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.

Listing 3.5. Wrapping widgets in a Row
// _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).

Figure 3.9. Row widget with single child and no alignment

3.6.2. Layout constraints in Flutter

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.

3.6.3. RenderObject

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.

Note

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.

3.6.4. RenderObject and constraints

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:

  • Those that try to take up as much space as possible, such as the boxes used by a Center widget
  • Those that try to the same size as their children, such as the boxes used by an Opacity widget
  • Those that try to be a particular size, such as the boxes used by an Image widget

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.

3.6.5. RenderBoxes and layout errors

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:',
          ),
        ),
      ],
    ),
  ],
),

  • 1 The outer Column gives its children unbounded height.
  • 2 The inner Column now has an unbounded constraint, so it will try to fit its children.
  • 3 The Expanded widget tells its children to take up as much space as they can on the main axis of the flex box.

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.

3.6.6. Multi-child widgets

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).

Figure 3.10. Row widget with multiple children and no alignment

Listing 3.6. Adding a second button to the Row
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,
    ),
  ],
),

  • 1 The newly added widget

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,
  // ...

  • 1 Uses the spaceAround alignment option

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.

Figure 3.11. Row widget with spaceAround alignment

Figure 3.12. Alignment styles in Flutter

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.

3.6.7. Icons and the FloatingActionButton

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.

Table 3.1. Common Material Design icons

Icon

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?

  1. Write a new method resetCounter, and pass it to the FAB’s onPressed argument.
  2. Change the icon used in the FAB.

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
),

  • 1 Calls _resetCounter on tap, rather than _incrementCounter
  • 2 Tooltip that reflects what the button does to make the app more accessible
  • 3 The Icon widget’s unnamed, required argument expects IconInfo. All Material Design icons are available as constants on the Icons class.

When you hot-reload the app, it should now reflect the changes with the different icon and functionality.

3.6.8. Images

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.)

Listing 3.7. Adding an image to your Flutter pubspec.yaml file
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

  • 1 Metadata describing your project
  • 2 Refers to your Dart SDK version, which should be 2 or higher
  • 3 Dependencies needed in the production version of your app
  • 4 Gives you access to iOS style icons, if you want to use them
  • 5 Dependencies used in development only
  • 6 Where you configure Flutter
  • 7 Flag that ensures you have access to Material Icons
  • 8 The important part, where you declare your assets

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.

Listing 3.8. Adding an image to your Flutter app
children: <Widget>[
  Image.asset(
    'flutter_logo_1080.png',    1
    width: 100.0,               2
  ),
  Text(
    'You have pushed the button this many times:',
  ),

  • 1 The first argument to Image.asset expects the exact name you listed in pubspec.
  • 2 The Image widget allows you to explicitly set the width and height of an image. If you don’t, the image will be as large as its true size in pixels.

Now, if you hot reload, you’ll see an image in your app.

3.6.9. Container widget

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.

Figure 3.13. Transforming the Flutter logo with the Container widget

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,
  })
    ...

  • 1 Here, you can set all kinds of other properties like Border, BorderRadius, BoxShadow, background images, and more.

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.

Listing 3.9. Adding a Container widget
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,
  ),
),

  • 1 Puts space between widgets. The EdgeInsets.only constructor tells Flutter where to add the margin (in this case, 100 pixels of margin below this widget).
  • 2 Adds space around the current widget. The EdgeInsets.all constructor puts space on all sides. Figure 3.13 illustrates margin vs. padding.
  • 3 Passes decoration a class called BoxDecoration, which decorates boxes
  • 4 Sets the background color
  • 5 BorderRadius has multiple constructors: use circular when you want to curve all four corners of the box.
  • 6 Passes the image in to the child property, as usual

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.

Figure 3.14. Using the margin property versus the padding property

3.7. The element tree

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.

Figure 3.15. A simplified look at the layers of abstraction in the Flutter SDK

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.

The element tree and its importance to the developer

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).

Figure 3.16. The Flutter framework manages three threes, which interact via the element tree.

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.

3.7.1. Elements and widgets

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).

Note

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.

3.7.2. Exploring the element tree with an example

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.

Listing 3.10. _MyHomePageState configuration
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  bool _reversed = false;                                     1
  List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()];   2
  // ... rest of class
}

  • 1 This boolean will be used to determine if the buttons should be swapped.
  • 2 These keys are going to be important, but for now just know that they exist.

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.

Listing 3.11. The FancyButton custom widget
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

  • 1 This button manages its own color.
  • 2 Manages color for all fancy buttons
  • 3 Helper methods, used to allow the buttons to manage their own state, but also ensure that they’re never the same color. This code is contrived and not important to the lesson at hand.

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.

Listing 3.12. Updated app to use the FancyButton class
// _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
    }

  }

  • 1 Fancy button representing the Increment button
  • 2 Fancy button representing the Decrement button
  • 3 Creates a _buttons variable, which will be passed into a Row later, and displays these widgets
  • 4 If the _reversed member is true, reverses the order of the buttons. Since this happens in the build method, they’re swapped whenever setState is called and _reversed has been updated.

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;
    });
  }

  • 1 This method turns around and calls _swap, which will swap the buttons’ locations.
  • 2 This method updates the _reversed Boolean and calls setState, which triggers a rebuild!

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.

3.7.3. The element tree and State objects

A few things to keep in mind as I explain what’s happening:

  • State objects are actually managed by the element tree.
  • State objects are long-lived. Unlike widgets, they aren’t destroyed and rebuilt whenever widgets re-render.
  • State objects can be reused.
  • Elements have references to widgets.

The relationship between a single stateful widget, an element, and a state object is shown in figure 3.17.

Figure 3.17. The relationship between an element and a widget

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).

Figure 3.18. Each element points to a different widget and knows its type.

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:

  • The exact type at runtime
  • A widget’s key (if there is one)

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).

Figure 3.19. The elements think they’re the same widgets because they’re of the same type.

This issue presents another feature of Flutter: keys, which can be used by the framework to explicitly identify widgets.

An important note about elements

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.

3.7.4. Widget keys

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,
  ),
];

  • 1 Unique key that allows Flutter to identify this widget among widgets of the same type
  • 2 The first and last methods on Lists retrieve the first and last elements, respectively.

There is quite a bit more to explore with keys, and they are extremely useful. Next, I’ll talk about the different types.

Key types and when to use them

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.

Note

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

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

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:

  • ValueKey<T>—Value keys are best when the object you’re adding a key to has a constant, unique property of some sort. For example, in a todo list app, each widget that displays a todo probably has a Todo.text that’s constant and unique.
  • ObjectKey—Object keys are perfect when you have objects that are of the same type, but that differ in their property values. Consider an object called “Product” in an e-commerce app: two products could have the same title (two different sellers could sell Brussels sprouts). And one seller could have multiple products. What makes a product unique is the combination of the product name and the seller name. So, the key is a literal object passed into an ObjectKey:
    Key key = ObjectKey({
        "seller": product.seller,
        "product": product.title
    })
  • UniqueKey—Unique keys can be used if you’re adding keys to children of a collection and the children don’t know their values until they’re created. In the sample app, the product cards don’t know their color until they’re created, so a unique key is a good option.
  • PageStorageKey—This is a specialized key used to store page information, such as scroll location.

3.8. A final note

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.

Summary

  • In Flutter, everything is a widget, and widgets are just Dart classes that know how to describe their view.
  • 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.
  • Flutter favors composition over inheritance. Composition defines a “has a” relationship, and inheritance defines an “is a” relationship.
  • Every widget must contain a build method, and that method must return a widget.
  • Widgets should be immutable in Flutter, but state objects shouldn’t.
  • Widgets have const constructors in most cases. You can, and should, omit the new and const keywords when creating widgets in Flutter.
  • A StatefulWidget tracks its own internal state, via an associated state object. A StatelessWidget is “dumb” and is destroyed entirely when Flutter removes it from the widget tree.
  • setState is used to tell Flutter to update some state and then repaint. It should not be given any async work to do.
  • initState and other lifecycle methods are powerful tools on the state object.
  • BuildContext is a reference to a widget’s location in the widget tree. In practice, this means your widget can gather information about its place in the tree.
  • The element tree is the smart one. It manages widgets, which are just blueprints for elements that are actually in use.
  • In Flutter, widgets are rendered by their associated RenderBox objects. These render boxes are responsible for the telling the widget its actual, physical size. These objects receive constraints from their parent, and then use those to determine their actual size.
  • The Container widget is a “convenience” widget that provides a whole slew of properties that you would otherwise get from individual widgets.
  • Flutter Row and Column widgets use the concept of flex layouts, much like FlexBox in CSS.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.95.38