Dart Basics

Learn the basics for dart programming language


Learn the basics for the dart programming language, which is used to build cross platform app using flutter.

Variables

Dart syntax is mostly like C++

int age = 21;
double num2 = 2.0;
String name = "Adesh";
 
// String interpolation 
String greetText = 'Hi! $name';
 
// use {} for expression
int currentYear = 2024;
String greetText = "Hi $name you are were born on ${currentYear - age}";
print(greetText);

Dart also has var, const and final. But there not anything like JS

var: it's like any in TS

void main() {
  var username; // dynamic type
 
  username = "adesh";
 
  username = "12";  // allowed
}

final works like const in JS

int num1 = 1;
int num2 = 2;
final sum = num1 + num2;
 
sum = 10; // throws an error, can only set the value once.

const : Can only set value once and the value should be know at the build time

int num1 = 1;
int num2 = 2;
 
const sum = num1 + num2; // const varible must be intialized with constant value
 
const sum = 1 + 2; // works since all the values are know at the build time
 

Null Safety

In Dart a variable cannot be assigned to null by default.

int age = null; // null cannot be assigned to type int
 
int? age = null; // makes age a nullable value
//or 
int? age; // sets age to null

Late

When using classes, you want to declare a variable, but set its value later in the code

class Animal {
  final String _size; // The final variable '_size' must be initialized.
 
  void goBig() {
    _size = "big";
    print(_size);
  }
}

Add the late keyword

class Animal {
  late final String _size;
 
  void goBig() {
    _size = "big";
    print(_size);
  }
}

late allows to keep the variable as non-nullable value but you can initialize it later.

Operators

String? name;
 
name ??= "Guest";

Assigns the value "Guest" only if name is null.

Cascade

Cascades (.., ?..) allow you to make a sequence of operations on the same object. In addition to accessing instance members, you can also call instance methods on that same object.

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

The constructor, Paint(), returns a Paint object. The code that follows the cascade notation operates on this object, ignoring any values that might be returned.

It is equivalent to

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

Functions

Positional Arguments

int sum(int a, int b) {
   return a + b;
}
 
print(sum(1, 2));

Named Arguments

int sum({int? a, required int b, int c = 5}) {
    return c + b;
  }
 
print(sum(b: 3));

You can pass the arguments in any order, the arguments are non-nullable. You can use optional, required and default values.

Class

It provides a way to create object of complex types.

void main() {
//  Basic thing = new Basic(20);
// OR
  Basic thing = Basic(20); // pass the value for id to the constructor
  print(thing.id);
  print(thing.value);
  thing.doStuff();
}
 
class Basic {
  int value = 10;
  int id;
 
  // define the constructor, Constructors are called once at the time of object creation
  Basic(this.id);
    
  // defining class methods
  doStuff() {
    // methods defined in the class have access to it properties      
    print("Hey my ID is $id");
  }
}

We can also define static methods that don't need objects for there execution

void main() {
  Basic.helper();
}
 
class Basic {
  int value = 10;
  int id;
 
  Basic(this.id);
 
  doStuff() {
    print("Hey my ID is $id");
  }
 
  static helper() {
    print("I'm available globally for the Class");
  }
}

Constructor

To initialize a class variable that depends on the value passed to the constructor, use the following syntax

void main() {
  Rectangle rect1 = Rectangle(10, 5);
 
  print(rect1.area);
}
 
class Rectangle {
  final int width, heigth;
  late final int area;
 
  Rectangle(this.width, this.heigth) {
    area = width * heigth;
  }
}

Passing optional parameters

class Rectangle {
  final int width, heigth;
  late final int area;
  String? name;
 
  Rectangle(this.width, this.heigth, [this.name]) {
    area = width * heigth;
  }
}

Using Named parameters

void main() {
  Circle cir = Circle(radius: 10, name: "Circle 1");
}
 
class Circle {
  Circle({required int radius, String? name});
}

NOTICE: In case of Named arguments, we didn't need to define the class properties since they are already named.

Named Constructors

Used when use want to initialize the constructor data by passing different types of arguments

