Book Image

Flutter for Beginners

By : Alessandro Biessek
Book Image

Flutter for Beginners

By: Alessandro Biessek

Overview of this book

Google Flutter is a cross-platform mobile framework that makes it easy to write high-performance apps for Android and iOS. This book will help you get to grips with the basics of the Flutter framework and the Dart programming language. Starting from setting up your development environment, you’ll learn to design the UI and add user input functions. You'll explore the navigator widget to manage app routes and learn to add transitions between screens. The book will even guide you through developing your own plugin and later, you’ll discover how to structure good plugin code. Using the Google Places API, you'll also understand how to display a map in the app and add markers and interactions to it. You’ll then learn to improve the user experience with features such as map integrations, platform-specific code with native languages, and personalized animation options for designing intuitive UIs. The book follows a practical approach and gives you access to all relevant code files hosted at github.com/PacktPublishing/Flutter-for-Beginners. This will help you access a variety of examples and prepare your own bug-free apps, ready to deploy on the App Store and Google Play Store. By the end of this book, you’ll be well-versed with Dart programming and have the skills to develop your own mobile apps or build a career as a Dart and Flutter app developer.
Table of Contents (21 chapters)
Free Chapter
1
Section 1: Introduction to Dart
5
Section 2: The Flutter User Interface - Everything is a Widget
10
Section 3: Developing Fully Featured Apps
15
Section 4: Advanced Flutter - Resources to Complex Apps

Introducing the structure of the Dart language

If you already know some programming languages inspired by the old C language or have some experience of JavaScript, much of the Dart syntax will be easy for you to understand. Dart provides the most typical operators for manipulating variables. Its built-in types are the most common ones found in high-level programming languages, with a few particularities. Also, control flows and functions are very similar to typical ones. Let's review some of the structure of the Dart programming language before diving into Flutter.

If you already know Dart, you can use this section as a review of the Dart syntax; otherwise, you can check out this introduction and refer to the Dart language tour for a quick and easy learning guide on Dart: https://dart.dev/guides/language/language-tour.

Dart operators

In Dart, operators are nothing more than methods defined in classes with a special syntax. So, when you use operators such as x == y, it is as though you are invoking the x.==(y) method to compare equality.

As you might have noted, we are invoking a method on x, which means x is an instance of a class that has methods. In Dart, everything is an Object instance; any type you define is also an Object instance. There's more on that in the Introduction to OOP in Dart section.

This concept means that operators can be overridden so that you can write your own logic for them. Again, if you have some experience in Java, C#, JavaScript, or similar languages, you can skip most of the operators, as they are very similar in several languages.

We are not going to go into every specific Dart syntax detail in this book. You can refer to the source code on GitHub for many examples of the Dart syntax.

Dart has the following operators:

  • Arithmetic
  • Increment and decrement
  • Equality and relational
  • Type checking and casting
  • Logical operators
  • Bits manipulation
  • Null-safe and null-aware (modern programming languages provide this operator to facilitate null value handling)

Let's look at each one in more detail.

Arithmetic operators

Dart comes with many typical operators that work like many languages; this includes the following:

  • +: This is for the addition of numbers.
  • -: This is for subtraction.
  • *: This is for multiplication.
  • /: This is for division.
  • ~/: This is for integer division. In Dart, any simple division with / results in a double value. To get only the integer part, you would need to make some kind of transformation (that is, type cast) in other programming languages; however, here, the integer division operator does this task.
  • %: This is for modulo operations (the remainder of integer division).
  • -expression: This is for negation (which reverses the sign of expression).

Some operators have different behavior depending on the left operand type; for example, the + operator can be used to sum variables of the num type, but also to concatenate strings. This is because they were implemented differently in the corresponding classes as pointed out before.

Dart also provides shortcut operators to combine an assignment to a variable after another operation. The arithmetic or assignment shortcut operators are +=, -=, *=, /=, and ~/=.

Increment and decrement operators

The increment and decrement operators are also common operators and are implemented in number type, as follows:

  • ++var or var++ to increment 1 into var
  • --var or var-- to decrement 1 from var

The Dart increment and decrement operators don't have anything different to typical languages. A good application of increment and decrement operators is for count operations on loops.

Equality and relational operators

The equality Dart operators are as follows:

  • ==: For checking whether operands are equal
  • !=: For checking whether operands are different

