Welcome to Flutter! 1 — What exactly are Widgets?

Matias Silveiro
12 min readMar 6, 2023

--

Before you continue!

This tutorial takes in consideration that you have a running Flutter installation in your PC or Mac, and that you have basic understanding of Dart language. If that’s not the case, I encourage you to take a look at these tutorials first:

(Thanks Cassia Barbosa for your tutorials!)

Not having a running Flutter installation? Don’t mind! You can use DartPad from the web. Just copy and paste the codes from this article, and click the big and green Run button!

What are Widgets?

In Flutter framework, everything you want to render on screen is called a Widget. Widgets are the building blocks in any Flutter application. If you’ve been working in other frameworks, a widget in Flutter is the same as:

  • A View from Android XML or SwiftUI.
  • A composable from Jetpack Compose.
  • A component from React / React Native.

Taken directly from the official documentation:

The central idea is that you build your UI out of widgets. Widgets describe what their view should look like given their current configuration and state.

The beauty of Flutter widgets is that they can be combined and composed to create a rich, dynamic UI. In fact, Flutter provides lots of built-in widgets, such as buttons, images, cards and containers, but you can also build your own custom ones from scratch or by composition of smaller ones too.

A sample widget tree

Want to go deeper on widget trees and how Flutter manages it internally? Go ahead and read this article!

How do I create a Flutter app?

First of all, start a new Flutter project in your favorite IDE. I’ll use VSCode as it’s the most lightweight choice and almost every programmer has it installed in their machines. Just open your command pallete and…

Create your Flutter app

IntelliJ IDEA or Android Studio are best picks if you come from the Android development world (I personally prefer those). You can read more here and a comparison of IDEs here.

Taking a quick look at the generated code, you’ll see that Flutter needs a main function (an entry point for your app) and you have to call runApp() inside to launch your Flutter app, something like this:

void main() {
runApp(…)
}

And what’s the first and only parameter of runApp()? A Widget! In fact, your entire fancy Flutter app is, in the end, a really big widget!

Before creating our own widgets, let’s play a bit with some built-in ones. Test this code first:

import 'package:flutter/widgets.dart';

void main() {
runApp(
Text("Hello Flutter!")
)
}

Text() is one of the most basic Flutter’s widgets you may encounter, and it comes with plenty of properties to choose from:
- style
- textAlign
- textDirection
- maxLines

Among many many others. Let’s try changing our Text() style to a bold one:

import 'package:flutter/widgets.dart';

void main() {
runApp(
Text(
'Hello, world!',
style: TextStyle(fontWeight: FontWeight.bold),
),
);
}

Cheat code #1
Always let a trailing comma after the last parameter. It’s not only free of errors, but also helps auto-format tools to format your code the Flutter way!

But what if you want to display an image next to your Text()? There are some built-in widgets specially designed to compose widgets. These are:

  • Row: to arrange views horizontally.
  • Column: to arrange views vertically.
  • Stack: to overlap widgets.
  • Container: to control many parameters of a widget (padding, borders, background colors if not supported, among others).
View composition in declarative frameworks

In our example, to display an icon with a text we can use Row() widget to arrange them horizontally:

Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.home),
Text("Hello Flutter!"),
],
)

What if you want to add some padding around a widget? Well, in Compose you have the Modifier.padding() modifier and in SwiftUI you have the View.padding() view modifier. In Flutter there’s a Padding() widget which accepts a child, surrounding that child with some padding, or you can also use a Container() with the right parameter set. Take a look at the following example:

const textWithPadding = Padding(
padding: EdgeInsets.all(8.0),
child: Text("Hello world!"),
);
final textWithPaddingUsingContainer = Container(
padding: const EdgeInsets.all(8.0),
child: const Text("Hello world!"),
);

Cheat code #2
To surround a widget within another, you can use IDEs refactor shortcut to choose a *Wrap with …* refactor to auto-generate the code for you!

