Introduction to Dart #

Dart was born out of a very specific frustration: JavaScript was never designed for large-scale applications, and there was no easy way to fix that from within. Google, which writes tens of millions of lines of JavaScript for its products, needed a language that could compile to JavaScript for the browser while also running natively on servers and mobile devices — with a strong type system, integrated tooling, and predictable performance. Dart is the answer to that problem. But Dart’s journey was anything but linear: the language nearly sank before it found its true purpose through Flutter — and with Flutter, Dart has become one of the fastest-growing languages in the world of mobile development. This article covers where Dart comes from, what makes it technically different, how its ecosystem works, and when Dart is the right choice.

Dart’s Design Philosophy #

Dart was designed around one central proposition: a language that can be a single source of truth for every platform — mobile, web, desktop, and server — without sacrificing performance or developer ergonomics.

Three principles shaped Dart’s design decisions:

Developer productivity comes first. Dart chose a familiar ergonomics (C/Java/JavaScript-like syntax) so developers coming from other languages can be productive quickly. Hot reload in Flutter — which lets you see UI changes in milliseconds without a restart — is the most tangible expression of this principle.

A sound type system, not gradual typing. Dart has enforced sound null safety since version 2.12 — not as an option, but as a guarantee. If your code compiles without errors, the compiler guarantees that no null can silently slip into a non-nullable variable. This is different from TypeScript, whose null safety can be “broken into” with any.

AOT and JIT in a single language. Dart supports two compilation modes: JIT (Just-In-Time) for fast development with hot reload, and AOT (Ahead-Of-Time) for optimized native binaries in production. No other language delivers both seamlessly in one toolchain.

flowchart TD
    A[Dart Code] --> B{Compilation Mode}
    B -- Development --> C[JIT Compilation]
    B -- Production --> D[AOT Compilation]
    C --> E[Hot Reload ✓]
    C --> F[Debug Info ✓]
    D --> G[Native Binary]
    D --> H[JavaScript via dart2js]
    D --> I[WebAssembly via dart2wasm]
    G --> J[Android / iOS / Desktop]
    H --> K[Browser]
    I --> K

History and Evolution of Dart #

Understanding Dart’s history matters because the early versions created a reputation that is no longer relevant — and many developers still carry that outdated perception around.

Lars Bak and Kasper Lund, two engineers who had previously built Google’s V8 JavaScript engine, introduced Dart at the GOTO conference in 2011 in Aarhus, Denmark. Dart was initially presented as a potential JavaScript replacement in the browser — an ambition that immediately sparked resistance from the web community. Browser vendors other than Google refused to implement the Dart VM, and the plan was gradually abandoned.

YearVersionMajor Milestone
2011Dart introduced publicly by Lars Bak and Kasper Lund
20131.0First stable release, web-focused with the dart2js compiler
2015Flutter begins development using Dart
20182.0Major redesign: sound type system, Dart for Flutter
20212.12Sound null safety — a paradigm shift in null handling
20212.13Type aliases for non-function types
20222.17Enhanced enums, super initializer parameters
20233.0Records, patterns, class modifiers — the modern Dart era
20233.2Extension types, interop improvements
20243.3Extension types stable, Wasm improvements

The real turning point was Flutter. When Google released Flutter 1.0 in 2018, Dart suddenly had a killer use case that couldn’t be ignored: one codebase for Android and iOS with near-native performance. Dart adoption surged — not because developers chose Dart on its own merits, but because they chose Flutter and Dart came with it.

Dart 2.0 was effectively a reboot. Almost everything you read about Dart before 2018 is no longer relevant — the type system changed fundamentally, many APIs were redesigned, and the language gained a clear identity as “the language of Flutter”.

stateDiagram-v2
    [*] --> WebEra: 2011-2014
    WebEra --> Transisi: Flutter prototype (2015)
    Transisi --> FlutterEra: Dart 2.0 + Flutter 1.0 (2018)
    FlutterEra --> NullSafety: Dart 2.12 (2021)
    NullSafety --> ModernDart: Dart 3.0 (2023)
    ModernDart --> [*]

    WebEra: Ambition to replace JavaScript, low adoption
    Transisi: Dart becomes Flutter's internal language
    FlutterEra: Adoption boom through Flutter
    NullSafety: Type system becomes fully sound
    ModernDart: Records, patterns, extension types

