Fluttering Dart
Fluttering Dart: Functions
How to write, use and abuse functions in Dart

Flutter projects can use both platform-specific and cross-platform code. The latter is written in Dart, and, for building Flutter apps, some basic knowledge of Dart is required.
Fluttering Dart’s goal is to explore fundamental knowledge and unveil tips & tricks of the powerful programming language that brings Flutter to life.
In the first part of the series, we went through the Dart built-in data types.
In the second part, we’ll discover functions.
Some of the code examples can be tried out, and played with, using DartPad.
Functions
A function (also might be referenced to as method in the context of an object) is a subset of an algorithm that is logically separated and reusable. It can return nothing (void) or return either a built-in data type or a custom data type. It can also have no parameters or any number of parameters.
In Dart, a true object-orientated programming language, functions are also objects, and their type is, you guessed it, Function.
This has some implications: we can assign functions to variables or pass them as arguments for other functions.
A simple function example, named toggle, can be seen below:
bool toggle(bool value) {
// returns the opposite
return !value;
}We can omit the types and the function will work the same (although the recommendation is to use type annotation):
toggle(value) {
// also returns the opposite
return !value;
}For functions that contain just one expression, we can use a shorthand syntax (also called the arrow syntax):
toggle(value) => !value;
Functions can have two types of parameters: required and optional. The required parameters are taking the front row, followed by any optional parameters.
Optional parameters
These can be either named or positional:
- Named parameters
When calling a function, we can specify named parameters using paramName: value. For example:
toggleSound(to: true, notifyUser:false);When defining a function, use {param1, param2, ...} to specify named parameters:
void toggleSound({bool to, bool notifyUser}) {
// code that (optionally) toggles the sound
// and (optionally) notifies the user
}Named parameters are optional by default. In order to make them mandatory, we have to annotate them with @required (this won’t work in DartPad):
import 'package:meta/meta.dart';void toggleSound({@required bool to, bool notifyUser}) {
// code that toggles the sound
// and (optionally) notifies the user
}- Positional parameters
If we wrap a set of function parameters in [], we mark them as optional positional parameters. The above example becomes:
void toggleSound(bool to, [bool notifyUser]) {
// code that toggles the sound
// and (optionally) notifies the user
}We can set compile-time constants as default values for both named or positional parameters. If no value is provided then the parameters will be null.
Let’s say that we want to set the default value of notifyUser to false in the above example. This will look like:
void toggleSound({bool to, bool notifyUser=false}) {
// code that (optionally) toggles the sound
// and (by default) doesn't notify the user
}We should note that only optional parameters can have default values.
main()
Every app that we build must have an entry point, and that role is served by the mandatory top-level main() function. This function returns void and has an optional List<String> parameter for arguments, as we can see below:
// a web app
void main() {
// web app code goes here
}or
// a command line app
void main(List<String> arguments) {
print(arguments);
}First-class objects
As mentioned in the beginning, we can pass functions as parameters to other functions and we can assign them to variables:
Function _toggleSound = (SoundSource source, [bool to=false, bool notifyUser=false]) {
// code that toggles the sound of a SoundSource object
// by default mutes it without notification
}List<SoundSource> soundSources = <SoundSource>[source1, source2, source3];// we're passing the _toggleSound Function variable as
// parameter to the forEach method of the soundSources List object
soundSources.forEach(_toggleSound);Anonymous functions
Although most of the functions that we’ll create are and should be named, we have the option of creating nameless functions. These functions are called anonymous, lambda or closure functions.
In the previous code sample, we’ve defined such function and we’ve assigned it to the _toggleSound Function variable.
Lexical scope and closures
Dart is a lexically scoped language, which means that the scope of variables is determined statically, simply by the layout of the code. We can “follow the curly braces outwards” to see if a variable is in scope.
In the game example bellow, the enemy String variable will be available, or be in scope, for all nested functions unless we decide to override the value by declaring a variable with an identical name (the new variable will be available on that scope level only). We did that in the firstLevel function. After the scope ends the variable will cease to exist, and the upper level declared variable will be available again.
void main() {
String enemy = '';
void game() {
void init() {
enemy = 'boss';
}
void firstLevel() {
String enemy = 'miniboss';
print(enemy);
}
void endGame() {
print(enemy);
}
init();
firstLevel();
endGame();
}
game();
}Closures are function objects that have access to variables in their scope, even when used outside of their initial scope.
Functions can close over variables defined in surrounding scopes. In the following example, customiseGreeting captures the variable greeting. Wherever the returned function goes, it remembers greeting.
Function customiseGreeting(String greeting) {
return (String name) => '$greeting, $name,';
}void main() {
var goodMorning = customiseGreeting('Good morning');
var goodEvening = customiseGreeting('Good evening'); print(goodMorning('Monica'));
print(goodEvening('Monica'));
}Typedefs
In Dart, functions are objects (instances of type Function). The actual type is in fact a result of its signature: parameters type + return type.
What matters is the actual function type when a function is used as a parameter or return value. typedef in Dart allows us to create an alias of a function type. The type alias can be used just like other types.
typedef Processor<T> = T Function(T value);
typedef Printer<T> = void Function(T value);Function customiseGreeting(String greeting) {
return (String name) => '$greeting, $name,';
}void greet<T>(List<T> list, Processor<T> processor, Printer<T> printer) {
list.forEach((item) {
printer(processor(item));
});
}void main() {
Function goodMorning = customiseGreeting('Good morning');
greet(['Monica', 'cats', 'Collin'], goodMorning, print);
}In the above example, we have the greet function that gets a list of names and greets everyone in that List. It does that using a Processor and a Printer typedefs. The processor is the function customiseGreeting we used in the previous example. The printer is the actual print function.
Exceptions
Often we call functions inside other functions and, sometimes, those functions fail.
Dart has an exception mechanism similar to what Java has, with the difference that all exceptions in Dart are unchecked (functions are not declaring exceptions they may throw). This means that we are allowed to skip catching the exceptions. This freedom can damage overall application performance and reliability and robust apps should handle failures properly.
To report failures we should use the throw keyword. Although all non-null objects can be thrown, it is recommended that we throw only objects of types Error and Exception.
An Error object represents a bug in our code. Errors are not meant to be caught and usually, they (should) lead to program termination.
An Exception object represents an unsuccessful turn of events in the code. Compared to errors, they are meant to be caught and dealt with. They usually carry useful information. We should create custom data types that extend from Exception to encapsulate useful data.
After an exception is thrown we can catch and stop it from propagating. The main goal is to handle it (we shouldn’t catch exceptions just for fun).
Exceptions are caught using the try, on and catch keywords. catch is used when we want to access the exception object and the stack trace, and on when we don’t care about all that.
To re-throw an exception, we should use the rethrow keyword. This can come in handy when we want to partially deal with an exception (not a good practice) or need it further up the stack.
Finally, we have the finally keyword that allows us to run code at the end, no matter of the exceptions we got on the way.
Applying all that on the previous example, and making sure we cause an exception, we’d get:
void main() {
Function goodMorning;
try {
goodMorning('test');
} on NoSuchMethodError catch(e) {
print('our attempt to greet failed: ${e.runtimeType}');
goodMorning = customiseGreeting('Good morning');
} finally {
greet(['Monica', 'cats', 'Collin'], goodMorning, print);
}
}Dart functions are flexible and very powerful.
You can take look at this nice example of using top-level functions:
We’ve just gone over the basics when it comes to functions. To get a hold on all things involved some time and practice are required.
In the next part of the Fluttering Dart series, we’ll delve into operators, another Dart fundamental needed for building robust Flutter apps.
Tha(nk|t’)s all!