Pit stop! Have you noticed something in the example above? The first widget is defined as const and the second one is defined as final (but internal child is const)… What about that? Well, some widgets define const constructors and some others don’t. The main difference between const and final in Dart is:

  • final means single-assignment, calculated at runtime. Once assigned a value, a final variable’s value cannot be changed. Similar to let in Swift or val in Kotlin.
  • const objecs must be created from data that can be calculated at compile time, thus not having access to anything you would need to calculate at runtime, and transitively immutable.

The advantage of const declared widgets is that they have better performance at runtime. So whenever you can define your widget tree (or at least a part of it) as const it would have an impact in the app’s overall performance! Take in consideration that const declaration is recursive, so in the const textWithPadding widget the internal Text() is const too! On the other side, if inside your widget tree you have a non-const widget, the top-level widget cannot be declared as const.

Rule of Thumb #1
If the value you have is computed at runtime, you can not use a const for it. However, if the value is known at compile time, then you should use const over final.

Please refer to this link if you want to know more about the differences between var, final and const in Flutter and Dart.

So far you know how to render widgets on screen and how to arrange them, but what about building your own? Or how can you encapsulate larger widgets in a single one?

How do I create my own widgets?

If you are using an IDE (such as Android Studio or IntelliJ IDEA) or a powerful code editor like VSCode (sorry Vim, not this time), the easier way to create a widget in flutter is to use this snippet:

Snippet to create a Widget in VSCode

The resulting code is this:

class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

You can see a few things at a glance:

  • MyWidget extends a Widget subclass (we’ll come back at that later).
  • MyWidget overrides a method called build() which returns a Widget. This particular method is called whenever your MyWidget is redrawn or recreated.

So in order to make our text with icon a single widget, we may proceed as followed:

class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.home),
Text("Hello Flutter!"),
],
);
}
}

How do I interact with my widgets?

First of all, you need to know that runApp() needs one of the special parent widgets that Flutter defines as root containers, such as:

  • MaterialApp(), using Material Design styles (Google’s preferred design interface for Android) by default.
  • CupertinoApp(), using Human Interface styles (Apple’s preferred design interface for iOS) by default.

And as a child of any of those app containers, we can return a Scaffold widget, which provides the most common widgets for a screen (app bar, bottom navigation, drawer, FABs, among many others). If you don’t want to extend from MaterialApp or CupertinoApp you can use a Container widget as the root layout.

I’ll use MaterialApp for this tutorial. So the most basic Flutter app is this:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Widgets Tutorial',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Widgets Tutorial'),
),
body: const Text("The most basic app ever!")),
);
}
}