Sound Null Safety — a Guarantee, Not a Suggestion #

Null safety in Dart is not just an extra feature — it’s a fundamental change in how the type system works. Since Dart 2.12, every non-nullable variable cannot hold null unless you explicitly declare the variable as nullable with ?.

// ANTI-PATTERN (pre-null-safety era or languages without sound null safety)
String nama;              // could be null at any time without a warning
print(nama.length);       // runtime error: Null check operator used on a null value

// CORRECT: Dart forces honest declarations about nullability
String namaPasti = "Unis";      // never null — guaranteed by the compiler
String? namaMungkinNull;        // may be null — you must handle this explicitly

// The compiler rejects this:
// print(namaMungkinNull.length);  // ERROR: The property 'length' can't be unconditionally accessed

// The correct way to handle nullables
print(namaMungkinNull?.length);              // null-aware access: returns null if null
print(namaMungkinNull?.length ?? 0);         // null-coalescing: falls back to 0
print(namaMungkinNull!.length);              // null assertion: you're sure it's not null (throws if null)

if (namaMungkinNull != null) {
  print(namaMungkinNull.length);  // promotion: inside this block, Dart knows it's not null
}
The ! operator (null assertion) is an escape hatch that must be used very carefully. If the variable turns out to be null at runtime, the program will throw Null check operator used on a null value. It’s the only way to “cheat” Dart’s null safety — and that’s intentional, so it’s easy to audit. Prefer ??, ?., or a conditional check over !.

The effects of null safety go beyond just preventing crashes. The compiler can optimize code more aggressively because it knows which values will never be null — producing faster, smaller binaries.


The Dart Type System #

Dart is a statically typed language, but with powerful type inference — you don’t always need to write types explicitly.

Type Inference with var and final #

// var — type is inferred from the initial value, cannot change type
var nama = "Unis";          // inferred as String
var umur = 25;              // inferred as int
var tinggi = 175.5;         // inferred as double

// ANTI-PATTERN: declaring types explicitly when it's already obvious
String nama2 = "Unis";      // redundant — var is enough
int umur2 = 25;             // redundant — var is enough

// CORRECT: use explicit types when they clarify intent
String? emailNullable;      // needs to be explicit because it's nullable
List<String> daftarNama = []; // needs to be explicit for empty generics

// final — assigned once, cannot be reassigned
final namaPengguna = "Unis";
// namaPengguna = "Lain";  // ERROR: The final variable can't be assigned

// const — compile-time constant
const pi = 3.14159;
const maxRetry = 3;

Generics #

Generics in Dart are reified — type information is available at runtime, not just at compile time like in Java (type erasure). This means you can do real type introspection.

// List with a specific type
List<int> angka = [1, 2, 3, 4, 5];
List<String> nama = ["Alice", "Bob", "Charlie"];

// Map with generics
Map<String, int> skor = {
  "Alice": 95,
  "Bob": 87,
};

// Generic function
T pertama<T>(List<T> list) {
  if (list.isEmpty) throw ArgumentError("List is empty");
  return list[0];
}

int angkaPertama = pertama([10, 20, 30]);      // => 10
String namaPertama = pertama(["Alice", "Bob"]); // => "Alice"

// Reified generics — type is available at runtime
print(angka is List<int>);    // true
print(angka is List<String>); // false — not a List<String>!
// In Java: both would be true due to type erasure

The Concurrency Model: Isolate #

Dart doesn’t use a thread model like Java or Go. Dart uses Isolate — a genuinely isolated unit of execution that doesn’t share memory with other isolates. Communication between isolates happens through message passing, not shared state.

flowchart LR
    A[Main Isolate\nHeap A] -- send message --> B[Worker Isolate\nHeap B]
    B -- send result --> A
    C[Isolate C\nHeap C] -- send message --> A

    style A fill:#4f86c6,color:#fff
    style B fill:#5aaf6a,color:#fff
    style C fill:#e0884f,color:#fff

