Skip to content

Introduction to Dart Programming

Flutter - Introduction to Dart Programming (with Null Safety)

Section titled “Flutter - Introduction to Dart Programming (with Null Safety)”

Flutter applications are written using the Dart programming language. Dart is a modern, object-oriented, client-optimized language developed by Google. It’s designed for building fast apps on any platform (mobile, web, desktop, server). Understanding Dart basics is essential for Flutter development.

This chapter covers fundamental Dart concepts relevant to Flutter, including Null Safety, a major feature introduced to help prevent null reference errors.

Every Dart app must have a top-level main() function, which serves as the entry point.

// This is a single-line comment.
/*
This is a
multi-line comment.
*/
// The main entry point of the application.
void main() {
// print() displays output to the console.
print('Hello, Dart!');
}

Variables store references to values. Dart is type-safe, meaning variable types are checked at compile time (or runtime if using dynamic).

  • Type Annotation: Explicitly declare the type:

  • var: Dart infers the type based on the initial value. Once inferred, the type cannot change.

  • dynamic: Allows the variable type to change at runtime. Use sparingly, as it sacrifices type safety.

Dart’s sound null safety means that variables cannot hold null unless you explicitly allow them to. This significantly reduces runtime null errors.

  • Non-Nullable (Default): Variables must be initialized before use and cannot be assigned null.

  • Nullable (?): Add a ? after the type to indicate that a variable can be null.

  • Null-aware Operators:

    • ?. (Conditional Access): Access a property/method only if the object isn’t null, otherwise returns null. message?.length
    • ?? (If Null Operator): Provide a default value if the expression on the left is null. String name = nullableName ?? 'Guest';
    • ??= (Null-aware Assignment): Assign a value only if the variable is currently null. count ??= 0;
  • Assertion Operator (!): Use ! to tell Dart you are certain an expression won’t be null at runtime. Use with caution; if it is null, it throws an exception.

  • int len = nullableString!.length; // Throws if nullableString is null

  • late Keyword: Used for non-nullable variables initialized after declaration. Useful for instance variables initialized in initState in Flutter.

  • late String description;

    • If accessed before initialization, it throws an error.
    • Can be combined with final: late final String apiData;
  • final: The variable can only be set once. Its value is determined at runtime.

  • const: Marks the variable as a compile-time constant. The value must be known at compile time. const objects are deeply immutable and canonicalized (a single instance is created and reused).

  • Numbers: int (arbitrary-size integers), double (64-bit floating-point). Both are subtypes of num.
  • Strings: Sequence of UTF-16 code units. Use single ('...') or double ("...") quotes. Supports string interpolation ($ or ${expression}).
  • Booleans: bool with values true and false.
  • Lists (Arrays): Ordered collection of objects. List<int> numbers = [1, 2, 3];
  • Sets: Unordered collection of unique items. Set<String> fruits = {'apple', 'banana', 'apple'}; // {'apple', 'banana'}
  • Maps (Dictionaries): Collection of key-value pairs. Keys must be unique. Map<String, int> scores = {'Alice': 90, 'Bob': 85};
  • Runes: Represent Unicode code points.
  • Symbols: Opaque identifiers used for reflection (less common in Flutter apps).
String s1 = 'Single quotes work.';
String s2 = "Double quotes also work.";
String s3 = 'It\'s easy to escape.';
int value = 10;
String interpolated = 'The value is $value.'; // The value is 10.
String calculated = 'Value times two is ${value * 2}.'; // Value times two is 20.
List<String> names = ['Ana', 'Bob', 'Charlie'];
print(names[0]); // Access by index: Ana
names.add('David');
Map<String, String> capitals = {
'USA': 'Washington D.C.',
'UK': 'London'
};
print(capitals['UK']); // Access by key: London

Functions are first-class objects in Dart (can be assigned to variables, passed as arguments).

