In the last few decades, several different electronic platforms have emerged to aid our everyday life. Today, there are two main categories for typical usage: desktop and mobile platforms, with many different operating systems to run applications on.
Due to their long history, desktop operating systems (Windows, macOS, and Linux) have mostly stabilized. Meanwhile, mobile platforms are more dynamic. While Android and iOS are currently the two main operating systems, and some have come and gone (Symbian, Windows Phone), new ones are still in development, like Google's elusive Fuchsia or Huawei's HarmonyOS).
On the other hand, web platform technologies have grown to the point where standalone applications can be developed with them. Starting as a way to share information with other users through images, texts, and hyperlinks, the capabilities of websites gradually grew, thanks to browser-side code execution with Javascript and the browsers providing a way to reach different native interfaces.
Having so many different environments, one key factor before developing an application is determining the platform or platforms of development. This is further complicated by the fact that in today's society, one user typically owns multiple devices running different platforms, where a seamless cross-platform experience is a must. For these reasons - among others, like cost-cutting - cross-platform development is a viable alternative to native development for each platform individually.
Developing an application for multiple different platforms can be achieved in several ways:
-
Native: Developing an application for every supported operating system with its native development framework. While performance-wise, this is the best way, and every platform-specific resource is available to us as-is, but development costs are high due to limited code sharing. In-depth testing must also be done for each platform individually.
-
Responsive website: Since most modern web browsers are available for consumer devices, creating a responsive website can be a reasonable alternative to native development. Responsive websites can accommodate different screen sizes, making them accessible for both desktop and mobile users. Depending on the browser's supported features, web applications can query the device's geographical location, provide offline support and push notifications, record audio and video, or even manage Bluetooth connections, just to mention a few capabilities. (for a detailed list, see https://whatwebcando.today/). If a web application conforms to certain requirements set by the browser, it is considered to be a Progressive Web Application (PWA). Such PWAs can be installed and seen as a standalone application for the user on Android, iOS, and desktop.
-
Hybrid: While browsers expose many useful interfaces for websites, several features are still not available (or are in experimental stages), such as NFC access or shape detection. A platform-specific native application (or at least a part of it) is necessary to access such features. To circumvent the need for a native application, an alternative is to package and ship our web application with a custom browser - that exposes these native interfaces - as a standalone application (similar to how Java applications can be packaged with a JVM implementation into a single executable application). Electron is a framework that does precisely this, powering many applications on the market nowadays, including Facebook Messenger, Slack, Twitch, and Discord on desktop platforms.
-
Native cross-platform: Frameworks such as React Native, NativeScript and Xamarin Native provide tools to write applications using a single code base in one programming language, while also allowing the use of platform-specific native UI components. Applications built this way may be executed with the help of some kind of virtual machine (like Mono for Xamarin) or may be compiled to native machine instructions.
Every approach comes with its advantages and disadvantages, which must be considered at the start of development to determine the best course for a project.
Flutter is an open-source, cross-platform development framework maintained and owned by Google. It is relatively new, as the first preview version was released in 2015, while the first production-ready stable version became available in 2018. The main goals of Flutter are the following:
- Compile native applications for mobile, web, and desktop platforms from a single codebase.
- Provide fast development cycles with the help of stateful hot reload.
- Support native-looking UI components with extensive customizability.
- Native-like performance in applications (60Hz rendering).
Flutter supports the following platforms as of now:
- Android: Stable, since initial release
- iOS: Stable, since initial release
- Web: Stable, since Flutter 2.0
- Windows: Stable, since 2.10
- macOS, Linux: Stable, since Flutter 3.0
The development team has chosen the Dart language to develop the framework, as well as writing applications with. Dart is also owned by Google. Starting as a compiled language running on the Dart VM with the intent to replace JavaScript in web browsers, they failed to convince Chrome's development team (a browser also owned by Google) to include a Dart VM in their application, and so refocused their efforts into providing a JavaScript transpiling tool. While Google uses Dart for their projects, it failed to gain wide-spread popularity, staying mainly as a niche language until Flutter was released.
Flutter is a hybrid cross-platform framework, where instead of a website, we create our application with Dart. Native cross-platform frameworks usually run into the 'lowest common denominator' problem, wherein the same UI component of different platforms might have different properties, such as different animation styles, different shapes, and different functionalities. The cross-platform library can only expose the common properties with ease. Subtle differences between native components and behaviors can easily break an app in mysterious ways, and debugging them also becomes more challenging due to the added communication layer.
To combat these, the Flutter team decided to stay away from native UI components. Instead, they have re-created every platform-specific UI component in Flutter. These components are rendered by the Skia (or by the new Impeller engine) on native canvases. This way, Flutter can support many platforms with less effort. The only requirements are
- a way to draw pixels on the screen (which is usually available through a native Canvas component),
- a way to handle raw user inputs (such as a touch screen or mouse events),
- and a way to run machine code compiled from C++ (available on most platforms).
A Flutter application will generally run the same way on every supported platform, and the styling (UI and UX) is up to the developer. For example, an application created with Material Design in mind will look and feel the same on iOS (which is not necessarily a good thing, but more on that later).
To achieve this, Flutter consist of 3 main parts:
- the framework, where the UI components and other application functionalities are implemented,
- the engine running our application,
- and the platform-specific embedder.
The following figure gives an overview of Flutter's architecture.
With most cross-platform frameworks, there is a tradeoff between fast compilation and the performance of an application created with it. To achieve hot-reload (where changes in the source code can be almost instantly seen in the running application), frameworks only analyze the source code for correctness and do small processing tasks, which will be then run directly on the device. In turn, to be performant, the source code is compiled with optimizations enabled, which increases compilation time, but has runtime benefits.
With this in mind, Flutter provides two separate build processes:
- In debug mode, an analyzer is run to ensure that the source code is syntactically correct. The source files are packaged with the Dart VM. Starting the application will load the source from this package into the memory. While running, triggering a hot reload (usually by saving source code files) will replace these source files in the memory, changing the behavior of the program. Restarting the application will load the source files again. This allows fast development cycles at the cost of low performance.
- In release mode, Dart code is compiled into native machine instructions while also linking some runtime libraries (such as the garbage collector). While the compilation time is longer than in debug mode, the resulting applications will be smaller and faster. Running them on emulators is not officially supported (however, Android x86_64 emulators seem to be working as of now).
- There is also profile mode, which is similar to release, except for a few debugging utilities being enabled to benchmark the application's performance.
Dart is a modern, object-oriented, class-based, statically typed (since 2.0), compiled, garbage collected language with a syntax somewhat similar to Java. Let's look at some examples to understand how Dart works. For every example, a DartPad link is provided, which opens the sample code in the browser in a Dart environment.
DartPad is a great way to try out Dart language features, and it can even be used for quick Dart and Flutter prototyping. However, it is not a full-featured IDE, although it has code completion to help us.
void main() {
print("Hello World!");
}
Like many other languages, every Dart application starts by calling a globally defined main
function. We can already see a few properties of the language here. void
means that the function does not return a value, while the parenthesis after the function name indicates that the main
function does not take any parameters. Optionally, we could declare a parameter with the type of a list of strings to accept command line parameters.
The first thing we, as developers, will probably do is declare variables. Since Dart 2.0, the Dart language is type-safe: static type checking and runtime checks are used to ensure that a variable's runtime type matches its static type. This is also referred to as sound typing. While types are mandatory for variables, type annotations are usually optional due to Dart having type interference.
Since Dart 2.10, Dart also features sound null safety - a feature some modern languages, like Kotlin, was designed with from early on. Due to Dart's history, this is a significant milestone for the language. Like with Kotlin, variable types and function return types must be annotated to indicate whether they can store or return a null value or not.
With this in mind, let's take a look at different variable declarations.
/// Global variable
bool globalFlag = false;
void main(List<String> args) {
/// Declaration with initialization
String myString = "My string";
print("String interpolation: $myString");
/// Declaration without initialization
String tempString;
/// print(tempString.length); //!ERROR! Non-null value not initialized
if (globalFlag){
tempString = "True";
} else {
tempString = "False";
}
print("String after initialization: $tempString");
/// Object initialization
Duration myDuration = Duration();
print(myDuration);
String? myNullString;
print("Null coalescing: ${myNullString ?? "Null variable!"}");
/// print(myNullString.length); !ERROR! Function call on nullable variable
print("Null aware property access: ${myNullString?.length}");
var myAutoVar = "Automatic type";
final myFinalVariable = "Hello";
final myOtherFinalVariable = myAutoVar;
const myConstVariable = "World!";
/// const myOtherConstVariable = myAutoVar; //!ERROR! Const value must be initialized with a constant value
}
A variable can be declared globally, locally in a function, or as a member of a class, while the initialization can happen at the declaration or a later point for local variables. If the variable is initialized as it is declared, the type can be inferred from the value, in which case the var keyword can be used instead of the type annotation.
Dart provides many built-in types, some of the most important being:
- int
- double
- num: a supertype of int and double
- bool
- String
- Collections, such as List, Set, and Map
A final variable can only be set once at initialization. A const "variable" is a constant expression that will be evaluated at compilation time (thus such "variables" and objects are called compile-time constants). If a constant object is created, the parameters in its constructor must also be constants. In this case, two calls to the constructor with the same parameters will 'return' the same object.
If a variable is not explicitly initialized before its first use, it will contain the null value by default. In this case, the variable must be declared as a nullable type, while non-nullable variables must be initialized before use.
In case we surely know that a variable will be initialized before its first use, we can create a non-nullable variable with the added keyword late
(similarly to Kotlin's lateinit
to help the compiler understand our intentions better, as shown in the following example:
bool test1(){
print("Test1 function");
return true;
}
bool test2(){
print("Test2 function");
return true;
}
late int testVariable;
void main(List<String> args) {
/// print("Late variable without initilaization: $testVariable"); //!RUNTIME ERROR! LateInitializationError
testVariable = 5;
print("Late variable after initialization: $testVariable");
bool flag1 = test1();
late bool flag2 = test2();
print("Flag1 value: $flag1");
print("Flag2 value: $flag2");
}
Using a late
variable before initialization will result in a LateInitializationError
being thrown. Annotating a local variable with the late
keyword will make it lazily initialized (meaning that the initialization will only happen the first time the value is read). Note that globally declared variables are inherently lazy-initialized whether they are late
or not.
Two special types are Null
and dynamic
:
void main() {
/// Null value
int? nullInt;
print("Null values");
print(nullInt.runtimeType);
print(nullInt.hashCode);
print(nullInt.toString());
/// Dynamic type
dynamic testDynamic;
testDynamic = 5;
print(testDynamic.runtimeType);
testDynamic = "Hello Dynamic!";
print(testDynamic.runtimeType);
/// testDynamic.gcd(3); //!ERROR!
}
Null
is a special type, with the only instance of it being null
. Because null
is an object, it has the runtimeType
and hashCode
properties, and a toString
function implementation, which we can use freely.
The dynamic
declaration tells the compiler to skip type checks for the given variable. Because of this, a dynamic
variable can contain any value, and any function can be called on it (essentially reverting Dart to dynamic typing, like in JavaScript). Runtime type checks are still used. Assigning a dynamic variable to a statically typed variable will result in a runtime exception if the assignment is not possible.
Besides variables, function declaration and usage are also essential parts of modern programming languages. Dart's function declaration and calling syntax is similar to Java's. The following code sample contains every important aspect of functions in Dart:
void mySimpleFunction(){
print("Inside function.");
return;
}
int calculateSomeValue(int a, int b) => a + b;
void namedParametersFunction({required String name, String? description}){
print("$name: $description");
}
positionalParametersFunction(int a, [int? b]) => a + (b ?? 0);
lambdaParameterFunction (void Function (int a) lambda){
lambda(10);
}
void main() {
mySimpleFunction();
print(calculateSomeValue(3,5));
namedParametersFunction(name: "Dani");
namedParametersFunction(description: "Hello", name: "Dani");
print(positionalParametersFunction(3));
print(positionalParametersFunction(3, 4));
void myLocalFunction(){
print("Function inside function!");
return;
}
myLocalFunction();
var myLambdaExpression = (int a, int b) => print(a+b);
myLambdaExpression(1,1);
var myLambdaDetailedExpression = (){
print("Inside lambda expression");
return 1;
};
print(myLambdaDetailedExpression());
print(myLambdaDetailedExpression.runtimeType);
lambdaParameterFunction((int a){
print(a);
});
}
A function declaration usually starts with the definition of the type of the returned value. If the function doesn't return any value, void can be used, or the type declaration can be omitted. Additionally, we can use the Never
type as an indicator for a function that never returns normally, either by throwing an exception or running in an infinite loop (this is analogous to Swift's Never
and Koltin's Nothing
types).
If the return type is non-nullable, a value must be returned from the function. If the return type is nullable, the return
statement can be omitted, in which case the null
value will be returned implicitly.
The type declaration is followed by the name and the input parameters of the function. Dart supports higher-order functions, meaning that functions themselves can be used as values, and they can be passed to other functions as parameters (think about function pointers in C, if you've never met higher-order functions before; otherwise, the concept is the same as in Kotlin, Swift, or even JavaScript). Because of this, functions have types too. The type declaration of such a function has the following format:
<return type> Function(<parameters>) <variableName>
Dart has four different function parameter types:
- Required positional parameters: These parameters must be passed to the function. The parameters are identified by the position in which they are passed in.
- Optional positional parameters: These parameters are optional. They follow the required positional parameter(s) in the parameter list of a function. If a value is not provided, an optional compile-time constant can be used as a default value; otherwise,
null
will be the default value. They can be declared between square brackets []. - Optional named parameters: These parameters are also optional. They have to be passed to the function with the name of the parameter followed by the provided value. Because of this, the order of such parameters is irrelevant (much like Kotlin's named arguments, or Swift's argument labels. If a value is not provided, an optional compile-time constant can be used as a default value; otherwise,
null
will be the default value. They can be declared between curly brackets {}. - Required named parameters: Similar to optional named parameters, with the
required
keyword inserted before the parameter declaration. A value must be given for these parameters.
A verbose function body goes between brackets {} after the parameter list. If the function only consists of one statement, it can be shortened with the => (expression body) syntax, in which case the return
keyword is also omitted.
Lambda functions (sometimes also referred to as anonymous functions) can be created similarly, except that the return type and function name must be skipped.
Like many other programming languages, Dart follows the usual conventions in regard to the different control structures,
such as if-else
for conditional execution, while
, do-while
and for
loops, switch
cases, as can be seen in the
following example:
void main() {
bool myFlag = true;
if (myFlag){
print("True");
} else if (!myFlag) {
print("False");
} else {
print("Null");
}
for (int i = 0; i < 4; i++){
print(i);
}
for (var j = ""; j.length < 4; j += "a"){
print(j);
}
var j = 0;
while(j < 6){
j++;
if (j == 3)
continue;
print(j);
}
do{
print("Do-while");
break;
} while(true);
var myList = [3, 4, 7];
for (var value in myList){
print(value);
}
dynamic mySwitchVariable = 'Hello';
switch (mySwitchVariable) {
case 'Hello':
print("First");
break;
case 0:
case 'World':
print("Second");
break;
case null:
print("Null");
break;
/*case 0.0: //!ERROR!
print("Zero");
break;*/
/*case const Duration(0): //!ERROR!
print("Zero seconds");
break;*/
default:
print("Unknown");
}
String? nullVariable;
try{
nullVariable!.substring(0);
} on NoSuchMethodError catch(e){
print("Error caught! : $e");
} finally {
print("Finally called!");
}
}
Besides the usual structures, there are some interesting bits. Dart supports for
loops on iterable variables (usually called for-each
loops).
In a switch
, a case
expression can only be a constant expression with one of the following three types:
- instance of int
- instance of String
- instance of class C which does not override the == operator.
Also, note that in Dart, the catch
differs slightly from other languages: we use the on
keyword to specify which values to catch and the catch
keyword to assign it to a variable used later.
Only one of them is required: If the on
part is missing, every value thrown will be caught, while the catch
can be skipped if we don't plan on using the value.
Dart has 3 main collection types: List, Map, and Set. Since Dart supports generic parameters, every collection can be constrained with the type of values it can hold. Some of the most common operations can be seen in the following example: DartPad
void main() {
var myList = [];
myList.add(5);
myList.add("Hello");
myList.add(Duration());
print(myList);
print(myList[1]);
var myTypedList = <String>[];
myTypedList.add("Hello");
myTypedList.add("World!");
//myTypedList.add(3); !ERROR!
var myAutomaticList = [
"World!",
5,
null
];
print(myAutomaticList.runtimeType);
var myIntList = [3, 5, 6, 18, 2];
print(myIntList.length);
print(myIntList.firstWhere((it) => it.isEven));
print(myIntList.where((it) => it.isEven));
//myIntList = myIntList.where((it) => true); !ERROR!
myIntList.where((it){
print("Inside where with: $it");
return it.isEven;
}).map((it){
print("Inside map with: $it");
return it.toString();
}).firstWhere((it){
print("Inside firstWhere with: $it");
return it.length > 1;
});
print(myIntList.indexOf(8));
//IMPORTANT! Double point notation returns the same object instance
print(myIntList..retainWhere((it) => it.isEven));
print(myIntList..sort((e1, e2) => e1.compareTo(e2)));
//print(myIntList[5]); !ERROR!
var myMap = {
"test1" : 2,
"test2" : 3,
null : 2
};
print(myMap.runtimeType);
print(myMap.keys);
print(myMap.values);
print(myMap.entries);
var mySet = {1, 4, 6, 5, 4, 2, 1, 3};
print(mySet);
mySet.add(6);
print(mySet);
print(mySet.difference({4, 6, 9}));
print(mySet.union({4, 6, 9}));
print(mySet.intersection({4, 6, 9}));
var whatIsThis = {};
print(whatIsThis.runtimeType);
}
Lists are an indexable collection of objects with a length. They can be created in the following ways:
- With the list creation literal: []. We can initialize the list with values inside the brackets.
- With the default constructor:
List([int? length])
. If a length is given, the list is filled with alength
number ofnull
values. It cannot be used if the values stored in the list are non-nullable. List.filled(int length, E fill, {bool growable: false})
: Creates a pre-filled list with the given value. Ifgrowable
is false, adding to the list or removing from it will cause an exception. Note that the filled value is shared between the elements. For example, if an empty list is passed to the parameter, the list will be filled with the same list instance, and modifying one will modify every other. In these cases, usegenerate
instead.List.generate(int length, E generator(int index), {bool growable: true})
: Similar tofilled
, but a lambda function is used to generate a new value for every position.List.empty ({bool growable: false})
: Similiar tofilled
, but creates an empty list.List.from(Iterable elements, {bool growable: true})
andList.of(Iterable\<E\> elements, {bool growable: true})
: Creates a list from an iterable of a given type. The latter one is type-aware.List.unmodifiable(Iterable elements)
: Creates an unmodifiable list. Changing any element of the list, adding and removing will throw an exception.
Every collection implements the Iterable
interface, which contains several useful utility functions (a comprehensive list can be found here. These are usually lazily evaluated. For example, if we were to use a map
function and a firstWhere
function to search for the first occurrence of a given value, the callback of the map
function would only be called until the value is not found.
Some List
operations are executed in-place, such as retainWhere
and sort
, so the return type of these functions is void. If we want to chain these operations, we can use the .. operator, which executes the given function but returns the object instance (this).
Maps are a collection of key-value pairs. A key may only be associated with one value, but multiple keys can be associated with the same value. Maps can be created in the following ways:
- The map creation literal: {}. We can initialize the map with key-value pairs inside the brackets.
- The default
Map()
constructor. Map.from(Map other)
andMap.of (Map<K, V> other)
: Creates a map from another one. The latter one is type-aware.Map.fromEntries(Iterable<MapEntry<K, V>> entries)
,Map.fromIterable(Iterable, {K key(dynamic element), V value(dynamic element)})
,Map.fromIterables(Iterable\<K\> key, Iterable\<V\> values)
: Creates a map from iterables.Map.unmodifiable(Map other)
: Creates an unmodifiable map from another map. Changing any element, adding and removing an element will throw an exception.- Map.identity(): Ignores the overriden == and hashCode implementations, using instead the identity relations for the key parameter.
Looking up a key not present in the Map will return null. If null is also a valid value for a given key, containsKey can be used to lookup whether a key is present or not.
A set is a collection of objects in which each object can only occur once, so an object is either contained by the set or not. Sets can be created in the following ways:
- The set creation literal: {}. We can initialize the set with values inside the brackets. Note that if it is ambiguous, the {} literal creates a map by default.
Set.from(Iterable elements)
andSet.of(Iterable\<E\> elements)
: Creates a set from an iterable. The latter is type-aware.Set.identity()
: Ignores the overriden==
andhashCode
implementations, using instead the identity relations for object comparison.
Because Flutter relies heavily on lists, the developers of Dart extended the way lists (and other collections) can be created from the literals. This is shown in the following example: DartPad
import 'dart:math';
int testFunction(){
return 4;
}
void main() {
var myFirstList = [3, 5, 9];
var mySecondList = [1, 2, 4, 5];
print(myFirstList + mySecondList);
myFirstList.addAll(mySecondList);
print(myFirstList);
var itemFlag = true;
var myDetailedList = [
1,
itemFlag ? 2 : 9,
if (itemFlag)
3,
testFunction(),
for(int j = 5; j < 6; j++)
j,
...myFirstList,
...mySecondList.map((it) => it + 1),
for (int j = 11; j < 13; j++)
...[
j,
j * 2,
],
for (var item in mySecondList)
(){
for (int i = 0; i < 10; i++){
if (item == pow(2, i))
return 0;
}
return item;
}(),
];
print(myDetailedList);
//var myGenerator = ...[3, 2, 1]; !ERROR!
}
Like in other languages, the ternary operator and function calls can be used since they always evaluate to a value.
Expanding on this, an if-else
structure can also be used to create more complex value assignments or to skip a value altogether. Note that only one expression can be placed after a condition. Also important is that the comma must only be placed after the last part of the if-else
structure.
If we have an iterable, and we want every value inside that iterable to be placed inside our list, we can use the ...
operator. Note that this expression is only available inside a collection creation literal.
We can also use a for loop to create a number of values in a loop. Just like with the if-else
structure, only one expression may be placed in the loop.
For both of these cases, we can overcome the one expression limitation with the combination of the ...
operator and lambda functions. If we want to add multiple simple values (useful with single if
cases), we create another list of values and use the ...
to add these to our original list. If we need to do something more complex, we can move the code out to a function or create a lambda function and call them. If these return a list of values, we can again use the ...
operator to add them to the original list.
Classes in Dart are similar to classes from other object-oriented programming languages. extend
can be used to create a subclass. Dart has no notion of interfaces. Instead, one can declare abstract
classes, which cannot be instantiated. A class can implement
any number of abstract and normal classes. Every class implicitly defines an interface containing all the instance members of the class and any interfaces it implements.
With these in mind, let us take a look at some of the features a class can have in Dart:
DartPad
class PointClass{
double? x;
double? y;
PointClass(double x, double y){
this.x = x;
this.y = y;
}
@override
String toString(){
return "PointClass(x: $x, y: $y)";
}
}
class ConstPointClass{
final double x;
final double y;
const ConstPointClass(double x, double y) : this.x = x, this.y = y;
@override
String toString(){
return "ConstPointClass(x: $x, y: $y)";
}
}
class VectorClass{
double x;
double y;
VectorClass(this.x, this.y);
VectorClass.zero() : this(0, 0);
static final List<VectorClass> _poolList = [];
factory VectorClass.pooled(){
if (_poolList.isNotEmpty){
return _poolList.removeLast()
..x = 0
..y = 0;
} else {
return VectorClass.zero();
}
}
void recycle(){
_poolList.add(this);
}
@override
String toString(){
return "VectorClass(x: $x, y: $y)";
}
@override
int get hashCode => x.hashCode ^ y.hashCode;
@override
operator ==(dynamic other) => other is VectorClass && other.x == x && other.y == y;
operator +(VectorClass other) => VectorClass(x + other.x, y + other.y);
}
enum Colors{
RED, BLUE, GREEN
}
abstract class Listenable{
void addListener(void Function(dynamic));
void notifyListeners();
}
class ListenableInt extends Listenable{
int value;
ListenableInt(this.value);
@override
void addListener(void Function(dynamic) Function) {
// TODO: implement addListener
}
@override
void notifyListeners() {
// TODO: implement notifyListeners
}
}
extension SafeListGetter<T> on List<T>?{
T? getOrNull(int index) => this != null && index < this!.length ? this![index] : null;
}
void main(){
var point1 = PointClass(1, 2);
var point2 = PointClass(1, 2);
print(point1);
print(point1 == point2);
var constPoint1 = const ConstPointClass(1, 2);
var constPoint2 = const ConstPointClass(1, 2);
print(constPoint1);
print(constPoint1 == constPoint2);
var vector1 = VectorClass(1, 2);
var vector2 = VectorClass(1, 2);
print(vector1);
print(vector1 == vector2);
print(vector1 + vector2);
vector1 += vector2;
var color = Colors.BLUE;
print(color.index);
print(color);
List<int?>? myList;
print(myList.getOrNull(4));
}
Class members can be initialized in the following ways:
- With a constant value at the variable declaration.
- After a constructor declaration.
- Within the constructor parameters with the
this
keyword, where the member name matches the parameter name. Note that the statements inside a constructor body do not count as initialization. When the execution reaches these statements, every member variable must be either explicitly initialized, or implicitly initialized to null if they are nullable.
Dart differentiates between two main types of constructors: generative and factory constructors. Generative constructors always return a new instance of the object, while factory constructors may return a previously constructed object. A class may only have one default constructor declared, while any number of named constructors are allowed.
A constructor can also be a constant constructor, which is denoted by the const
keyword. This is only allowed if every member property of the class is also final, and the constructor doesn't have a body.
Functions are implicitly overridden when a function with the same name is declared in Dart. The @override
annotation helps the compiler detect situations where a function doesn't override a superclass member or function. Operator overloading is also possible. A comprehensive list of available operators can be found here. Enums
are also supported similarly to other languages.
A relatively new feature of Dart is class extension
functions. These functions are statically declared functions that add some functionality to an already defined class and can be called like a class function. Note that these functions are not actually part of the class: they are not part of the class's interface and cannot be called if the compiler is not aware of the type of the variable (for example, on a dynamic variable).
While in this course we will showcase the several target platforms of Flutter, our main platform will be Android due to the level of support it receives, and the availability of tools such as Android Studio and the Android emulator. To develop for the Android platform, installing Android Studio is highly recommended, as it comes with the latest Android SDK and emulator. This can be done by either downloading it directly from the website or through the Jetbrains Toolbox app. Note that the Android SDK will be installed inside the current User directory by default, which must not contain spaces or any other special characters. It is also possible to install just the Android SDK, but we will also use Android Studio for application development in this course.
For the Flutter SDK, we generally recommend only using the latest stable release for development. At the time of this writing, the latest major Flutter SDK version is 3.16.0, which can be downloaded from here. The zip file should be extracted to a folder which any program can freely access. We also recommend adding the Flutter SDK directory to the Path environment variable as described here
To develop with Android Studio, a Dart and a Flutter plugin must be installed. This can be reached from the Configure->Plugins menu item (or from File->Settings->Editor->Plugins if a project is opened). Search for and install first the Dart plugin, then the Flutter plugin. After restarting the IDE, the Start a new Flutter project should be available from the menu. This allows us to create a new Flutter application, in which we will have to specify the previously installed Flutter SDK path. If everything is installed correctly, a sample project should be generated, which can be installed on the emulator.
Visual Studio Code is another officially supported IDE for Flutter. In this case, the Dart and Flutter extensions must be installed, which can be reached with the Ctrl-Shift-X hotkey. After installing it, we can bring up the Command Palette with the Ctrl-Shift-P hotkey, which will contain an option to Create a new Flutter application. The editor automatically detects the path of the SDK from the PATH variable (which must be added in this case). Pressing F5 will start the sample project on a supported device.
In this lecture, we have learned the history behind Flutter, a hybrid cross-platform framework maintained by Google and its programming language, Dart. The basics of Dart were also introduced. Note that this was not a comprehensive list: features such as asynchronous programming and mixins will be presented in later lectures, while others will be skipped in this course (such as reflection, which is not supported in Flutter). In the next lesson, we will examine how a Flutter project is structured and introduce the basic components of a Flutter application.