Learning Flutter: Chapter I
I am following this udemy course Flutter & Dart - The Complete Guide [2021 Edition]. This project has the only objective form me to code along the course and to learn.
The app is deployed in GitHub Pages
I've deployed in GH Pages just for leaning purposes, I haven't adapted the UI for the web.
Here I've been taking notes while following the course. It may be incomplete. The only purpose of this project is to learn learn dart and flutter and to keep it posted so I can review it any time when needed.
- Create Flutter Project
- Material Design
- Flutter vs alternatives
- IDE: Visual Studio Code
- Emulator
- Some Dart basics
- Widgets
- Understanding "State"
Command to create a flutter project from the terminal. The project name cannot contain spaces or dashes
flutter create name_of_the_project
It's a design system created and heavily used by Google. It's highly customizable and works on iOS too. Whils material design is built into Flutter, there are alsom Apple styled widgets in the Flutter framework (Cuppertino widgets).
- uses the Dart programming language and the Flutter framework
- You get compiled Native apps. Great performance.
- Does NOT compile to iOS / Android UI Component. Flutter controlls every pixel of the app, so there is a lot of flexibility on the customisation.
- You can build Cross-platform (mobile, web, desktop)
- Developed by Google
- uses JavaScript and the React.js library
- You get partialy compiled native apps. Some parts are not compiled but enclosed in the native app and runs as javascript in the native app.
- Does compile to iOS / Android UI Components. There is less customization options as there are UI components restrictions.
- You can build mostly mobile apps ( + React Native web)
- Developed by Facebook
- uses only Javascript. No frameworks.
- You get webApp wrapped as a Native app. The advantage is that you can use normal web technologies. A dawnside could be performance.
- Does not compile to native UI Components. You can build it as in web.
- you can build Cross-platform (mobile, web, desktop)
- Developed by Ionic
In order to work with Visual Studio and Flutter really well we should install some extensions:
- Flutter
- Dart (it's installed automatically when installing the Flutter extension)
- /.idea: holds some configuration for Android Studio
- /.vscode: holds configurations for the IDE. It's created only when you do changes of the vs code config
- android: it holds a complete android project. This is the project which the Flutter SDK will use to merge the flutter code into it. Normally, this is a passive folder, it will be used by flutter but we don't need to work on it.
- /build: it is generated and managed by the flutter SDK. We shouldn't change anythng in here. It holds the output of the flutter aplication.
- /ios: is the same as android, but for ios projects. It only exists in when you work in mac, because only there you can build ios app.
- /lib: here we will add all the dart files. Here we will work 99% of the time and here is located all our dart code.
- /test: this folder is where we can put the automated tests. Code that runs our code and tests it for bugs or errors.
- .gitignore: the file to configure the list of files we want to ignore in the git commits. It's optional, only important when using GIT.
- .metadata: it's not a file we will work on. it's automatically generated by flutter.
- .packages: generated automatically by the flutter sdk.
- name_of_project.iml: we will not work on it. Automatically made by flutter and includes some dependencies.
- pubspeck.lock: is a file generated automatically based on the pubspec.yaml file. It holds more details about all the dependencies of the project. It's required by flutter but we won't work on it.
- pubspeck.yaml: we will work in this file. Here we configure the dependencies of the project, the third party packages we may be using. We will also configure other things like fonts, images. It's written in yaml, a text format where indentation is very important.
- readme.md: is the text file where we can include some information of the project. Is the file you are reading right now.
Start Android Studio > Configure > AVD Manager Create Virtual Device > Phone > Pick one phone > pick last Android stable version image > finish
Run the app:
- From terminal: "flutter run" command
- From the visual studio menĂş: "debug" > "start Debugging" or "start without debugging"
//In dart we comment any line by adding two slices before the line of code.
Functions are code snippets that you can execute multiple times and any time you want. It is defined by:
It has to follow a naming convention which is called camelcase. It should not have spaces between words. The first word whould start with a lowercase character and the other words will start with uppercase letters. "main" is a special function name. The main function is the entry point of a dart application, it is the first function to be called automatically by dart when the app starts.
The inputs for the function, they are inside de parentesis. The arguments are separed by commas and they can have any name we want. There can be 0 arguments.
- Positional arguments: when we call the function, we provide the values of the arguments separated by commas. In the function, they will be asigned to every argument based on the position. The first value to the first argument, the second with the second... and so on.
- Named arguments: when we call the function, we write the name of the argument followed by a colon and the value for it.
The code executed when the function gets called is limitated between curly braces. For every expression in dart we have to add a semicolon (;) at the end, except for the definition of a function, there we don't use the semicolon.
Before the function name there is the type of the function. Dart is a typed language, which means that everything has a type. So the type indicates what type of data the function returns. When it returns nothing, the return type is void. As dart is a strictly typed programming language, we need to indicate the type of data a function returns and the type of every argument. If we don't indicate the type of data, dart asumes the data is a dynamic type, and the compiler cannot help us to avoid mismatch data type errors. If posible, we should avoid dinamic type, and asign explicit types.
To call a function we write the function name followed by the parentesis. If the function take arguments, we put them inside the parentesis. If it doesn't, we write the parentesis with nothing inside.
- print() is a predefined dart function that prints somethng on the console.
Inside the function we can use the special keyword "return" to express what will be the result of that function. The type of the object returning must be the same as the function itself. If it's a void function, it wouldn't return anything.
Everything in dart is an object, which is a data structure that has some conflicts logic inside. There are some special types of predefined objects in dart. Some of them are:
- String: a text. It's defined between cuotes ('simple' or "double")
- Integer: numbers without decimal places
- double: numbers with decimal places.
- num: any number: can be double or int
- bool: a boolean can be true or false.
"null" is a value that means nothing. Can be asigned to any type. Normally we would use it to reset the value of a variable.
We can store data in memory with the help of variables so we can use that data some lines later in the same code. We don't want to store it in a file or in a database, but in the memory of the device.
- before the name of the variable we use the keyword "var", so dart understand we are creating a variable. Or we can insted tell dart the type of data will be stored. If we asign a value when initializing the variable it's considered a better practice to use the "var" kayword.
- We create a variable by giving it any name we want. We follow the same naming convention as for naming functions.
- We asign a value to that with a equal sign. We also can reuse a variable by reasigning another value to the same variable, so it will be overwritten.
We write classes to define objects in dart. Structure of a class:
- keyword "class"
- class name: should start with UpperCase character
- class body: inside curly braces
- a variable inside a class are called property
- a function inside a class are called methods Then, we instantiate the class to create an object based on that class. We can access the data inside an object (for example, the instance of a class), by using a dot (.) followed by a property name.
// a class
class Person {
String name = 'Max';
int age = 30;
}
//a function that returns a double
double addNumbers(double num1, double, num2) {
return num1 + num2;
}
// the main function
void main() {
//p1 is an instance of the Person class
var p1 = Person();
var p2 = Person();
p2.name = 'Eloi';
double firstResult;
firstResult = addNumbers(1,1);
print(firstResult + 1); //it prints "3"
print(p1.name); //it prints "Max"
print(p2.name); //it prints "Eloi"
}
In functions with only one expression in the body, we can get rid of the curly fraces and instead use the "=>". The return of this expression will automatically be returned (if it returns something)
void main(){
print(hello);
}
// is the same as:
void main() => print(hello);
When we use a widget that takes a function as a parameter, we should provide a pointer to that function, not the function itself. So we should provide the name of the function without parentesis. Becouse if we provide the parentesis, we'll be telling flutter that the value of the parameter will be the result of the function, not the function itself.
onPressed: answerQuestion(), // WRONG! in the build method flutter will execute the function, it won't wait for the user to press the button.
onPressed: answerQuestion, // CORRECT. The function will be executed with the onPressed event.
When we have a function that you only need in one place and we are not calling it from anywhere else, we can use an anonymous function. It's like a normal (named) function but without the name. We can use the curly braces or the arrow:
onPressed: () => print('hola')) // short form, for only one expression function
onPressed: () {
// long form, for more than one expressions.
print('hola');
}),
For both examples, it's only the definition of a function, so it's not executed in the build method but it's executed in the onPressed event, so it's great for having it inside the widget arguments.
A constructor is a function inside a class, so, a method that is different of other methods because it is executed once when we instantiate an object based on that class.
We add a constructor by repeating the name of the class, the parentesis for arguments and body between curly braces. When inside the constructor, we can use the keyword "this" to refer to the class level.
We can pass any type of data as an argument, even a pointer to a function
class Person {
String name;
int age;
// the constructor
Person(String inputName, int age){
name = inputName; // we can use a different name for the attribute name
this.age = age; // or we can use (this. + the property name) to refer to the class property
}
}
void main(){
//when creating the object based on the Person class, we pass the arguments to the constructor.
p1 = Person('Eloi', 29);
}
We can have extra constructors by declaring a function with the functionName equal to the className followed by a dot and the name of the constructor:
Person.old(this.name){
age = 65; // this constructor will create a person with the age = 65
}
We could also use named arguments. To do so, we wrap all the arguments in the constructor with curly braces. Then, all theese arguments will be optional. Now, to call the constructor we'll write the argument name followed by a colon and the value. This concept of named arguments is also available for normal functions, not only constructors. We can use the @required annotation to ensure an argument is always required. If we don't provide it, the compiler will yield us.
class Person {
String name;
int age;
// with named arguments we can provide default values with the = sign. In case this is not provided, the default value will be used.
Person({@required String inputName, int age = 29 }){
name = inputName;
this.age = age;
}
}
void main(){
//when using named arguments, we can change the order.
p1 = Person(age: 29, inputName: 'Eloi');
}
There is a shortcut for getting the data as arguments and assigning it to variables in the class that allow us to get rid of the constructor body. To use it, we have to use the same name to arguments and class variables and target "this.+argumentName".
class Person {
String name;
int age;
// In this short constructor we are asigning the given values recived as arguments to the class variables. The names have to match
Person({this.name, this.age});
}
void main(){
p1 = Person(age: 29, name: 'Eloi');
}
It we want a class to only be available inside a file, we can declare it a private function by adding a _ before the name. Normally the State class of a statefulwidget will be private And the same for functions and variables, they will be private if they start by _
// Theese could not be reached from outside the file they are.
class _MyPrivateClass {
var _privateVariable
void _privateFunction() {
}
}
When we use the keywork "final" before the declaration of a variable, we are telling dart that the value of the variable won't change after the initialization. It can change everytime we initialize, but when initialized it can't change.
FINAL: Run time constant value
When we have a variable that has an asigned value that won't change anytime, we must indicate it by adding "const" before it. It's value is constant in run time and also in compile time.
CONST: Compile time constant value
Getters are a mixture between a property and a method. We define a getter by:
- define the type of value we want to get
- the "get" keyword
- the getter name
- the getter body between {curly braces}
String get resultPhrase {
String resultText;
if (resultScore >= 8){
resultText = 'you win!';
}else{
resultText = 'you loose!';
}
return resultText;
}
Lists is a type of data that consists on a group of items. In other languages are called arrays. It is defined in squared brackets and it's normally used to group related data.
// This is a list of Strings. We use the "\" character to scape the next character.
var names = ['Eloi', 'Marti', 'Gemma'];
To access an element in the list, we can use the builtin .elementAt(index) method or just indicate the index between []. IMPORTANT: The lists start at index 0.
questions.elementAt(0); // using the List.elementAt() method
questions[0]; // using the short form
To find how many items are in a list we use .lenght
print(questions.lenght); //prints 2
Dart also offers many methods (functions that belong to an object) on the List object (like every other value in Dart, lists are an object).
questions.add('Celeste'); // this adds 'Celeste' as a new element to the end of the list
questions.remove('Marti'); // this removes 'Marti' from the list, all other items would move and fill the gap
A map is a collection of key-value pairs. We create a map with {curly braces}. We define the key-value pairs separed by commas:
- The key. It hasn't to be a string but normally we use a string key.
- Then we use the colon
- And then we write the value, that can be any type of data: a string, number, a list, a map... or any object. We can access the value of a key-value refering the name of the map followed by the key in square brackets
//this is a map. The value for the key 'questionText' is a string and the value for the key 'answers' is a List of strings.
var question =
{
'questionText': 'What\s your favorite color',
'answers': ['Black', 'Red', 'White'],
}
var questionValue = question['questionText'];
- In a flutter project, the depeloper build an UI by adding widgets, the building blocks of the user interface.
- There are a lot of built in widgets, shipped with the flutter framework. There are widgets for everything: for text, for buttons, for images... You can configure all the widgets to change their appereance.
- We can also make our own custom widgets, grouping some others putting them somewhere in the screen.
A widget is a special type of object in flutter. To create a widget, we need to create a class based on special flutter class: StatelessWidget(). To do so, we need to import a file from the flutter package called 'material.dart' and then we define the widget using the keywork "extends" + "StatelessWidget" or "StatefulWidget". We also have to implement a special method (function) inside the class: the build() method. This method is called by flutter and takes an argument called "context" of type BuildContext, and it is provided by flutter. build() returns a Widget (which is also a class provided by the material.dart) Everything that belongs to a widget should go into the same class, so that the widget is a standalone unit. We can add functions to a widget class, we call it methods, but it's still a function.
import 'package:flutter/material.dart';
void main() {
//runApp is a special function provided by material.dart. It construct the widget and calls the build() method.
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(home: Text('hello world'),);//a named argument called 'home'
}
}
- There are Visible widgets, for Output & Input. For example the button, text, card... that are drown onto the screen and the user can see them.
- There are Invisible widgets that help us with Layout & Control for the structure of our app. For example Row, Column, ListView... There is the Contaner() widget, that by default is invisible but you can give some style and it could become visible.
Is a widget available thanks to having imported material.dart. It has the job of creating a base page designing, coloring and structure for the app. It takes some named arguments
- appBar: an AppBar() widget
- body: here we will all the widget tree of the page. It accepts only one widget, so we need to pass a widget with a children parameter (not child but children), for example the Column() widget, that takes a list of widgets in the children parameter.
Tip: the flutter extension will autoformat our code in a very readable way if we add a comma after closing parentesis.
In general, State is Data/information used in our app. We can differentiate between:
- App State, data that is used widely in the entire app, or in a large number of screens. For example authenticated users, loaded jobs, etc.
- Widget State, data that is used in one or few screens, for example: current user input, is a loading spinner being shown, etc.
Depending on if a widget needs to have a State that could change or not, we have 2 types of widgets:
- StatelessWidget: It doesn't have state.
- We provide Input Data to create it.
- It gets re-rendered when the external input data changes, but inside the widget class, the data will never change. Only receives new data from outside.
- StatefulWidget: It has State, It can change when its state changes
- We also provide input data to create it
- We can have an Internal State. The widget will be re-rendered when either the external input data or internal State changes.
Is a combination of two classes:
- The widget class: one that extends StatefulWidget. It will be rebuilt when the state changes.
- We should use the builtin method createState() returning the StateObject
- The classState: extends State. It will be persistent.
- In the State class wi add a to the widget class
- When we want flutter to run the build() method, we have to call SetState(). SetState takes a function, so in here we create an anonymous function and inside it we change the value of the data that the UI depends on.
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
var displayed_text = 'hello';
Widget build(BuildContext context) {
return MaterialApp(home: Scaffold(
appBar: AppBar(
title: Text('My First App'),
),
body: Column(
children: [
Text(text),
RaisedButton(
child: Text(displayed_text), // this text will change when the user presses the button as we are updating the displayed_text value inside setState and we are in a Stateful Widget.
onPressed: (
setState(){
(){
text = 'bye bye';
},
},
)
)
]
)
),);
}
}
Let's suppose we have a lists of maps for questions and answers of a quiz. We can programatically build widgets based on that lists on map. There is method called .map() (built in the any List object) that allow us to transform a list into something else. In this case, we can transform a list of maps into a list of widgets. The map() method executes a function on every element on the list where we are calling map. So we define an anonymous function inside .map(). This function automatically receives an argument that is the current element in the list. In the body of the function we have to return a new value.
- The map method returns an iterable, but we have to use another method .toList() to convert it to a list.
- Also, to not have a nested list but to have all items of the list in the parent list, we use a dart operator called spread operator. It is defined as three dots (...) and it's used before the list.
final _questions = [
{
'questionText': 'What\'s your favorite color?',
'answers': ['black', 'green', 'blue']
},
{
'questionText': 'What\'s your favorite aniaml?',
'answers': ['pig', 'rabbit', 'horse']
},
{
'questionText': 'What\'s your favorite fruit?',
'answers': ['apple', 'banana', 'orange']
},
];
list of answer() widgets: [
// we have to tell dart that the value for the 'answer' key is a list of String
...(questions[_questionIndex]['answers'] as List<String>).map((answer){
return Answer(answer);
}).toList()
]
The most primitive form of managing the State and sharing it betwen widgets is called "Lifting the State Up". It consists on having the State Data in the part of the widget tree that is common on all widgets where will be needed and pass the data as arguments in the constructors. So to have the State in the parent of all widgets that need it.
- To run the code conditionally we can use the if keyword followed by a condition in parentesis. Then, surrounded by curly braces will be the code that should run only if the condition is met, so if the condition returns true.
- We can combine it with the else if keyword, that will trigger some code if the condition of the previous if returns false and if it returns true to the new condition.
- And also can add an else block, to add code that would exectue if the previous ir or if else returns false.
- We can elaborate complex conditions by combining boolean operators
- == equal to
- < smaller than
- > bigger than
- <= smaller or equal to
- >= bigger or equal to
- != not equal to And also joining operators
- || or
- && and And grouping the operations with parentesis, as mathematical equations.
if(_questionIndex > 4){
//this code will execute when _questionIndex is bigger than 4
}else if(_questionIndex ==4){
//this code will execute when _questionIndex equal to 4.
}else{
//this code will execute when _questionIndex is smaller than 4.
}
There is a short form of using conditions in dart, and it has 4 pieces
- The condition
- Question mark (?)
- The code that should run if the condition returns true
- Colon (:)
- The code that should run if the condition returns false
(_question < 3)
? Text('The question index is smaller than 3')
: Text('The question index is greater or equal to 3')