void main() {
  Point point1 = Point.fromList([1.0, 2.0]);
  Point point2 = Point.fromMap({'lat': 1.0, 'lng': 2.0});
}
 
class Point {
  double lat = 0, lng = 0;
 
  // Named Constructor
  // Using map to initialize
  Point.fromMap(Map data) {
    lat = data["lat"];
    lng = data["lng"];
  }
 
  // Using List for initialize
  Point.fromList(List data) {
    lat = data[0];
    lng = data[1];
  }
}

Extends

Used for implementing inheritance in dart

// abstract class (interface), can't have objects of Dog type
abstract class Dog {
  walk() {
    print("walking...");
  }
}
 
class Pug extends Dog {
  String breed = "pug";
 
  // override the base class methods
  @override
  walk() {
    super.walk();
    print("Stopping! Now tired...");
  }
}

Mixins

When extending classes is not enough and you want to add additional behaviors

class Human {}
 
class SuperHuman extends Human with Strong, Fast {}
 
mixin Strong {
  bool doesLift = true;
 
  void benchPress() {
    print("doing bench press...");
  }
}
 
mixin Fast {
  bool doesRun = true;
 
  void sprint() {
    print("running fast");
  }
}

Generics

It allows you to pass a type as a parameter

List<int> numbers = [1, 2, 3];
void main() {
  Box<String> box1 = Box("test");
  Box<double> box2 = Box(3.14);
  Box<List<int>> box3 = Box([1, 2, 3]);
}
 
class Box<T> {
  T value;
 
  Box(this.value);
 
  T openBox() {
    return value;
  }
}

Packages

  1. Importing package

    import 'package.dart';
  2. Import package with a different namespace

    import 'package.dart' as my_utils;
     
    // to access methods
    my_utils.sum()
  3. Hide a certain class

    import 'package.dart' hide sum;
  4. Only use 1 class from the package

    import 'package.dart' show sum;

Asynchronous programming

Futures

Futures are just a re-branding of Promises from the JS world

import "dart:async";
 
void main() {
  print("Before Future");
 
  var delay = Future.delayed(Duration(seconds: 5));
  delay
      .then((value) => print("Waiting for 5 seconds"))
      .catchError((err) => print(err));
 
  print("After Future");
}

You can also use async, await syntax

import "dart:async"; // import the dart async package (built-in)
 
void main() {
  runInFuture();
}
 
void runInFuture() async {
  var data = await Future.value("world");
 
  print('hello $data');
}

Example

Future<List<dynamic>> fetchTodos() async {
  final response =
      await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos'));
  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  } else {
    throw Exception('Failed to fetch todos');
  }
}
 
void main() async {
  final todos = await fetchTodos();
  print(todos);
}

In flutter there are dedicated widget (future builder), that are used to resolve a future directly in the UI.

Streams

Is like promise.all(), It is used to handle multiple async events and handle them from the same place, as they get resolved over time.

import "dart:async";
 
void main() {
  var stream = Stream.fromIterable([1, 2, 3]);
 
  stream.listen((event) => print(event));
}

A stream is like a list of values that will come with time. And you can also use methods like map, reduce, ...

void main() {
  var stream = Stream.fromIterable([1, 2, 3]);
 
  // stream.listen((event) => print(event));
 
  stream.map((event) => event * 2).listen((event) => print(event));
}
 
// prints 
// 2, 4, 6

By default you can only listen to a stream once. To listen to a stream multiple times convert it to a broadcast stream

void main() {
  var stream = Stream.fromIterable([1, 2, 3]).asBroadcastStream();
 
  stream.listen((event) => print(event));
 
  stream.map((event) => event * 2).listen((event) => print(event));
}

Using async/await with streams

import "dart:async";
 
void main() {
  streamFunc();
}
 
streamFunc() async {
  var stream = Stream.fromIterable([1, 2, 3]);
 
  // async for loop prints the value when the streams emits a new event
  await for (int value in stream) {
    print(value);
  }
}

Just like FutureBuilder, flutter has StreamBuilder to resolve multiple async request in the UI