Dart Language Basics

Dart is the language Flutter is written in, and it is a language designed for exactly one job: making application code that reads like a single, coherent system. It borrows the object model from Java, the expression syntax from JavaScript, and the type discipline from a statically typed language, and it adds something neither of those has - a sound type system that can prove a null never sneaks in where it should not. Before you can build a single widget, you need to be comfortable with values and variables, functions, classes, collections, and asynchronous code, because every Flutter API you will meet is built out of them.

1. Values, Variables, and Types

Every value in Dart has a static type that is known at compile time, and every variable is declared with an explicit type or inferred from its initialiser.

int count = 3;             // integer
double ratio = 0.75;       // double
String label = 'Flutter';  // UTF-16 string
bool isReady = true;       // boolean

Dart has a num type that is the common supertype of int and double. A literal with a decimal point is a double; a literal without one is an int.

var, final, and const

  • var lets the compiler infer the type from the initialiser. The type is then fixed: a var that held a String can never hold an int later.
  • final means the value is assigned exactly once, but the type may still be inferred. It is the right choice for anything you compute at run time.
  • const means the value is a compile-time constant. It must be known statically, and it is canonicalised, so two identical constants are the same object.
var name = 'Ana';          // inferred as String
final now = DateTime.now(); // String-like: assigned once, inferred type
const maxItems = 100;       // compile-time constant, type int

Null safety

Since Dart 3, every type is non-nullable by default. A variable declared String cannot hold null; to allow it you write the type with a question mark.

String name = 'Ana';        // never null
String? nickname;           // may be null
String fallback = nickname ?? 'no nickname'; // null-aware default
nickname ??= 'anonymous';   // assign only if currently null

The ! operator asserts that a value is not null and returns it as non-nullable. Use it only where you have just checked - if (value != null) print(value!.length); - because a wrong assertion throws at run time.

2. Functions

Dart functions declare a return type, a name, and a parameter list.

int add(int a, int b) => a + b;

String greet(String name, {String greeting = 'Hello'}) {
  return '$greeting, $name!';
}

Curly braces around parameters mark them as named parameters, which may be passed in any order and can have default values. Square brackets mark them as optional positional parameters.

void log(String message, [int level = 0]) {
  print('[$level] $message');
}

A function that returns a function is a closure, and Dart supports the => shorthand for a single-expression body.

int Function(int) makeAdder(int n) => (int x) => x + n;
final addFive = makeAdder(5);
print(addFive(3)); // 8

Dart also has optional positional parameters of the “optional named” variety, plus the required keyword, which forces a caller to pass a named parameter that has no default.

void draw({required String label, double opacity = 1.0}) {
  print('$label at $opacity');
}
draw(label: 'overlay');

3. Classes and Objects

Every class implicitly extends Object. Constructors come in several forms.

class Point {
  final double x;
  final double y;

  // Named constructor
  Point.origin() : x = 0, y = 0;

  // Initialising formal
  Point(this.x, this.y);

  double get distanceFromOrigin => x * x + y * y;
}

class Point3D extends Point {
  final double z;
  Point3D(double x, double y, this.z) : super(x, y);
}
  • Default constructor: Point(x, y) when none is declared.
  • Named constructor: Point.origin(), declared with Point.origin() after the class name.
  • Initialising formals: this.x both declares and assigns the field.
  • Initialiser list: : super(x, y), x = 0 runs before the body.
  • Getters and setters: get and set let you expose computed or validated properties.
  • Abstract classes and interfaces: declare abstract and implement with implements.
  • Mixins: mixin on a class that has no constructor of its own, applied with with, lets you add behaviour without inheritance.

4. Collections

Dart ships with three useful collection types.

final numbers = <int>[1, 2, 3];                 // List
final unique = <String>{'a', 'b'};              // Set
final scores = <String, int>{'Ana': 90, 'Bo': 75}; // Map

Common operations:

numbers.add(4);
numbers.removeWhere((n) => n.isEven);
final doubled = numbers.map((n) => n * 2).toList();
final total = scores.values.fold<int>(0, (sum, v) => sum + v);
final names = scores.keys.where((k) => scores[k]! > 80).toList();

The spread operator and the collection if/for elements let you build lists declaratively:

final combined = <int>[...numbers, ...extra, if (debugMode) 0, for (final n in extra) n * 2];

5. Error Handling

Dart uses exceptions, and try/on/catch/finally works the way you would expect.

try {
  final value = int.parse(raw);
  print(value);
} on FormatException catch (e) {
  print('Not a number: ${e.message}');
} catch (e, stackTrace) {
  print('Unexpected: $e\n$stackTrace');
} finally {
  print('Always runs');
}

6. Asynchronous Programming

Dart runs on a single-threaded event loop, so long-running work must not block it. Future represents a value that will arrive later, and async/await is the readable way to work with it.

Future<String> fetchTitle() async {
  final response = await http.get(Uri.parse('https://example.com'));
  return response.body;
}

await pauses the current function without blocking the event loop, and functions marked async return a Future automatically. When you need a stream of values rather than a single one, use Stream.

Stream<int> countdown(int from) async* {
  for (var i = from; i > 0; i--) {
    yield i;
    await Future.delayed(const Duration(seconds: 1));
  }
}

Practice

DartNull SafetyType InferenceAsync Await

Approach: The question mixes three separate topics, so answer them in the order they build on each other: first the type system (what the compiler can prove), then the function signature (how a named parameter and a return type interact), and finally the asynchronous part (what async does to the return type and where the await sits).

Steps:

  1. final String? nickname is nullable, so every use of it must either be null-checked or given a default. nickname ?? 'anonymous' provides the default lazily, and nickname!.length asserts non-nullness - which throws if the value really is null.
  2. A function declared int parse(String raw) async returns a Future<int>, not an int, because async functions always complete later. The int in the signature is the type of the resolved value.
  3. await suspends only the enclosing function; the event loop keeps running other tasks, so a second await on the same future would be wasteful.

Answer: The nullable type is String?, the effective return type of the async function is Future<int>, and await suspends the calling function without blocking the event loop, so the remaining code runs only after the future completes.

Where to Go Next

With Dart in hand, the next page moves to Flutter’s central idea: everything on screen is a widget, and the widget tree - not the imperative code - is what you build.

Found an issue with this content?

Tell us what could be improved.