Sitemap
YavarTechWorks

Today a Reader, Tomorrow a Leader

Flutter — Developer — Part-1

--

Flutter: Complete Roadmap Series

Press enter or click to view image in full size

Flutter is a powerful open-source UI software development toolkit backed by Google. It allows developers to build beautiful and high-performance applications for mobile, web, and desktop platforms using a single codebase. Flutter’s hot reload feature enables fast and efficient development, making it a favorite among developers.

With a rich collection of built-in widgets, responsive layouts, and support for state management, Flutter empowers developers to create stunning and feature-rich user interfaces. Its growing ecosystem of packages provides easy integration with various functionalities. Whether you are a beginner or an experienced developer, Flutter’s simplicity and versatility make it an ideal choice for building cross-platform applications.

Okay, Ley’s Start with Dart…

Dart Basics:

Press enter or click to view image in full size

Dart is an open-source, general-purpose, object-oriented programming language with C-style syntax developed by Google in 2011. The purpose of Dart programming is to create frontend user interfaces for the web and mobile apps. It can also be used to build server and desktop applications.

DartPad:

DartPad is an online tool that allows developers to write and run Dart code snippets. It can also be used to run Flutter code, making it a convenient way to try out Flutter apps and widgets without having to set up a full development environment.

Press enter or click to view image in full size

Variables:

In Flutter, variables are used to store values. There are two types of variables in Flutter:

  • local variables: These are declared within a function and are only accessible within that function
  • Instance variables: They are declared within a class and are accessible throughout the entire class.

Variables in Flutter can store values of different data types, such as numbers, strings, booleans, and more.

var personName = 'John Doe';
var personAge = 25;

print(personName); // we get John Doe
print(personAge); // we get 25

Build-In Types:

There are several built-in data types, including:

  • int: used to store integers
  • double: used to store floating-point numbers
  • String: used to store text
  • bool: used to store true or false values
  • List: used to store ordered collections of objects
  • Map: used to store unordered collections of key-value pairs

Additionally, there are other complex data types like dynamic, var, and Object in the Dart programming language, which is used in Flutter.

int: a numeric value without a fractional part.

int age = 21;
print(age);// we get 21

double: Any 64-bit (double-precision) floating point number.

double weight = 40.2;
print(weight);// we get 40.2
// we can't get this by using 'int', but we can use double for integers also.

String: A Dart string (String object) holds a sequence of UTF-16 code units. You can use either single or double quotes to create a string:

String name = 'Ruban Raghavendar K';
String nameString = "My Name is $name";//This is String Interpolation
// Instead of using '+' for concat, in dart we can use '$' to interpolate.

bool: To represent boolean values, Dart has a type named bool. Only two objects have type bool: the boolean literals true and false, which are both compile-time constants.

bool darkMode = true;
bool isExpanded = false;

Print('$darkMode : $isExpanded');// we get true : false

List: Perhaps the most common collection in nearly every programming language is the array or ordered group of objects. In Dart, arrays are objects, so most people just call them lists.

List names = ['yasif', 'sanjay', 'manoj', 'vinayak'];
print(names);// we get [yasif, sanjay, manoj, vinayak]

Map: In general, a map is an object that associates keys and values. Both keys and values can be any type of object. Each key occurs only once, but you can use the same value multiple times. Dart support for maps is provided by map literals and the Map type.

Map nobleGases = {
'name': 'tony',
'age': 20,
'place': 'cbe'
};

Map<String, String> gifts = {
// Key: Value
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};

// we can also say the map key value types.

dynamic: There is another way to create a variable in Dart by using the keyword ‘dynamic‘. As the name suggests, the variable which is created by using the keyword dynamic, can contain any type of data.

dynamic myVar = "Hello World";
myVar = 100; // we can assign a different type data
myVar = true; // assigning boolean value

Functions:

Dart is a true object-oriented language, so even functions are objects and have a type, Function. This means that functions can be assigned to variables or passed as arguments to other functions. You can also call an instance of a Dart class as if it were a function.

bool isDark(String theme) {
return theme == 'dark' ? true : false;
} //the function with the type

isDark(String theme) {
return theme == 'dark' ? true : false;
} //the function still works if you omit the types

For functions that contain just one expression, you can use a shorthand syntax:

