Stateless Widgets
Learning Objectives
- You know what widgets are and know how to create a stateless widget.
- You know how to defined classes and objects in Dart, and can create a class that extends another class.
- You know of a few ways to define constructors in Dart, and can use named arguments and constant constructors.
All Flutter applications are built using widgets. Widgets are essentially classes that extend Flutter’s Widget class in one way or another. Widgets can be stateful or stateless — here, we focus on stateless widgets.
Creating a stateless widget
Stateless widgets that do not maintain a state are created by inheriting (extending) Flutter’s class StatelessWidget. The StatelessWidget class has a method build
, which the child class overrides. The method build
must return an object that extends Flutter’s Widget class.
All Flutter’s existing widgets extend the Widget class.
The following example outlines the creation of a widget called CenteredText
that is used for showing centered text. The widget extends the StatelessWidget class, has a property “text”, and returns an instance of the Center widget. The Center widget has a Text widget as a child, which is given the text property of the widget.
There are three key expectations to follow when creating a stateless widget:
- If we define properties for the widget, we define them using the
final
keyword that states that their value cannot be reassigned. - If we define properties for the widget, we define a constructor that sets the values for the properties.
- We define a method
Widget build(BuildContext context)
that returns a widget.
If there are no properties, we can skip the constructor and the final
keyword. The following example shows a widget HelloWorldWidget
used for showing the text “Hello world!”.
Classes and objects
Dart is an object-oriented programming language. Like in object-oriented programming in general, classes are used to define the blueprint of objects, and objects are instances of classes.
Classes in Dart are defined using the keyword class
, which is followed by the name of the class and the block that contains the class definition. Naming of classes follows UpperCamelCase
, i.e. the first character of the name is also written in uppercase.
The following example defines a class called Person
that has a name and a year of birth. The name and year of birth are instance properties. The class also has a constructor Person
which is used to assign the values of the instance properties. The example also outlines the creation of an object of the class Person
, and printing the name of the created person.
Instance properties
As objects are instances of classes, each object has its own values of the instance properties. Instance properties of an object are accessed using the dot notation, where the name of the object comes before the dot and the name of the instance property comes after the dot. In the following, two objects are created out of the class Person
, and their names are printed.
Instance methods
Instance methods — i.e. kind-of-functions that have access to the instance properties of the object that the method is invoked on — are defined within the block of a class. In the following example, we define an instance method that is used to determine whether the person has been born after another person.
The example also demonstrates defining the type of return values as well as the type of the parameters. If we would wish to be explicit about instance properties, we could use the keyword this
, writing the function bornAfter
as follows.
bool bornAfter(Person person) {
return this.yearOfBirth > person.yearOfBirth;
}
Constructors
In the prior examples, we used a constructor that sets instance property values automatically, at least from the perspective of e.g. Java programmers (prior to the introduction records). There was no explicit need to define a constructor block to assign the instance property values.
When we write a constructor that follows the form Class(this.property, this.another, ...)
, we override a default constructor of the class. Each class can have only one default constructor, but a class can have more than one named constructor. A named constructor is defined by providing the class name followed by a dot and the name for the constructor, which is then followed by the parameters.
The following example defines two constructors for the class Person
, a default one and a named one called unknownBirthYear
. The named constructor sets the birth year to -9999
upon creation.
Dart also allows using another constructor from constructors (called “redirecting constructors” in Dart) using the keyword this
that indicates calling another constructor. The constructor unknownBirthYear
from the above class could also be written as follows.
Note that when using a redirecting constructor, the arguments cannot have the prefix this
.
Named arguments
When using some of Flutter’s widgets, we explicitly defined the name of the property to which we were passing values. As an example, the MaterialApp
was given a property home
, while the Scaffold
was given a property body
.
Creating constructors that explicitly require defining the properties to which the values are passed is done using named arguments. The names are written within curly brackets, and when calling a constructor, values for named arguments follow after the names.
In the following example, we adjust the default constructor of the class Person
class so that the constructor takes the name and the year of birth of the person as named arguments.
Similarly, if we would wish that our earlier CenteredText
widget requires explicitly stating the property text
, would define the constructor of the widget as follows.
Constant constructors
Dart allows defining constant constructors for creating constant objects. Constant objects are reused over and over by the language, without recreating the constant object when initiated. This is beneficial for performance, as the language can, in practice, cache existing objects.
Constant constructors are defined using the keyword const
. In addition, the properties of a class with constant constructors need to be declared final. The following example demonstrates using constant constructors in the class Person
, and creating the objects alvar
and fossil
using the constant constructors.
We could also define and use our CenteredText
widget as a constant object, as shown in the following example.
Many of the Flutter widgets have constant constructors that can be used to create constant objects. For example, the MaterialApp
widget is a constant object, the Scaffold
widget is a constant object, and so on. The above example could also be written as follows.
When creating Flutter applications, using constant constructors for objects that are used over and over again can improve the performance of the application.
Inheritance
Inheriting the properties and methods of an existing class when defining new classes is a common practice in object-oriented programming. As Dart is an object-oriented programming language, classes in Dart can inherit properties and methods from other classes.
In Dart, inheritance is done using the keyword
extends
. The class that inherits the properties and methods of another class is called a subclass, and the class that is inherited from is called a superclass.
Let’s look at inheritance through an example, starting with the following class Task
.
class Task {
String name;
bool completed;
Task(this.name, this.completed);
Task.completed({required name}) : this(name, true);
}
Imagine needing a separate class called PriorityTask
that needs the functionality from the class Task
and, in addition, a number indicating the priority of the task. In such a case, inheritance could be a solution, as the class PriorityTask
could inherit the properties and methods from the class Task
.
When inheriting a class, the constructors are not inherited, and the subclass needs to call the constructor of the superclass. Calling a constructor of the superclass is done using the keyword super
. The following example shows the class PriorityTask
that inherits the class Task
, adding a property priority
to the class, and providing an utility method to compare the priorities of two tasks.
class PriorityTask extends Task {
int priority;
PriorityTask(String name, this.priority): super(name, false);
bool hasHigherPriority(PriorityTask another) {
return priority > another.priority;
}
}
The constructor of the class PriorityTask
calls the constructor of the class Task
using the keyword super
. The constructor of the class Task
is called with the name of the task and the task is set as not completed. In addition, the constructor sets the priority of the task.
Now, we could create tasks with different priorities.
In a similar way, we could also define widgets that inherit the properties and methods of other widgets. The following example shows a widget GreetingWidget
that inherits the properties and methods of the widget CenteredText
, and simply calls the constructor of the superclass with the text “Hello world!”.