Dart Tutorial: Learn UI Language from Scratch (2026)
Dart is the language behind Flutter, but it is also a capable general-purpose language for server and CLI applications. I have built Flutter apps for both iOS and Android, and the developer experience — hot reload, strong tooling, consistent type system — is among the best I have encountered.
Dart feels like a modernized Java with JavaScript-like asynchronous patterns. It is familiar to developers from either ecosystem while adding its own twists like sound null safety and isolates for concurrency.
Null Safety
Sound null safety means non-nullable types can never contain null. Variables are non-nullable by default; add ? for nullable. The compiler analyzes flow: after a null check, a nullable variable is promoted to non-nullable. The ! operator asserts non-null, crashing on null. ?? provides a fallback value.
String? maybe = null;
String name = maybe ?? 'guest';
if (maybe != null) {
// maybe promoted to String here
print(maybe.length);
}
// Late initialization
late String config = loadConfig();
Streams
Streams provide an asynchronous sequence of events. A Stream emits data or error events and a done event when complete. Use await for to iterate over a stream. transform applies stream transformers for filtering and mapping. Stream controllers manage custom streams.
Stream countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
await Future.delayed(Duration(seconds: 1));
}
}
Future main() async {
await for (final n in countStream(5)) {
print(n);
}
}
Futures and Async
Future<T> represents a value that will be available later. async functions return futures; await pauses until the future completes. Multiple futures run concurrently with Future.wait. Dart has a single-threaded event loop, so async I/O does not block the UI.
Future fetchData() async {
final resp = await http.get(Uri.parse('https://api.com'));
return resp.body;
}
Future main() async {
final f1 = fetchData();
final f2 = fetchData();
final results = await Future.wait([f1, f2]);
print(results);
}
Collections
Dart provides List, Set, and Map with collection literals. Spread operators (... and ...?) insert elements from another collection. Collection-if and collection-for enable conditional and iterative inclusion. Null-aware spreads handle nullable collections safely.
final list = [1, 2, 3];
final copy = [0, ...list, 4];
final nullable = [1, null, 3];
final safe = ['a', ...?nullable];
final evens = [
for (final n in list)
if (n.isEven) n
];
final map = {
for (final n in list)
n.toString(): n
};
Mixins
Mixins reuse code across class hierarchies without multiple inheritance. Declared with mixin, they provide methods and fields that classes incorporate with with. Mixins cannot be instantiated directly. You can restrict mixin usage with on to limit which classes can use them.
mixin Logger {
void log(String msg) => print('[log] $msg');
}
mixin Timestamp on Logger {
@override
void log(String msg) {
super.log('${DateTime.now()}: $msg');
}
}
class Service with Logger, Timestamp {}
final svc = Service();
svc.log('started');
Isolates
Isolates are independent workers with their own memory heap. Unlike threads, isolates do not share memory; they communicate via message passing. Isolate.spawn creates a new isolate. For simpler use cases, compute (in Flutter) runs a function in a separate isolate and returns the result.
import 'dart:isolate';
void heavyWork(SendPort send) {
var sum = 0.0;
for (int i = 0; i < 100000000; i++) {
sum += i * 0.5;
}
send.send(sum);
}
Future main() async {
final rp = ReceivePort();
await Isolate.spawn(heavyWork, rp.sendPort);
final result = await rp.first;
print(result);
}
Frequently Asked Questions
Dart vs JavaScript?
Dart is optionally typed with sound null safety, compiles to native or JS, and has a richer standard library. JS has a larger ecosystem.
What is the difference between async and async*?
async returns a Future. async* returns a Stream and uses yield to emit values. Use async* for multiple asynchronous values.
When should I use an isolate?
For CPU-intensive operations that would block the UI thread. For I/O, async/await is sufficient.
What does the cascade notation do?
.. chains method calls on the same object without repeating the variable name. Useful for builder patterns.
Originally published on Ayodhyyya. Last updated June 1, 2026.