// Basic function with type annotations
int add(int a, int b) {
return a + b;
}
// Shorthand syntax for single-expression functions
double multiply(double x, double y) => x * y;
// Functions can have optional parameters
// Positional optional parameters (use [])
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
// Call: say('Bob', 'Hi') or say('Bob', 'Hi', 'smartphone')
// Named optional parameters (use {})
void enableFlags({bool? bold, bool? hidden}) {
print('Bold: ${bold ?? false}, Hidden: ${hidden ?? false}');
}
// Call: enableFlags(bold: true) or enableFlags(hidden: false, bold: true)
// Named required parameters (use 'required' keyword)
void printUser({required String name, int? age}) {
print('User: $name, Age: ${age ?? 'Unknown'}');
}
// Call: printUser(name: 'Alice') or printUser(name: 'Bob', age: 30)
void main() {
print(add(5, 3)); // 8
print(multiply(4.0, 2.5)); // 10.0
print(say('Alice', 'Hello')); // Alice says Hello
enableFlags(hidden: true);
// Anonymous function (closure)
var numbers = [1, 2, 3];
numbers.forEach((number) {
print('Number: $number');
});
}

Dart supports standard control flow:

  • if/else
  • for loops (including for-in)
  • while and do-while loops
  • break and continue
  • switch/case (Dart’s switch is powerful, supporting arbitrary objects and requiring break or other exit statements)
  • assert (Used during development to disrupt execution if a boolean condition is false. Disabled in production code.)
void processScore(int score) {
if (score >= 90) {
print('Grade A');
} else if (score >= 80) {
print('Grade B');
} else {
print('Grade C or lower');
}
var list = [10, 20, 30];
for (var item in list) {
print(item);
}
for (int i = 0; i < 3; i++) {
print('Index $i');
}
// Assert example (only works in debug mode)
// assert(score > 0, 'Score must be positive');
}

Dart is object-oriented with classes and mixin-based inheritance.

  • Classes: Blueprints for objects. Use the class keyword.
  • Constructors: Special functions for creating instances (objects) of a class. Can be unnamed, named (ClassName.namedConstructor()), or factory (factory ClassName(...)).
  • Fields (Instance Variables): Variables within a class.
  • Methods: Functions within a class.
  • Inheritance: Use extends to inherit from another class. Use @override annotation when overriding a superclass member.
  • Mixins: A way to reuse code across multiple class hierarchies without deep inheritance. Use the mixin keyword to define and with to apply.
  • Interfaces: Dart doesn’t have a specific interface keyword. Every class implicitly defines an interface. Use implements to guarantee a class provides the interface’s members.
class Vehicle {
String model;
int year;
// Constructor (shorthand for assigning fields)
Vehicle(this.model, this.year);
void honk() {
print('Beep beep!');
}
@override
String toString() => '$year $model';
}
class Car extends Vehicle {
int numberOfDoors;
// Call superclass constructor
Car(String model, int year, this.numberOfDoors) : super(model, year);
// Override method
@override
void honk() {
print('Honk honk!');
super.honk(); // Optionally call superclass method
}
}
// Mixin example
mixin Electric {
void charge() {
print('Charging...');
}
}
// Class using a mixin
class ElectricCar extends Car with Electric {
ElectricCar(String model, int year) : super(model, year, 4);
}
void main() {
var myCar = Car('Toyota Camry', 2022, 4);
print(myCar); // 2022 Toyota Camry
myCar.honk(); // Honk honk! Beep beep!
var myElectricCar = ElectricCar('Tesla Model 3', 2023);
myElectricCar.charge(); // Charging...
}

Asynchronous Programming (async, await, Future)

Section titled “Asynchronous Programming (async, await, Future)”

Dart uses Future objects to represent values that will be available in the future, and async/await keywords for writing asynchronous code that looks synchronous.

  • Future<T>: Represents a potential value or error of type T that will complete later.
  • async: Marks a function as asynchronous; allows the use of await inside it. An async function automatically wraps its return value in a Future.
  • await: Pauses execution until the Future it’s waiting for completes. Can only be used inside an async function.
// Function that simulates fetching data (returns a Future)
Future<String> fetchData() {
// Simulate network delay
return Future.delayed(const Duration(seconds: 2), () => 'Data Fetched!');
}
// Using async/await
Future<void> printData() async {
print('Fetching data...');
try {
String data = await fetchData(); // Wait for the Future to complete
print(data);
} catch (e) {
print('Error fetching data: $e');
}
}
// Using .then() for callbacks (alternative)
void printDataWithThen() {
print('Fetching data (using .then)...');
fetchData().then((data) {
print(data);
}).catchError((error) {
print('Error fetching data: $error');
});
}
void main() async { // main can be async
await printData();
print('Finished.');
// printDataWithThen(); // This would run concurrently if not awaited
}

This covers the Dart essentials needed for Flutter. As you progress, you’ll encounter more advanced features like Streams, Isolates, and Generators.