This model eliminates data races architecturally — not with locks or mutexes, but because there’s no shared memory to begin with. This differs from Go (goroutines share the heap) or Java (threads share the heap with manual synchronization).

For most concurrency needs in Dart, async/await built on Future and Stream is enough — use Isolate for CPU-intensive tasks that could block the UI thread.

import 'dart:async';
import 'dart:isolate';

// async/await — for non-blocking I/O
Future<String> ambilData(String url) async {
  // simulate a network request
  await Future.delayed(Duration(seconds: 2));
  return "Data from $url";
}

Future<void> main() async {
  print("Starting fetch...");
  
  // Sequential — wait one at a time
  final data1 = await ambilData("https://api.example.com/users");
  final data2 = await ambilData("https://api.example.com/posts");
  
  // BETTER: Parallel — run both at the same time
  final hasil = await Future.wait([
    ambilData("https://api.example.com/users"),
    ambilData("https://api.example.com/posts"),
  ]);
  
  print("Done: ${hasil[0]}, ${hasil[1]}");
}

// Isolate — for heavy computation that must not block the UI
Future<List<int>> hitungPrimaBerat(int batas) async {
  // Run in a separate Isolate to keep the UI responsive
  return await Isolate.run(() {
    final prima = <int>[];
    for (int i = 2; i <= batas; i++) {
      bool isPrima = true;
      for (int j = 2; j * j <= i; j++) {
        if (i % j == 0) { isPrima = false; break; }
      }
      if (isPrima) prima.add(i);
    }
    return prima;
  });
}

Stream — Continuous Asynchronous Data #

Future is for a single value in the future, Stream is for an ongoing sequence of asynchronous values — like user events, sensor data, or WebSocket messages.

// A simple Stream
Stream<int> hitunganMundur(int dari) async* {
  for (int i = dari; i >= 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i;  // emit values one at a time
  }
}

Future<void> main() async {
  // Listen to the stream
  await for (final angka in hitunganMundur(5)) {
    print("Countdown: $angka");
  }
  
  // Or with listen
  hitunganMundur(3).listen(
    (angka) => print("Value: $angka"),
    onError: (e) => print("Error: $e"),
    onDone: () => print("Stream done"),
  );
}

Modern Features: Dart 3.x #

Dart 3.0, released in 2023, brought three major features that significantly changed how Dart code is written.

Records #

A record is an anonymous, immutable data type for grouping several values without having to define a new class.

// ANTI-PATTERN: using a List or Map that isn't type-safe
List hasilDivide(int a, int b) {
  return [a ~/ b, a % b];  // the caller can't tell which is the quotient and which is the remainder
}

// CORRECT: Record — type-safe, named fields optional
(int hasil, int sisa) bagi(int a, int b) {
  return (a ~/ b, a % b);
}

void main() {
  final (hasil, sisa) = bagi(17, 5);
  print("17 / 5 = $hasil remainder $sisa");  // => 17 / 5 = 3 remainder 2

  // Named fields
  ({String nama, int umur}) profil = (nama: "Unis", umur: 28);
  print(profil.nama);  // => Unis
}

Pattern Matching #

Pattern matching enables destructuring and conditional logic that are far more expressive.

sealed class Bentuk {}
class Lingkaran extends Bentuk { final double radius; Lingkaran(this.radius); }
class Persegi extends Bentuk { final double sisi; Persegi(this.sisi); }
class Segitiga extends Bentuk { final double alas, tinggi; Segitiga(this.alas, this.tinggi); }

double hitungLuas(Bentuk bentuk) => switch (bentuk) {
  Lingkaran(:final radius) => 3.14159 * radius * radius,
  Persegi(:final sisi)     => sisi * sisi,
  Segitiga(:final alas, :final tinggi) => 0.5 * alas * tinggi,
};

void main() {
  final bentuk = Lingkaran(5);
  print("Area: ${hitungLuas(bentuk)}");  // => Area: 78.53975

  // Pattern in an if statement
  final nilai = (nama: "Unis", skor: 95);
  if (nilai case (nama: final n, skor: >= 90)) {
    print("$n passed with an A");
  }
}

Sealed Classes #

sealed forces exhaustiveness checking — the compiler ensures you handle every possible subtype, like an enum but with data.

