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 --> KHistory 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.
| Year | Version | Major Milestone |
|---|---|---|
| 2011 | — | Dart introduced publicly by Lars Bak and Kasper Lund |
| 2013 | 1.0 | First stable release, web-focused with the dart2js compiler |
| 2015 | — | Flutter begins development using Dart |
| 2018 | 2.0 | Major redesign: sound type system, Dart for Flutter |
| 2021 | 2.12 | Sound null safety — a paradigm shift in null handling |
| 2021 | 2.13 | Type aliases for non-function types |
| 2022 | 2.17 | Enhanced enums, super initializer parameters |
| 2023 | 3.0 | Records, patterns, class modifiers — the modern Dart era |
| 2023 | 3.2 | Extension types, interop improvements |
| 2024 | 3.3 | Extension 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 typesSound 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 benullat runtime, the program will throwNull 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:#fffThis 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
Popular Packages by Category #
| Category | Package | Use Case |
|---|---|---|
| HTTP Client | http, dio | Requests to external APIs |
| JSON | json_serializable, freezed | Code generation for serialization |
| State Management | riverpod, bloc, provider | State management in Flutter |
| Database | drift, isar, hive | Local database |
| DI / Service Locator | get_it, injectable | Dependency injection |
| Testing | test, mockito, mocktail | Unit and integration testing |
| CLI | args, dcli | Command-line applications |
| Code Generation | build_runner, source_gen | Automatically 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
| Criteria | Dart (Flutter) | React Native | Swift/Kotlin | Kotlin 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
nullcan 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/awaitbuilt on Future and Stream — for most async needs, useFuturefor single values andStreamfor 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
dartCLI tooling covers everything — packages, format, analyze, test, and compile.
Next: Installation →