Upon that, let’s build a sample app with some interaction: a clicks counter app. In fact, we’ll build the Flutter sample app from scratch. Take a look at this code:

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatelessWidget {

MyHomePage({super.key, required this.title});

int _counter = 0;
final String title;

void _incrementCounter() {
_counter++;
print("Counter value: $_counter");
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

Reading and running the code above you can see the following:

  • We have a MaterialApp root with MyHomePage being a custom Scaffold widget.
  • We also define a callback to be executed whenever the FAB is pressed. If the callback is set as null, the button will have neither interaction nor ripple effect.
  • If you see the logcat of the running app, you’ll see that the value of _counter is being updated, but it’s not being updated on the screen!
    So what’s going on?? Why the counter is not being updated?

Flutter is declarative, meaning that Flutter builds its user interface to reflect the current state of your app. You code what you want to see, not how.

So when the state of your app changes, that triggers a redraw of the user interface, in opposition of imperative frameworks such as Android SDK or iOS UIKit. In Flutter, UI is reactive, it reacts to the state changes. But how does Flutter listen to state changes?

Stateless and stateful widgets

Widgets can be of two elemental types: stateless and stateful.

  • Stateless widgets do not hold any state. This means that they can be updated only when its parent changes.
  • Stateful widgets can hold the states internally, so it can be updated whenever its internal states changes or whenever its parents changes too.

Both stateless and stateful widgets are immutable, meaning that the only way to change them is by building a new instance of that widget. The key difference is that in stateful widgets, the state object it’s coupled with is mutable and allows the rebuild of the widget whenever the state changes.

In other words: A StatelessWidget will never rebuild by itself, but it has the ability to do so from external. A StatefulWidget can trigger the rebuild by itself (e. g. by calling setState()).

Extracted from this link:

The StatelessWidget only offers one public API to trigger a build which is the constructor, whereas the StatefulWidget has numerous triggers that cause a (re)build. didUpdateWidget() is called whenever configuration of the widget changes. This can for example be the case when the screen rotates.

Let’s go back with the example of the click counter app. We need a Stateful widget to hold the current count, and to setState() internally. How can we convert a Stateless widget to a Stateful one? Using Refactor feature in your IDE!

Refactor feature in VSCode

There’s something else missing in that code: the explicit call to `setState()`, which will trigger the re-render of the widget:

void _incrementCounter() {
setState(() {
_counter++;
});
}

Now try out the resulting code:

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {

MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
print("Counter value: $_counter");
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

Take a look at the resulting MyHomePage widget. It has a title property being set outside it, and an internal _counter state. So you can have a mix of internal and external states in a Stateful widget.

It may be tricky at first whether to chooser stateless or stateful approach, specially if you haven’t touched any of the frameworks mentioned above, but here are some tricks to make your choice a bit easier:

Rule of Thumb #2
If you have doubts, first go (state)LESS, and then become (state)FUL.

Rule of Thumb #3
Having an internal state means having a property that changes independent from the parent. Who is in charge of a potential change? Make that question to yourself!

Think about how you would build the previous widget for a second. If the products counter is inside the item widget (that is to say, a stateful item widget), it’s his responsibility of managing that value. Also it would have to expose some sort of callback to react to the event of incrementing and decrementing the value. And that value would be out of sync if an error occurs triggering those callbacks.

So it would be a good idea that the main widget (the one that holds the list) manages the state of each item, right? Well, in this particular case I think that’s the best solution, BUT taking in consideration that whenever any state changes, the whole stateful widget gets redrawn. So be careful with performance issues!

This is why widgets with animations tend to hold their animation state inside themselves.

Cheat code #2
If you want to reuse your widgets, make sure they are the most agnostic possible, so go stateless whenever possible.

Cheat code #3
(A friend told me that…) Stateful widgets tend to be harder to test.

After these few tips, you may think that I am an anti state-ist, but no!… Or at least not at all. The truth is that state management would probably become a pain if you don’t manage it the right way.

In these declarative frameworks, one of the main patters when building UIs is State Hoisting. From Jetpack Compose’s documentation:

State hoisting implies moving state to a widget’s caller to make that widget stateless.
State that is hoisted this way has some important properties:
- Single source of truth: By moving state instead of duplicating it, we’re ensuring there’s only one source of truth. This helps avoid bugs.
- Encapsulated: Only stateful widgets can modify their state. It’s completely internal.
- Shareable: Hoisted state can be shared with multiple widgets.
- Interceptable: callers to the stateless composables can decide to ignore or modify events before changing the state.
- Decoupled: the state for the stateless widget may be stored anywhere. For example, it’s now possible to move any property into a ViewModel.

So after reading about this pattern, here you have another useful rule:

Rule of Thumb #3
Widgets that are gonna be entire screens (state hosts) or have to run some animations may be stateful. And almost the rest of widgets are candidates to be stateless.

Speaking of state hoisting, take a look at this flow from the official Flutter documentation:

State management example in Flutter

In this example, since the shopping cart will be used in both MyCatalog and MyCart widgets, the state is being held in MyApp, hence we can probably define both MyCatalog and MyCart as Stateless if any other internal state is not needed.

State management best practices will be covered in another tutorial. Spoiler ahead: MVVM pattern looks really really good… And somebody mentioned ChangeNotifier class too!

See you in the next tutorial!

--

--

Matias Silveiro

Electronic Engineer & Mobile Developer (Android/iOS native, and now learning Flutter!). Currently working @ Globant