sealed class HasilOperasi<T> {}

class Sukses<T> extends HasilOperasi<T> {
  final T data;
  Sukses(this.data);
}

class Gagal<T> extends HasilOperasi<T> {
  final String pesan;
  final Exception? exception;
  Gagal(this.pesan, [this.exception]);
}

class Loading<T> extends HasilOperasi<T> {}

// The compiler FORCES you to handle every case — nothing can slip through
String deskripsiHasil(HasilOperasi<String> hasil) => switch (hasil) {
  Sukses(:final data)   => "Success: $data",
  Gagal(:final pesan)   => "Failed: $pesan",
  Loading()             => "Loading...",
  // No default needed — the compiler knows this is exhaustive
};

The Ecosystem: pub.dev and Dart Tooling #

pub.dev is the official package registry for Dart and Flutter. Dart tooling is integrated into a single dart command:

# Project management
dart create nama_project          # create a new project (console app)
dart create -t web nama_project   # web project
dart create -t package nama_lib   # library/package

# Package management
dart pub get                      # install dependencies from pubspec.yaml
dart pub add http                 # add a dependency
dart pub add --dev mockito        # add a dev dependency
dart pub upgrade                  # update all packages

# Build, run, compile
dart run                          # run the project
dart run bin/main.dart            # run a specific file
dart compile exe bin/main.dart    # compile to a native executable
dart compile js bin/main.dart     # compile to JavaScript
dart compile wasm bin/main.dart   # compile to WebAssembly

# Quality
dart analyze                      # static analysis
dart format .                     # format all files
dart test                         # run all tests
dart doc                          # generate documentation

Example pubspec.yaml for a pure Dart (non-Flutter) project:

name: aplikasi_saya
description: Contoh aplikasi Dart dengan dependensi umum
version: 1.0.0
environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  http: ^1.1.0           # HTTP client
  json_annotation: ^4.8.0 # JSON serialization
  args: ^2.4.2           # CLI argument parsing
  logging: ^1.2.0        # Logging

dev_dependencies:
  test: ^1.24.0
  mockito: ^5.4.0
  build_runner: ^2.4.0
  json_serializable: ^6.7.0
  lints: ^3.0.0          # official lint rules
CategoryPackageUse Case
HTTP Clienthttp, dioRequests to external APIs
JSONjson_serializable, freezedCode generation for serialization
State Managementriverpod, bloc, providerState management in Flutter
Databasedrift, isar, hiveLocal database
DI / Service Locatorget_it, injectableDependency injection
Testingtest, mockito, mocktailUnit and integration testing
CLIargs, dcliCommand-line applications
Code Generationbuild_runner, source_genAutomatically generated code

Dart Beyond Flutter #

One of the biggest misconceptions about Dart: many developers assume Dart can only be used for Flutter. In reality, Dart is a general-purpose language that works perfectly well on its own.

CLI Applications — Dart compiles to native binaries that can be distributed as a single file with no runtime dependencies. This makes it a great fit for tooling and CLI apps.

// bin/main.dart — a simple CLI app
import 'dart:io';
import 'package:args/args.dart';

void main(List<String> arguments) {
  final parser = ArgParser()
    ..addOption('nama', abbr: 'n', defaultsTo: 'Dunia')
    ..addFlag('sapa', abbr: 's', defaultsTo: false);

  final results = parser.parse(arguments);
  
  if (results['sapa'] as bool) {
    print("Hello, ${results['nama']}! Welcome to the Dart CLI.");
  } else {
    print("${results['nama']}");
  }
}

Server-side — Frameworks like shelf and dart_frog let you write REST APIs or backend services in Dart. Google itself uses Dart for several internal backend services.

import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';

void main() async {
  final router = Router()
    ..get('/health', (Request req) => Response.ok('OK'))
    ..get('/api/users/<id>', (Request req, String id) {
      return Response.ok('{"id": "$id", "nama": "User $id"}',
          headers: {'content-type': 'application/json'});
    });

  final server = await shelf_io.serve(router, 'localhost', 8080);
  print('Server running at http://${server.address.host}:${server.port}');
}

When to Choose Dart #

