Dart Introduction
The Language Behind Flutter
Explanation
Why Dart?
Dart powers Flutter, Google's UI toolkit for building natively compiled apps. It's designed for client development with features like hot reload, sound null safety, and excellent async support.
Key Concepts
- Sound Null Safety: No null pointer exceptions
- Hot Reload: See changes instantly
- Async/Await: First-class async support
- Strong Typing: With type inference
Dart vs JavaScript
// JavaScript
const user = { name: "Arthur", age: 30 };
const doubled = [1, 2, 3].map(n => n * 2);
// Dart
final user = {'name': 'Arthur', 'age': 30};
final doubled = [1, 2, 3].map((n) => n * 2).toList();
Demonstration
Example 1: Dart Basics
void main() {
// Variables
String name = 'Arthur'; // Explicit type
var age = 30; // Type inference
final email = 'art@bpc.com'; // Runtime constant
const pi = 3.14159; // Compile-time constant
// Null safety
String? nullable; // Can be null
String nonNull = 'hello'; // Cannot be null
// Late initialization
late String lateValue;
lateValue = 'initialized later';
// String interpolation
print('Hello, $name!');
print('Age in months: ${age * 12}');
// Multi-line strings
var multiLine = '''
This is a
multi-line string
''';
// Lists
List<int> numbers = [1, 2, 3, 4, 5];
var fruits = ['apple', 'banana', 'cherry'];
// List operations
fruits.add('date');
fruits.remove('apple');
// Spread operator
var more = [...numbers, 6, 7, 8];
// Collection if
var nav = [
'Home',
'Products',
if (isAdmin) 'Admin',
];
// Collection for
var squared = [
for (var n in numbers) n * n
];
// Maps
Map<String, dynamic> user = {
'name': 'Arthur',
'age': 30,
'active': true,
};
// Sets
Set<String> tags = {'dart', 'flutter', 'mobile'};
// Conditional expressions
var status = age >= 18 ? 'adult' : 'minor';
// Null-aware operators
String? maybeName;
var displayName = maybeName ?? 'Guest'; // If null, use 'Guest'
var length = maybeName?.length ?? 0; // Null-safe access
}
bool get isAdmin => true;
Example 2: Functions
// Basic function
String greet(String name) {
return 'Hello, $name!';
}
// Arrow function
String greetArrow(String name) => 'Hello, $name!';
// Optional positional parameters
String greetWithTime(String name, [String time = 'day']) {
return 'Good $time, $name!';
}
// Named parameters
String createUser({
required String name,
required String email,
String role = 'user',
}) {
return '$name ($email) - $role';
}
// First-class functions
void processUsers(List<String> users, Function(String) callback) {
for (var user in users) {
callback(user);
}
}
// Higher-order function
Function(int) multiplier(int factor) {
return (int n) => n * factor;
}
void main() {
print(greet('Arthur'));
print(greetWithTime('Sarah', 'morning'));
print(createUser(name: 'Arthur', email: 'art@bpc.com'));
// Lambda/anonymous function
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map((n) => n * 2).toList();
// Function as variable
var triple = multiplier(3);
print(triple(5)); // 15
// Callbacks
processUsers(['Alice', 'Bob'], (user) => print('Processing $user'));
}
Example 3: Classes
class User {
// Properties
final int id;
String name;
String email;
bool _active = true; // Private (underscore)
// Static
static int _counter = 0;
// Constructor
User(this.name, this.email) : id = ++_counter;
// Named constructor
User.guest() : this('Guest', 'guest@example.com');
// Factory constructor
factory User.fromJson(Map<String, dynamic> json) {
return User(json['name'], json['email']);
}
// Getter
bool get isActive => _active;
// Setter
set active(bool value) {
_active = value;
}
// Methods
String greet() => 'Hello, I\'m $name!';
void deactivate() {
_active = false;
}
// Static method
static int get count => _counter;
@override
String toString() => 'User($name, ${_active ? "active" : "inactive"})';
}
// Inheritance
class Admin extends User {
List<String> permissions;
Admin(String name, String email, {this.permissions = const []})
: super(name, email);
bool hasPermission(String perm) => permissions.contains(perm);
@override
String greet() => 'Admin ${super.greet()}';
}
// Mixins
mixin Timestamped {
DateTime? createdAt;
DateTime? updatedAt;
void touch() {
final now = DateTime.now();
createdAt ??= now;
updatedAt = now;
}
}
class Post with Timestamped {
String title;
String content;
Post(this.title, this.content);
}
// Abstract class / Interface
abstract class Repository<T> {
Future<T?> findById(int id);
Future<List<T>> findAll();
Future<T> save(T entity);
Future<void> delete(int id);
}
void main() {
var user = User('Arthur', 'art@bpc.com');
print(user.greet());
var guest = User.guest();
print(guest.name); // Guest
var admin = Admin('Sarah', 'sarah@example.com',
permissions: ['read', 'write', 'delete']);
print(admin.hasPermission('write')); // true
var post = Post('Hello', 'World');
post.touch();
print(post.createdAt);
}
Example 4: Async Programming
import 'dart:async';
// Future - single async value
Future<String> fetchUser(int id) async {
// Simulate API call
await Future.delayed(Duration(seconds: 1));
return 'User $id';
}
// Multiple async operations
Future<Map<String, dynamic>> fetchUserData(int id) async {
// Parallel execution
final results = await Future.wait([
fetchUser(id),
fetchUserPosts(id),
fetchUserComments(id),
]);
return {
'user': results[0],
'posts': results[1],
'comments': results[2],
};
}
Future<List<String>> fetchUserPosts(int id) async {
await Future.delayed(Duration(milliseconds: 500));
return ['Post 1', 'Post 2'];
}
Future<List<String>> fetchUserComments(int id) async {
await Future.delayed(Duration(milliseconds: 300));
return ['Comment 1'];
}
// Stream - multiple async values
Stream<int> countDown(int from) async* {
for (var i = from; i >= 0; i--) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// Stream controller
class EventBus {
final _controller = StreamController<String>.broadcast();
Stream<String> get events => _controller.stream;
void emit(String event) {
_controller.add(event);
}
void dispose() {
_controller.close();
}
}
void main() async {
// Await
try {
final user = await fetchUser(1);
print(user);
} catch (e) {
print('Error: $e');
}
// Then (callback style)
fetchUser(2).then((user) => print(user)).catchError((e) => print(e));
// Stream listening
await for (var count in countDown(5)) {
print(count);
}
// Stream with listen
final bus = EventBus();
final subscription = bus.events.listen((event) => print('Event: $event'));
bus.emit('user_logged_in');
bus.emit('page_viewed');
await Future.delayed(Duration(seconds: 1));
subscription.cancel();
bus.dispose();
}
Key Takeaways:
- Dart has sound null safety built-in
- Use
finalfor runtime constants,constfor compile-time - Classes support constructors, named constructors, factories
- Mixins enable code reuse without inheritance
- async/await and Streams for async programming
Imitation
Challenge 1: Create a Todo Class
Task: Build a Todo class with completion toggling and JSON serialization.
Solution
class Todo {
final int id;
String text;
bool completed;
DateTime createdAt;
static int _counter = 0;
Todo(this.text)
: id = ++_counter,
completed = false,
createdAt = DateTime.now();
Todo.fromJson(Map<String, dynamic> json)
: id = json['id'],
text = json['text'],
completed = json['completed'],
createdAt = DateTime.parse(json['createdAt']);
void toggle() {
completed = !completed;
}
Map<String, dynamic> toJson() => {
'id': id,
'text': text,
'completed': completed,
'createdAt': createdAt.toIso8601String(),
};
@override
String toString() => '[${completed ? "✓" : " "}] $text';
}
class TodoList {
final List<Todo> _todos = [];
void add(String text) => _todos.add(Todo(text));
void toggle(int id) {
final todo = _todos.firstWhere((t) => t.id == id);
todo.toggle();
}
void remove(int id) {
_todos.removeWhere((t) => t.id == id);
}
List<Todo> get all => List.unmodifiable(_todos);
List<Todo> get active => _todos.where((t) => !t.completed).toList();
List<Todo> get completed => _todos.where((t) => t.completed).toList();
}
Challenge 2: Create an Async Data Loader
Task: Build a generic DataLoader with caching and error handling.
Solution
class DataLoader<T> {
final Future<T> Function() _fetcher;
T? _cache;
DateTime? _cacheTime;
final Duration _cacheDuration;
bool _loading = false;
DataLoader(this._fetcher, {Duration? cacheDuration})
: _cacheDuration = cacheDuration ?? Duration(minutes: 5);
bool get _isCacheValid {
if (_cache == null || _cacheTime == null) return false;
return DateTime.now().difference(_cacheTime!) < _cacheDuration;
}
Future<T> load({bool forceRefresh = false}) async {
if (!forceRefresh && _isCacheValid) {
return _cache!;
}
if (_loading) {
// Wait for existing request
while (_loading) {
await Future.delayed(Duration(milliseconds: 100));
}
return _cache!;
}
_loading = true;
try {
_cache = await _fetcher();
_cacheTime = DateTime.now();
return _cache!;
} finally {
_loading = false;
}
}
void invalidate() {
_cache = null;
_cacheTime = null;
}
}
// Usage
final userLoader = DataLoader<User>(() => fetchUser(1));
final user = await userLoader.load();
Practice
Exercise 1: Shopping Cart
Difficulty: Intermediate
Build a ShoppingCart class with:
- Add/remove items
- Quantity management
- Price calculation
- JSON serialization
Exercise 2: State Management
Difficulty: Advanced
Create a simple state management solution:
- Observable state container
- Stream-based updates
- Undo/redo support
- Persistence
Summary
What you learned:
- Dart syntax and null safety
- Functions and closures
- Classes, inheritance, and mixins
- Async programming with Future and Stream
- Collection operations
Next Steps:
- Read: Flutter Fundamentals
- Practice: Build a console app
- Explore: Flutter documentation