bool isDark(String theme) => theme == 'dark' ? true : false;
// The => expr syntax is a shorthand for { return expr; }.
// The => notation is sometimes referred to as arrow syntax.

Parameters:

Parameters are the most common type of function parameters. They are also known as positional parameters. When you call a function with parameters, you need to pass arguments in the same order as the parameters defined in the function signature.

void printNameAge(String name, int age) {
print('Name: $name, Age: $age');
}

// Calling the function with parameters
printNameAge('John', 30);

Named Parameters:

Named parameters allow you to specify arguments in a function call using their names, regardless of their order. Named parameters are enclosed in curly braces {} and if we need to declare as required, then it must be defined in the function signature with required annotation from the meta package or by providing a default value using =.

void printDetails({required String name, int age = 0}) {
print('Name: $name, Age: $age');
}

// Calling the function with named parameters
printDetails(name: 'Alice', age: 25);
printDetails(age: 30, name: 'Bob'); // The order doesn't matter for named parameters

Optional Parameters:

Optional parameters allow you to provide default values for arguments, making them optional during function calls. They can be positional or named parameters. Positional optional parameters are enclosed in square brackets [], and named optional parameters are enclosed in curly braces {}. Optional parameters can have default values, which are used when you don't pass a value during the function call.

// Positional Optional Parameter
void printMessage(String message, [String author = 'Anonymous']) {
print('$author says: $message');
}

// Calling the function with optional parameter
printMessage('Hello'); // Output: Anonymous says: Hello
printMessage('Hi', 'John'); // Output: John says: Hi

// Named Optional Parameter
void printOptions({bool bold = false, bool italic = false}) {
print('Bold: $bold, Italic: $italic');
}

// Calling the function with named optional parameters
printOptions(); // Output: Bold: false, Italic: false
printOptions(bold: true); // Output: Bold: true, Italic: false
printOptions(italic: true, bold: true); // Order doesn't matter for named parameters

The main() function:

In Dart, the main function is the entry point of any Dart program. It is the first function that gets executed when you run a Dart application. The main function must have a specific signature, which is:

void main() {
// Code to be executed when the program starts.
}

Here’s a breakdown of the main function:

  1. void: The void keyword indicates that the main function does not return any value. In Dart, if a function does not specify a return type, it is considered to return void by default.
  2. main The name of the function is always main. This is a reserved name for the entry point in Dart.
  3. ()The main function takes no arguments. You can see that there are empty parentheses () after the function name, indicating that it doesn't expect any parameters.

The main function is where you typically start writing your Dart code. From the main function, you can call other functions, create objects, and perform various operations to execute your program's logic.

Example:

void main() {
print('Hello, Dart!'); // Output: Hello, Dart!

int result = addNumbers(5, 7);
print('The sum of 5 and 7 is $result.'); // Output: The sum of 5 and 7 is 12.
}

int addNumbers(int a, int b) {
return a + b;
}

Functions as parameters:

In Dart, you can pass a function as a parameter to another function. This allows you to pass behavior or logic to a function and make it more dynamic and flexible. Functions that accept other functions as parameters are known as higher-order functions.

Here’s an example of how to pass a function as a parameter:

void main() {
// Call the higher-order function and pass the 'printMessage' function as an argument
executeFunction(printMessage);

// Call the higher-order function and pass an anonymous function as an argument
executeFunction(() {
print('This is an anonymous function.');
});
}

// Higher-order function that accepts a function as a parameter
void executeFunction(Function function) {
// Call the function that was passed as an argument
function();
}

// Function to be passed as an argument
void printMessage() {
print('Hello from printMessage function!');
}

Anonymous functions:

Anonymous functions, also known as lambda functions or function literals, are functions in Dart that do not have a name. Instead of defining a named function, you create an anonymous function on-the-fly directly where it’s needed. Anonymous functions are often used as arguments to higher-order functions, in callback functions, or when a short-lived function is required.

The syntax for defining an anonymous function in Dart is using the () {} notation:

void main() {
// Using an anonymous function as an argument to a higher-order function
executeFunction(() {
print('This is an anonymous function.');
});

// Using an anonymous function with parameters
var sum = (int a, int b) {
return a + b;
};

print('Sum of 5 and 7 is: ${sum(5, 7)}'); // Output: Sum of 5 and 7 is: 12
}