For relational tests, the operators are as follows:

  • >: For checking whether the left operand is greater than the right one
  • <: For checking whether the left operand is less than the right one
  • >=: For checking whether the left operand is greater than or equal to the right one
  • <=: For checking whether the left operand is less than or equal to the right one
In Dart, unlike Java and many other languages, the == operator does not compare memory references but rather the content of the variable.

Type checking and casting

Dart has optional typing, as you already know, so type checking operators may be handy for checking types at runtime:

  • is: For checking whether the operand has the tested type
  • is!: For checking whether the operand does not have the tested type

The output of this code will be different depending on the context of the execution. In DartPad, the output is true for the check of the double type; this is due to the way JavaScript treats numbers and, as you already know, Dart for the web is precompiled to JavaScript for execution on web browsers.

There's also the as keyword, which is used for typecasting from a supertype to a subtype, such as converting num into int.

The as keyword is also used to specify a prefix for the libraries using imports (you can read more about this in Chapter 2, Intermediate Dart Programming).

Logical operators

Logical operators in Dart are the common operators applied to bool operands; they can be variables, expressions, or conditions. Additionally, they can be combined with complex expressions by combining the results of the expressions. The provided logical operators are as follows:

  • !expression: To negate the result of an expression, that is, true to false and false to true
  • ||: To apply logical OR between two expressions
  • &&: To apply logical AND between two expressions

Bits manipulation

Dart provides bitwise and shift operators to manipulate individual bits of numbers, usually with the num type. They are as follows:

  • &: To apply logical AND to operands, checking whether the corresponding bits are both 1
  • |: To apply logical OR to operands, checking whether at least one of the corresponding bits is 1
  • ^: To apply logical XOR to operands, checking whether only one (but not both) of the corresponding bits is 1
  • ~operand: To invert the bits of the operand, such as 1s becoming 0s and 0s becoming 1s
  • <<: To shift the left operand in x bits to the left (this shifts 0s from the right)
  • >>: To shift the left operand in x bits to the right (discarding the bits from the left)

Like arithmetic operators, the bitwise ones also have shortcut assignment operators, and they work in the exact same way as the previously presented ones; they are <<=, >>=, &=, ^=, and |=.

Null-safe and null-aware operators

Following the trend on modern OOP languages, Dart provides a null-safe syntax that evaluates and returns an expression according to its null/non-null value.

The evaluation works in the following way: if expression1 is non-null, it returns its value; otherwise, it evaluates and returns the value of expression2: expression1 ?? expression2.

In addition to the common assignment operator, =, and the ones listed in the corresponding operators, Dart also provides a combination between the assignment and the null-aware expression; that is, the ??= operator, which assigns a value to a variable only if its current value is null.

Dart also provides a null-aware access operator, ?., which prevents accessing null object members.

Dart types and variables

You probably already know how to declare a simple variable, that is, by using the var keyword followed by the name. One thing to note is that when we do not specify the variable's initial value, it assumed null no matter its type.

final and const

A variable will never intend to change its value after it is assigned, and you can use the final and const ways for declaring this:

final value = 1;

The value variable cannot be changed once it's initialized:

const value = 1;

Just like the final keyword, the value variable cannot be changed once it's initialized, and its initialization must occur together with a declaration.

In addition to this, the const keyword defines a compile-time constant. As a compile-time constant, the const values are known at compile time. They also can be used to make object instances or Lists immutable, as follows:

const list = const [1, 2, 3]
// and
const point = const Point(1,2)

This will set the value of both variables during compile time, turning them into completely immutable variables.

Built-in types

Dart is a type-safe programming language, so types are mandatory for variables. Although types are mandatory, type annotations are optional, which means that you don't need to specify the type of a variable when declaring it. Dart performs type inference, and we will examine more of this in the Type inference – bringing dynamism to the show section.

Here are the built-in data types in Dart:

  • Numbers (such as num, int, and double)
  • Booleans (such as bool)
  • Collections (such as lists, arrays, and maps)
  • Strings and runes (for expressing Unicode characters in a string)

Numbers

Dart represents numbers in two ways:

  • Int: 64-bit signed non-fractional integer values such as -263 to 263-1.
  • Double: Dart represents fractional numeric values with a 64-bit double-precision floating-point number.

Both of them extend the num type. Additionally, we have many handy functions in the dart:math library to help with calculations.

In JavaScript, numbers are compiled to JavaScript Numbers, and allow the values -253 to 253-1.

