Introduction to Dart: a language built for client apps
By Ann Tech · 11 March 2026
Dart was designed for building user interfaces. That design intent explains features that seem redundant with other languages — until you use them in a UI context.
Null safety
Null pointer exceptions are the most common crash in mobile apps. Dart's sound null safety makes null-related crashes a compile-time error rather than a runtime surprise:
String name = 'Alice'; // Non-nullable — guaranteed never null
String? nickname; // Nullable — must be checked before use
final upper = nickname?.toUpperCase(); // Safe access — returns null if nickname is null
final display = nickname ?? name; // Null-coalescing
nickname ??= 'Ali'; // Null-aware assignment
Async/await for UI responsiveness
Network calls, database reads, and file access are all async. Dart's async/await makes sequential async code readable:
Future<void> loadDashboard() async {
setState(() => isLoading = true);
try {
final user = await userRepo.getUser(userId);
final orders = await orderRepo.getOrders(userId);
setState(() {
_user = user;
_orders = orders;
isLoading = false;
});
} catch (e) {
setState(() { error = e.toString(); isLoading = false; });
}
}
// Parallel fetches — both requests fire simultaneously
Future<void> loadDashboardFast() async {
final results = await Future.wait([
userRepo.getUser(userId),
orderRepo.getOrders(userId),
]);
}
Isolates for background work
Dart is single-threaded within an isolate. CPU-heavy work on the main isolate blocks the UI thread and causes jank:
// Move JSON parsing off the UI thread
final products = await compute(parseProducts, rawJsonString);
List<Product> parseProducts(String json) {
final list = jsonDecode(json) as List;
return list.map((e) => Product.fromJson(e as Map<String, dynamic>)).toList();
}
// For longer-running work:
final result = await Isolate.run(() => expensiveComputation(data));
Extension methods
extension StringX on String {
bool get isValidEmail =>
RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
String capitalize() =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
'[email protected]'.isValidEmail // true
'hello'.capitalize() // 'Hello'
AOT vs JIT compilation
- JIT (development): Dart VM interprets code — enables hot reload
- AOT (release): compiles to native ARM — fast startup, no VM overhead
This dual-mode explains why Flutter hot reload is instant yet production builds perform like native apps.
Records and patterns (Dart 3)
// Records: lightweight anonymous tuples with named fields
(String name, int age) getUser() => ('Alice', 30);
final (name, age) = getUser();
// Exhaustive pattern matching
switch (shape) {
case Circle(radius: final r): return math.pi * r * r;
case Rectangle(width: final w, height: final h): return w * h;
}
Common pitfalls
Using dynamic to avoid type errors. dynamic opts out of type checking entirely. Mistakes become runtime crashes instead of compile-time errors. Use generics or sealed classes instead.
Blocking the event loop. Any synchronous work over ~2ms — large JSON parsing, image processing, sorting large lists — causes dropped frames on the UI thread. Always offload heavy computation to compute() or Isolate.run().
Sign in to like, dislike, or report.