// Higher-order function that accepts a function as a parameter
void executeFunction(Function function) {
// Call the function that was passed as an argument
function();
}
void main() {
final List<String> names = ['yasif', 'rohith', 'sanjay', 'noorul'];

names
.map((item) => item.toUpperCase())
.forEach((item) => print('$item: ${item.length}'));
}

Lexical scope:

Lexical scope, also known as static scope, is a fundamental concept in computer programming that determines how the visibility and accessibility of variables and functions are determined in a given block of code. The lexical scope is determined at compile-time and depends on the physical arrangement of the code in the source file, rather than the runtime flow of the program.

In a lexically scoped language, the scope of a variable or function is determined by its position within the source code’s lexical structure (i.e., where it is defined) and the nesting of code blocks (e.g., functions, loops, conditionals). A variable or function defined in an outer block is accessible from the inner block, but the reverse is not true.

Here’s a simple example to illustrate lexical scope:

void main() {
var outerVar = 'I am outside';

void outerFunction() {
var innerVar = 'I am inside';
print(innerVar); // Output: I am inside
print(outerVar); // Output: I am outside
}

outerFunction();
// print(innerVar); // Error: innerVar is not accessible here
}

In this example, we have a variable outerVar and a function outerFunction defined in the main function. The innerVar variable is defined within the outerFunction. In this lexical scope, the innerVar can access variables from the outer scope (e.g., outerVar), but variables defined inside the outerFunction are not accessible from outside of it.

When outerFunction is called, it can access both innerVar and outerVar. However, if we try to access innerVar outside of the outerFunction, it will result in an error, as innerVar is not in the scope at that point.

Lexical scope provides a clear and predictable way to manage variables and functions in your code. It allows for better code organization, reduces naming conflicts, and improves code readability and maintainability. Most modern programming languages, including Dart, use the lexical scope to determine variable and function visibility.

Lexical Closures:

Lexical closures are a powerful concept in programming that arises from the combination of lexical scoping and functions. A closure is a function that “closes over” its surrounding environment, meaning it captures and retains the variables and bindings from its lexical scope even after the outer function has finished executing. In other words, a closure allows a function to remember and access the variables of the scope in which it was created, even if that scope is no longer active.

Here’s an example to illustrate lexical closures in Dart:

Function createClosure() {
int counter = 0;

// This is the closure function that "closes over" the 'counter' variable
return () {
counter++;
print('Counter: $counter');
};
}

void main() {
var closure1 = createClosure();
var closure2 = createClosure();

closure1(); // Output: Counter: 1
closure1(); // Output: Counter: 2

closure2(); // Output: Counter: 1 (Independent closure with its own 'counter')
closure1(); // Output: Counter: 3 (closure1 retains its own 'counter')
}

In this example, the function createClosure returns an anonymous closure function. The closure function captures the counter variable from its surrounding scope (createClosure function) and increments it each time it is called.

When we create two closures (closure1 and closure2) using createClosure, each closure maintains its own separate copy of the counter variable, preserving its state independently. This behavior is possible due to lexical closures, where the closure functions retain a reference to their enclosing environment (the scope of createClosure), even after createClosure has finished executing.

Get Ruban Raghavendar K’s stories in your inbox

Join Medium for free to get updates from this writer.

Lexical closures are essential for implementing various programming patterns, such as callbacks, event handling, and encapsulation. They provide a way to encapsulate data within functions, prevent global state pollution, and enable functional programming paradigms.

Testing functions for equality:

In Dart, you can test whether two functions are equal by comparing their references or by using the Function type. However, there are some caveats to consider depending on the types of functions you are comparing.

  1. Comparing function references: When you compare two function references, you are checking whether they point to the same memory location, indicating they are the same function.
void functionA() {
print('Function A');
}

void functionB() {
print('Function B');
}

void main() {
Function funcA = functionA;
Function funcB = functionB;

print(funcA == funcB); // Output: false
print(funcA == functionA); // Output: true
}

In this example, we have two functions functionA and functionB. We then create two Function variables (funcA and funcB) and assign the functions to them. When we compare funcA and funcB, we get false because they refer to different functions. However, comparing funcA with functionA directly gives true since both point to the same function.

2. Comparing functions with the same signature: If you have two functions with the same signature (return type and parameter types), you can use the Function type to compare them.

bool areEqual(Function function1, Function function2) {
// Compare functions using the 'Function' type
return function1 == function2;
}

void main() {
Function funcA = (int a, int b) => a + b;
Function funcB = (int a, int b) => a + b;

print(areEqual(funcA, funcB)); // Output: false
print(areEqual(funcA, funcA)); // Output: true
}

In this example, the areEqual function takes two functions with the same signature and compares them using the == operator. Note that comparing two anonymous functions (like funcA and funcB) with the same logic will result in false because they are different instances.

Keep in mind that Dart’s function comparison is based on function references or the Function type, and it doesn't perform a deep comparison of the functions' internal logic. If you need to compare the behavior of two functions, you might need to devise your own custom logic for function equivalence.

Return Values:

Functions can return values using the return keyword. When a function is executed and encounters the return statement, it stops executing further and immediately returns the specified value (if any) to the caller.

Here’s a simple example of a function that returns a value:

int addNumbers(int a, int b) {
int sum = a + b;
return sum; // Return the result to the caller
}

void main() {
int result = addNumbers(5, 7); // Call the function and store the returned value
print('The sum of 5 and 7 is $result.'); // Output: The sum of 5 and 7 is 12.
}

The function addNumbers takes two integer parameters a and b and calculates their sum, storing it in the sum variable. The function then uses the return statement to return the value of sum back to the caller.

When the addNumbers function is called in the main function with the arguments 5 and 7, it returns the result of the addition (12) as the function's return value, which is then stored in the result variable. The print statement displays the result on the console.

It’s important to note that not all functions need to return a value. Functions that don’t explicitly return a value have a return type of void. For example:

`void printMessage(String message) {
print(message);
}

void main() {
printMessage('Hello, Dart!'); // Output: Hello, Dart!
}

In this case, the printMessage function takes a String parameter message, prints it to the console, and doesn't return any value. The return type void indicates that the function does not return any meaningful result.

Generators:

generators are functions that produce a sequence of values lazily, on-demand, using the sync* or async* syntax. They are useful when you need to generate a potentially infinite sequence of values or when you want to optimize memory usage by generating values one at a time instead of creating an entire list in memory.

Generators are defined using functions that include the sync* or async* modifier:

  1. sync* Generator: A sync* generator produces values synchronously, one after the other, using the yield keyword. When you call a sync* generator, it returns an iterable that can be used in a for...in loop or with other iterable operations.
Iterable<int> generateEvenNumbers(int max) sync* {
for (int i = 2; i <= max; i += 2) {
yield i; // Yield the even number
}
}

void main() {
var evenNumbers = generateEvenNumbers(10);

for (var number in evenNumbers) {
print(number);
}
}

Output:

2. async* Generator: An async* generator produces values asynchronously, allowing you to use await inside the generator function. It also uses the yield keyword, similar to sync*, but it can perform asynchronous operations while producing values.

import 'dart:async';

Stream<int> countdown(int start, int end) async* {
for (int i = start; i >= end; i--) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}

Future<void> main() async {
var countdownStream = countdown(5, 0);

await for (var number in countdownStream) {
print(number);
}
}

Output:

Number printed one by one with the delay of 1 second

In this example, we define the countdown function as an async generator using the async* modifier. The function takes two integer parameters: start and end, representing the countdown range. In the generator, we use a loop to yield numbers from start to end in descending order. We use await Future.delayed to introduce a 1-second delay between each yield to simulate the countdown effect.

In the main function, we call the countdown function with start as 5 and end as 0. This returns a stream that produces the countdown numbers asynchronously. We use the await for loop to listen to the stream and print each number as it becomes available.

Operators:

Press enter or click to view image in full size

Operators are symbols or keywords that perform various operations on variables and values. Operators are used to manipulate data and perform calculations in expressions. Dart supports a wide range of operators, and they can be categorized into different types:

Arithmetic operators:

Press enter or click to view image in full size

Dart also supports both prefix and postfix increment and decrement operators:

Press enter or click to view image in full size

I’m giving a small and cool example for prefix and postfix:

a++ (Postfix Increment Operator): The postfix increment operator a++ increments the value of a but returns the original value of a before the incrementation. This means that the current value of a is used in the expression where a++ appears, and then a is incremented by 1.

int a = 5;
int result = a++;

print(a); // Output: 6 (a is incremented after its value is used)
print(result); // Output: 5 (the original value of a before incrementation)

++a (Prefix Increment Operator): The prefix increment operator ++a increments the value of a and immediately returns the new value of a after the incrementation. This means that a is incremented first, and then the updated value of a is used in the expression where ++a appears.

int a = 5;
int result = ++a;

print(a); // Output: 6 (a is incremented before its value is used)
print(result); // Output: 6 (the updated value of a after incrementation)
  • a++ is the postfix increment operator, which returns the original value and then increments the variable.
  • ++a is the prefix increment operator, which increments the variable and then returns the updated value.

It’s essential to understand the difference between these operators, as using them in different scenarios may produce different results in your code.

The same steps followed in the decrement(- -) side also…

Equality and relational operators:

Press enter or click to view image in full size

Type test operators:

The as, is, and is! operators are handy for checking types at runtime.

I’m giving an easy example to understand:

void main() {
// Using the 'is' operator to check the type of an object
dynamic value = 42;
if (value is int) {
print('value is an integer.'); // Output: value is an integer.
}

// Using the 'as' operator for type casting
dynamic animal = 'Cat';
String catSound = animal as String;
print('The sound a cat makes is: $catSound'); // Output: The sound a cat makes is: Cat

// Using the 'is!' operator to check if an object is not of a certain type
dynamic number = 3.14;
if (number is! int) {
print('number is not an integer.'); // Output: number is not an integer.
}
}

Output:

Assignment operators:

As you’ve already seen, you can assign values using the = operator. To assign only if the assigned-to variable is null, use the ??= operator.

Press enter or click to view image in full size
// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;

Logical operators:

Press enter or click to view image in full size

Bitwise and shift operators :

You can manipulate the individual bits of numbers in Dart. Usually, you’d use these bitwise and shift operators with integers.

Press enter or click to view image in full size

Conditional expressions:

Conditional expressions are compact ways to evaluate and return a value based on a condition. They are often used to simplify simple if-else constructs when you need to assign a value to a variable or return a value from a function based on a condition.

The syntax of a conditional expression is as follows:

condition ? expression1 : expression2
  • condition: The condition that is evaluated to either true or false.
  • expression1: The value returned if the condition is true.
  • expression2: The value returned if the condition is false.

The conditional expression works as follows: If condition is true, expression1 is evaluated and returned; otherwise, expression2 is evaluated and returned.

Here are some examples to illustrate the usage of conditional expressions:

Example 1: Using a conditional expression to assign a value to a variable:

void main() {
int age = 25;
String message = age >= 18 ? 'You are an adult' : 'You are a minor';
print(message); // Output: You are an adult
}

Example 2: Using a conditional expression to return a value from a function:

bool isEven(int number) => number % 2 == 0;

void main() {
int num1 = 10;
int num2 = 15;

String result1 = isEven(num1) ? '$num1 is even' : '$num1 is odd';
String result2 = isEven(num2) ? '$num2 is even' : '$num2 is odd';

print(result1); // Output: 10 is even
print(result2); // Output: 15 is odd
}

In these examples, we use conditional expressions to determine whether a person is an adult or a minor based on their age (Example 1), and to check whether numbers are even or odd (Example 2).

Conditional expressions offer a concise and readable way to handle simple conditional logic. However, for more complex conditions or multiple branches, using if-else statements might be more appropriate.

Cascade notation:

Cascade notation, also known as method cascades or cascading, is a feature in Dart that allows you to make a sequence of method calls on the same object. It is denoted using the .. operator.

With cascade notation, you can perform multiple operations on the same object without having to repeat the object reference for each method call. This results in more concise and readable code, especially when you need to chain multiple methods together.

Here’s the syntax for cascade notation:

object..method1()..method2()..method3();

The object is the target object on which you want to perform the cascading operations, and method1(), method2(), and method3() are the methods that you want to call on that object.

Let’s see an example to illustrate the use of cascade notation:

class Person {
String name;
int age;

Person(this.name, this.age);

void introduce() {
print('Hello, my name is $name and I am $age years old.');
}

void celebrateBirthday() {
age++;
}
}

void main() {
Person person = Person('Alice', 30);

// Without cascade notation:
person.introduce();
person.celebrateBirthday();
person.introduce();

// With cascade notation:
person..introduce()..celebrateBirthday()..introduce();
}

Output:

Other Operators:

Press enter or click to view image in full size

--

--