Additionally, note that num, double, and int types cannot be extended or implemented.

BigInt

Dart also has the BigInt type for representing arbitrary precision integers, which means that the size limit is the running computer's RAM. This type can be very useful depending on the context; however, it does not have the same performance as num types and you should consider this when deciding to use it.

JavaScript has the concept of safe integers, which Darts follows when transpiling to it. However, as JavaScript uses double-precision to represent even integers, we do not have an overflow when doing (maxInt * 2).

Now, you might consider putting BigInt everywhere you would use integers to be free of overflows, but remember, BigInt does not have the same performance as int types, making it unsuitable for all contexts.

Additionally, if you want to know how Dart VM handles numbers internally, take a look at the Further reading section at the end of this chapter.

Booleans

Dart provides the two well-known literal values for the bool type: true and false.

Boolean types are simple truth values that can be useful for any logic. One thing you may have noticed, but that I want to reinforce, is about expressions.

We already know that operators, such as > or ==, for example, are nothing more than methods with a special syntax defined in classes, and, of course, they have a return value that can be evaluated in conditions. So, the return type of all these expressions is bool and, as you already know, Boolean expressions are important in any programming language.

Collections

In Dart, lists are considered to be the same as arrays in other programming languages with some handy methods to manipulate elements.

Lists have the [index] operator to access elements at the given index and, additionally, the + operator can be used to concatenate two lists by returning a new list with the left operand followed by the right one.

Another important thing about Dart lists is the length constraint. This is in the way we define the preceding lists, making them grow as needed by using the add method, which will grow to append the element.

Another way to define the list is by setting its length on creation. Lists with a fixed size cannot be expanded, so it's the developer's responsibility to know where and when to use fixed size lists, as it can throw exceptions if you try to append or access invalid elements.

Dart Maps are dynamic collections for storing values on a key basis, where the retrieval and modification of a value is always performed by using its associated key. Both the key and value can have any type; if we do not specify the key-value types, they will be inferred by Dart as Map<dynamic,dynamic>, with its keys and values of the dynamic type. We'll explain more about dynamic types later.

Strings

In Dart, strings are a sequence of characters (UTF-16 code) that are mainly used to represent text. Dart strings can be single or multiple lines. You can match single or double quotes (typically for single lines), and multiline strings by matching triple quotes.

We can use the + operator to concatenate strings. The string type implements useful operators other than the plus (+) one. It implements the multiplier (*) operator where the string gets repeated a specified number of times, and the [index] operator retrieves the character at the specified index position.

String interpolation

Dart has a useful syntax to interpolate the value of Dart expressions within strings: ${}, which works as follows:

main() {
String someString = "This is a String";

print
("The string value is: $someString ");
// prints The string value is: This is a String

print
("The length of the string is: ${someString.length} ");
// prints The length of the string is: 16
}

As you may have noticed, when we are inserting just a variable and not an expression value into the string, we can omit the braces and just add $identifier directly.

Dart also has the runes concept to represent UTF-32 bits. For more details, check out the Dart language tour: https://dart.dev/guides/language/language-tour.

Literals

You can use the [] and {} syntaxes to initialize variables such as lists and maps, respectively. These are some examples of literals provided by the Dart language for creating objects of the provided built-in types:

Type

Literal example

int

10, 1, -1, 5, and 0

double

10.1, 1.2, 3.123, and -1.2

bool

true and false

String

"Dart", 'Dash', and """multiline String"""

List

[1,2,3] and ["one", "two", "three"]

Map

{"key1": "val1", "b": 2}
A literal is a notation to represent a fixed value in programming languages. You have likely already used some of these before.

Type inference – bringing dynamism to the show

In the previous examples, we demonstrated two ways of declaring variables: by using the type of the variable, such as int and String, or by using the var keyword.

So, now you may be wondering how Dart knows what type of variable it is if you don't specify it in a declaration.

From the Dart documentation (https://dart.dev/guides/language/effective-dart/documentation), consider the following statement:

"The analyzer can infer types for fields, methods, local variables, and most generic type arguments. When the analyzer doesn't have enough information to infer a specific type, it uses the dynamic type."

This means that, when you declare a variable, the Dart analyzer will infer the type based on the literal or the object constructor.

Here is an example:

import 'dart:mirrors';

main() {
var someInt = 1;
print(reflect(someInt).type.reflectedType.toString()); // prints: int
}

As you can see, in this example we have only the var keyword. We didn't specify any type, but as we used an int literal (1), the analyzer tool could infer the type successfully.

Local variables get the type inferred by the analyzer in the initialization. In the preceding example, trying to assign a string value to someInt would fail.

So, let's consider the following code:

main() {
var a; // here we didn't initialized var so its
// type is the special dynamic
a = 1; // now a is an int
a = "a"; // and now a String

print
(a is int); // prints false
print(a is String); // prints true
print(a is dynamic); // prints true
print(a.runtimeType); // prints String
}

As you may have noticed, a is a String type and a dynamic type. dynamic is a special type and it can assume any type at runtime; therefore, any value can be cast to dynamic too.

Dart can infer types for fields, method returns, and generic type arguments; we'll explore each one in more detail in their respective sections in this book.

The Dart analyzer also works on collections and generics; for the map and list examples in this chapter, we used the literal initializer for both, so their types were inferred.

Control flows and looping

We've reviewed how to use Dart variables and operators to create conditional expressions. To work with variables and operators, we typically need to implement some control flow to make our Dart code take the appropriate direction in our logic.

Dart provides some control flow syntax that is very similar to other programming languages; it is as follows:

  • if-else
  • switch/case
  • Looping with for, while, and do-while
  • break and continue
  • asserts
  • Exceptions with try/catch and throw

The Dart syntax for these control flows does not have any important particularities that need to be reviewed in detail. Please refer to the official language tour on control flows for details: https://dart.dev/guides/language/language-tour#control-flow-statements.

Functions

In Dart, Function is a type, like String or num. This means that they can also be assigned to fields or local variables, or passed as parameters to other functions; consider the following example:

String sayHello() {
return "Hello world!";
}

void main() {
var sayHelloFunction = sayHello; // assigning the function
// to the variable
print(sayHelloFunction()); // prints Hello world!
}

In this example, the sayHelloFunction variable stores the sayHello function itself and does not invoke it. Later on, we can invoke it by adding () to the variable name just as though it was a function.

Trying to invoke a non-function variable could result in a compiler error.

The function return type can be omitted as well, so the Dart analyzer infers the type from the return statement. If no return statement is provided, it assumes return null. If you want to tell it that it doesn't have a return, you should mark it as void:

sayHello() { // The return type stills String
return "Hello world!";
}

Another way to write this function is by using the shorthand syntax, () => expression;, which is also called the Arrow function or the Lambda function:

sayHello() => "Hello world!";

You cannot write statements in place of expression, but you can use the already known conditional expressions (that is, ?: or ??).

In this example, the sayHello function is a top-level function. In other words, it does not need a class to exist. Although Dart is an object-oriented language, it is not necessary to write classes to encapsulate functions.

Function parameters

A function can have two types of parameters: optional and required. Additionally, as with most modern programming languages, these parameters can be named on call to make the code more readable.

The parameter type doesn't need to be specified; in this case, the parameter assumes the dynamic type:

  • Required parameters: This simple function definition with parameters is achieved by just defining them in the same way as most other languages. In the following function, both name and additionalMessage are required parameters, so the caller must pass them when calling it:
sayHello(String name, String additionalMessage) => "Hello $name. $additionalMessage";
  • Optional positional parameters: Sometimes, not all parameters need to be mandatory for a function, so it can define optional parameters as well. The optional positional parameter definition is done by using the [ ] syntax. Optional positional parameters must go after all of the required parameters, as follows:
sayHello(String name, [String additionalMessage]) => "Hello $name. $additionalMessage";

If you run the preceding code without passing a value for additionalMessage, you will see null at the end of the returned string. When the optional parameter is not specified, the default value is null unless you specify default values for them:

void main() {
print(sayHello('my friend')); // Hello my friend. null
print(sayHello('my friend', "How are you?"));
// prints Hello my friend. How are you?
}

To define a default value for a parameter, you add it after the = sign right after the parameter definition:

sayHello(String name, [String additionalMessage = "Welcome to Dart Functions!" ]) => "Hello $name. $additionalMessage";

Not specifying the parameter results in printing the default message, as follows:

void main() {
var hello = sayHello('my friend');
print(hello);
}
  • Optional named parameters: The optional named parameter definition is done by using the { } syntax. They must also go after all of the required parameters:
sayHello(String name, {String additionalMessage}) => "Hello $name. $additionalMessage";

The caller must specify the name of the optional named parameter, as follows:

void main() {
print(sayHello('my friend'));
// it stills optional, prints: Hello my friend. null

print(sayHello('my friend', additionalMessage: "How are you?"));
// prints: Hello my friend. How are you?
}

Named parameters are not exclusive to optional parameters; to make a named parameter a required parameter, you can mark it with @required:

sayHello(String name, {@required String additionalMessage}) => "Hello $name. $additionalMessage";

Again, the caller must specify the name of the required named parameter:

void main() {
var hello = sayHello('my friend', additionalMessage:"How are
you?");
// not specifying the parameter name will result in a hint on
// the editor, or by running dartanalyzer manually on console

print(hello); // prints "Hello my friend. How are you?"
}
  • Anonymous functions: Dart functions are objects and they can be passed as parameters to other functions. We already saw this when using the forEach() function of the iterable.

An anonymous function is a function that doesn't have a name; it is also called lambda or closure. The forEach() function is a good example of this; we need to pass a function to it that will be executed with each of the list collection elements:

void main() {
var list = [1, 2, 3, 4];
list.forEach((number) => print('hello $number'));
}

Our anonymous function receives an item but does not specify a type; then, it just prints the value received by the parameter.

  • Lexical scope: The Dart scope is determined by the layout of the code using curly braces like many programming languages; the inner functions can access variables all the way up to the global level:
globalFunction() {
print("global/top-level function");
}

simpleFunction() {
print("simple function");
globalFunction() {
print("Not really global");
}

globalFunction();
}

main() {
simpleFunction();

globalFunction();
}

If you examine the preceding code, globalFunction function from simpleFunction will be used instead of the global version, because it is defined locally on its scope.

In the main function, in contrast, the global version of globalFunction function is used, because, in this scope, the internal globalFunction function from simpleFunction is not defined.

Data structures, collections, and generics

Dart provides multiple kinds of structures to manipulate a set of values. Dart lists are widely used even in the most simple use cases. Generics are a concept when working with collections of data tied to a specific type, such as List or Map, for example. They ensure a collection will have homogeneous values by specifying the type of data it can hold.

Generics

The <..> syntax is used to specify the type supported by a collection. If you look at the previous examples of lists and maps, you will notice that we have not specified any type. This is because they are optional, and Dart can infer the type based on elements during the collection initialization.

Check this chapter's source code on GitHub for examples on collections and generics. Remember, if the Dart analyzer tool cannot infer the type, it assumes the dynamic type.

When and why to use generics

The use of generics can help a developer to maintain and keep collection behavior under control. When we use a collection without specifying the allowed element types, it is our responsibility to correctly insert the elements. This, in a wider context, can become expensive, as we need to implement validations to prevent wrong insertions and to document it for a team.

Consider the following code example; as we have named the variable avengerNames, we expect it to be a list of names and nothing else. Unfortunately, in the coded form, we can also insert a number into the list, causing disorganization or confusion:

main() {
List avengerNames = ["Hulk", "Captain America"];
avengerNames.add(1);

print("Avenger names: $avengerNames");
// prints Avenger names: [Hulk, Captain America, 1]
}

However, if we specify the string type for the list, then this code would not compile, avoiding this confusion:

main() {
List<String> avengerNames = ["Hulk", "Captain America"];

avengerNames.add(1);
// Now, add() function expects an 'int' so this doesn't compile

print("Avenger names: $avengerNames");
}

Generics and Dart literals

If you check out this chapter's list and map examples, you will see we used the [] and {} literals to initialize them. With generics, we can specify a type during the initialization, adding a <elementType>[] prefix for lists and <keyType, elementType>{} for maps.

Take a look at the following example:

main() {
var avengerNames = <String>["Hulk", "Captain America"];
var avengerQuotes = <String, String>{
"Captain America": "I can do this all day!",
"Spider Man": "Am I an Avenger?",
"Hulk": "Smaaaaaash!"
};
}

Specifying the type of list, in this case, seems to be redundant as the Dart analyzer will infer the string type from the literals we have provided. However, in some cases, this is important, such as when we are initializing an empty collection, as in the following example:

var emptyStringArray = <String>[];

If we have not specified the type of the empty collection, it could have any data type on it as it would not infer the generic type to adopt.

To learn how Dart plays with the generics concept and the additional data structures provided by the language, you can refer to the official language tour for details: https://dart.dev/guides/language/language-tour#generics.