Dart isn’t the best choice for every situation. The right decision depends on your project’s context.

Choose Dart if:
  ✓ You're building a Flutter app (mobile/desktop/web)
  ✓ You need one codebase for Android, iOS, and web
  ✓ Your team is already familiar with Dart from previous Flutter projects
  ✓ You're building a CLI tool that needs to compile to a native binary
  ✓ Sound null safety and a strong type system are requirements
  ✓ Hot reload / fast development cycles are a priority

Consider alternatives if:
  ✗ You're building a standalone API backend → Go, Kotlin, Python have more mature ecosystems
  ✗ ML/AI projects → Python is irreplaceable
  ✗ Your team is web-only and doesn't need mobile → TypeScript + React is more relevant
  ✗ You need a broad, proven server-side ecosystem → Node.js, Java, Go
  ✗ Native iOS development without Flutter → Swift is the natural choice
  ✗ Native Android without Flutter → Kotlin is more natural
CriteriaDart (Flutter)React NativeSwift/KotlinKotlin Multiplatform
One codebase✓ Full✓ Full✗ Separate✓ Partial
UI performance★★★★★★★★☆☆★★★★★★★★★★
Ecosystem★★★★☆★★★★☆★★★★★★★★☆☆
Ease of learning★★★★☆★★★★☆★★★☆☆★★★☆☆
Hot reload★★★★★★★★★☆★★☆☆☆★★★☆☆
Web support★★★★☆★★★☆☆

FAQ #

Can Dart be used without Flutter?

Yes, completely. Dart is a general-purpose language that can compile to native binaries (for CLI/server), JavaScript (for web), or WebAssembly. Flutter is a framework built on top of Dart — not the other way around. You can learn and use Dart without ever touching Flutter.

What’s the difference between var, final, and const in Dart?

var is a variable that can be reassigned (its type is inferred from the initial value). final is a variable that can only be assigned once at runtime. const is a compile-time constant — its value must be evaluable at compile time. Use const as much as possible for performance, final for values set only once, and var for everything else.

Does Dart support functional programming?

Dart supports functional programming styles very well: higher-order functions, closures, lambdas, immutable collections, and — since Dart 3 — pattern matching and records. Dart isn’t a purely functional language, but it strongly supports a hybrid style.

How does Dart handle dependency injection?

Dart has no built-in DI, but the package ecosystem is mature. get_it is the most popular service locator. For larger Flutter projects, injectable together with get_it provides annotation-based DI similar to Spring in Java. riverpod is also commonly used as an alternative for both DI and state management.

Is Dart stable for production?

Very stable. Dart 2.x has been in production since 2018, and Dart 3.x (2023) is a major release where all breaking changes have been well managed. Google uses it internally, and Shopify, BMW, eBay, and thousands of startups use Flutter (and Dart) for their production apps.


Summary #

  • Dart was born from Google’s real needs — not an academic research project, but a solution to a JavaScript scaling problem that couldn’t be solved from within. Its original goal shifted, but the result is a far more solid language.
  • Sound null safety is a compiler guarantee — not just a lint warning. If your Dart code compiles cleanly, no null can silently enter a non-nullable variable. This eliminates an entire class of common bugs.
  • JIT for development, AOT for production — the two compilation modes work within one toolchain. Millisecond hot reloads during development, optimized native binaries in production.
  • Isolate is not a thread — Dart eliminates data races architecturally with isolates that don’t share memory. Communication happens via message passing, not shared state.
  • async/await built on Future and Stream — for most async needs, use Future for single values and Stream for continuous data. Isolate is only for CPU-intensive computation.
  • Dart 3.x brings records, patterns, and sealed classes — these features change how you write correct, exhaustive code. Pattern matching in particular removes much of the need for manual type casting.
  • Dart is more than just Flutter — Dart can compile to native CLI binaries, JavaScript, and WebAssembly. Backends with shelf/dart_frog, CLI tools, and web apps can all be written in pure Dart.
  • pub.dev is a mature ecosystem — especially in the Flutter category, but server-side and CLI packages keep growing too. The dart CLI tooling covers everything — packages, format, analyze, test, and compile.

Next: Installation →
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact