Flutter and Dart Basics

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.

Run the program to see the output

There are three key expectations to follow when creating a stateless widget:

  1. If we define properties for the widget, we define them using the final keyword that states that their value cannot be reassigned.
  2. If we define properties for the widget, we define a constructor that sets the values for the properties.
  3. We define a method Widget build(BuildContext context) that returns a widget.
Loading Exercise...

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!”.

Run the program to see the output

Loading Exercise...

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.

Run the program to see the output

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.

Run the program to see the output

Loading Exercise...

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.

Run the program to see the output

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.

Run the program to see the output

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.

Run the program to see the output

Note that when using a redirecting constructor, the arguments cannot have the prefix this.

Loading Exercise...

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.

Run the program to see the output

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.

Run the program to see the output

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.

Run the program to see the output

We could also define and use our CenteredText widget as a constant object, as shown in the following example.

Run the program to see the output

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.

Run the program to see the output

When creating Flutter applications, using constant constructors for objects that are used over and over again can improve the performance of the application.

Loading Exercise...

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.

Run the program to see the output

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!”.

Run the program to see the output

Loading Exercise...