Flutter Widgets explained - LLF #4

Keff - Sep 5 '21 - - Dev Community

Hey there 👋

So far in this series, we've covered what Flutter is, how to set up the development environment, and dissected the starter project file by file. So the next logical step would be to get a better understanding of Widgets, as they are the foundation of Flutter's UI.

In this post, we will cover what Widgets are, the different types available, and finally how to use and create them.


There is a repo that complements this post here.


What are Widgets?

Flutter emphasizes widgets as a unit of composition. This means that they are the building blocks of a Flutter app interface. From a text or image, to how they are laid out on the page, almost everything is composed of widgets.

They form a hierarchy based on composition, also known as the widget tree. Each widget nests inside its parent and each widget receives "context" from its parent (the context is just some information about the location of the widget in the tree). This tree structure goes all the way up to the root widget. You could also imagine the widget tree as similar to the DOM but not the exactly the same (read more).

This is how a tree of widgets looks like:

ListView(
  children: const [
    Text('ListView is my parent'),
    ListTile(
      title: Text('My parent is ListTile'),
      trailing: Icon(Icons.check),
    )
  ],
);
Enter fullscreen mode Exit fullscreen mode

Complete example


Categories of widgets

From the research I've done, these are the main categories people seem to combine widgets in:

  • Accessibility
  • Animation and Motion
  • Assets, Images, and Icons
  • Async
  • Basics
  • Input
  • Interactive
  • Layout/structural
  • Painting and effects
  • Scrolling
  • Styling
  • Text

Flutter provides a set of pre-made Widgets, that help you build apps that follow Material Design or Cupertino style.

Let's break down some of them:

Layout/structural

They have no visual representation, they just control some aspect of the layout of one or more widgets. For example a Column, Row, Padding, etc...

Example of a structural Widget:

Column(
  children: [
    Text('Element 1'),
    Text('Element 2'),
  ]
);
Enter fullscreen mode Exit fullscreen mode

Complete example

The Column widget, will position the child widgets in a column or vertical direction.
Screenshot showing the app resulting from the previous code

Interactive

These are widgets that provide interactivity, for example, a button, a text field, a scrollable list, etc...

Example of an Interactive Widget:

ElevatedButton(
  onPressed: () {
    print('Click!');
  },
  child: const Text('A button'),
),
Enter fullscreen mode Exit fullscreen mode

Complete example

The ElevatedButton widget, will render a button that responds to events, like onPressed. This event will execute whenever the user presses/clicks the button.
Screenshot showing the app resulting from the previous code

Accessibility

These are widgets that help with making our apps more accessible.


Types of widgets

When writing flutter apps, we will use two kinds of widgets, depending on whether the widget has a state or not:

Stateless Widget

These are widgets that have no mutable state, meaning they will not change over time (for example a label or an icon)

const Text('I am a stateless widget!');
Enter fullscreen mode Exit fullscreen mode

The Text widget is stateless, as it does not need to keep any state, no value will change internally.

read more

Stateful Widget

These types of Widgets do have a mutable state, meaning they will react whenever the state changes, like a counter widget or in the following example a text field (or text input):

TextField(
  obscureText: true,
  decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Password',
  ),
);
Enter fullscreen mode Exit fullscreen mode

The TextField widget is stateful because it has to keep track of the value of the field and react whenever the user types on the keyboard.

read more

How do widgets update?

Flutter apps update their UI in response to events, such as a user interaction like a tap/click, or a state change.

These events tell Flutter that they need to replace a widget in the hierarchy with another widget. Flutter compares both the old and new widgets and efficiently updates the UI. Similar to VDOM if you're familiar with that, though not exactly the same (more info here).


Using widgets

Using widgets is rather easy, we just instantiate them wherever we like, like any other class. Note that using the new keyword is optional in Dart, which makes for cleaner code as loads of classes are instantiated all over the code.

For example, if we want to create a piece of text centered on the page, we could compose it like this:

build(context){
  return Center(
    child: Text('I see 4 widgets up my tree!'),
  );
}
Enter fullscreen mode Exit fullscreen mode

Or like this:

build(context){
  return Column(
    mainAxisAligment: MainAxisAligment.center,
    srossAxisAligment: CrossAxisAligment.center,
    children: [Text('I see 4 widgets up my tree!')],
  );
}
Enter fullscreen mode Exit fullscreen mode

Most widgets accept either child or children arguments, and each widget will manipulate or position those children in some way. Some will put them side by side, whilst others will make your widget contortionate in such ways that defy physics...

Let's see a complete example:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Home Page'),
        ),
        body: Center(
          child: Column(
            children: [
              const Text('Hello World'),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  print('Click!');
                },
                child: const Text('A button'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Complete example

We can see that MyApp is a widget that returns more widgets nested hierarchically. Flutter apps are composed by combining premade widgets like Scaffold or AppBar, and custom widgets created by ourselves (like the one created further down) or by third-party developers.

The above example will result in a widget tree like this:
Image of example's widget tree

And would render like this, when run on a mobile device (Android in this example):
Screenshot showing the app resulting from the previous code


Creating Widgets

Widgets are usually composed of many smaller, single-purpose widgets that combine to create more powerful ones. Flutter emphasized the importance of keeping the class hierarchy shallow, meaning that whenever possible, the widgets should be small, composable, and do one thing well.

As we've seen at the start of the post, Flutter apps are built by composing widgets. But we are not restricted to the widgets Flutter offers. We can create custom widgets too, let's see a basic example of how to do that.

To create a widget we need to create a class that extends from either StatelessWidget or StatefulWidget, and override the build method. What the build method returns will determine the visual representation of your widget.

For the custom widget, let's create a to-do list item widget. It will have text on the left, and an icon on the right.

class TodoListItem extends StatelessWidget {
  final bool isComplete;
  final String label;

  const TodoListItem({
    Key? key,
    this.isComplete = false,
    this.label = 'TODO',
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Icon(isComplete ? Icons.check : Icons.close),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

See complete widget

After that, we can use it anywhere in our app, like this:

Column(
  children: const [
    TodoListItem(label: 'Make coffee',  isComplete: true),
    TodoListItem(label: 'Drink it', isComplete: true),
    TodoListItem(label: 'Sleep', isComplete: false),
  ],
),
Enter fullscreen mode Exit fullscreen mode

See full example

The example above would result in something that looks like this:
Screenshot showing the app resulting from the previous code

Considerations

There are some important considerations to know when building widgets:

  • build() method should be free os side effects. Meaning that the function should return a new widget tree, regardless of what was returned previously. All that heavy work is done for you by Flutter.
  • The build() method should also be fast and returns quickly. Heavy computation should always be done asynchronously.

Summary

Widgets are the building block of Flutter's UI. There are different widgets for various kinds of use. Some of them respond to user input, like pressing a button, while others make our app more accessible or dynamic.

We can also create and compose widgets of our own in really simple and declarative ways. There is still much more to cover, more complementary posts will be heading this way to hopefully fill in all the gaps :P

Learning more


As always, thanks for reading. Hopefully, you now have a better understanding of what Widgets are.

And remember to comment if you have any suggestions or something you would like me to cover in these posts.


< Previous Post ... Next Post >

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .