A hands-on foundation course covering Dart, asynchronous programming, Flutter fundamentals, and tooling, with detailed, runnable examples.
This is Phase 0 of a 30-hour in-person Flutter training plan. It assumes you know basic programming (variables, loops, functions) in any language. By the end, you will be able to write idiomatic Dart, understand how Flutter builds UI from widgets, set up your environment, and ship a small multi-screen app with forms, navigation, and interactive state.
pubspec.yaml, dependencies, assets, and fontsDart is a statically typed, object-oriented language with sound null safety. You can run Dart code on its own, without Flutter, which makes it ideal for learning.
Create a file called hello.dart:
void main() {
print('Hello, Flutter learners!');
}
Run it:
dart run hello.dart
main() is the entry point of every Dart program.
void main() {
// Type inference with var
var name = 'prajwal'; // inferred as String
var count = 10; // inferred as int
// Explicit types
String city = 'Kathmandu';
int age = 25;
double height = 5.9;
bool isTrainer = true;
// final: assigned once, at runtime
final DateTime now = DateTime.now();
// const: compile-time constant
const double pi = 3.14159;
// late: initialized later, before first use
late String config;
config = 'production';
print('$name from $city, age $age');
print('Next year: ${age + 1}'); // expression inside interpolation
}
Key rules:
var lets Dart infer the type, but the type is then fixed. You cannot assign a string to a var that was inferred as int.final means "set once." const means "known at compile time."$variable or ${expression}) instead of concatenation.void main() {
int a = 7;
double b = 2.5;
num c = 3; // num can hold int or double
String s = 'Dart';
String multi = '''
This is a
multi-line string''';
print(s.toUpperCase()); // DART
print(s.length); // 4
print(s.contains('ar')); // true
print('a,b,c'.split(',')); // [a, b, c]
print(a ~/ 2); // 3 (integer division)
print(a % 2); // 1 (remainder)
print(a / 2); // 3.5
}
void main() {
int x = 5;
// Arithmetic: + - * / ~/ %
// Comparison: == != > < >= <=
// Logical: && || !
x += 3; // x = 8
x++; // x = 9
// Ternary
String label = x > 5 ? 'big' : 'small';
// Null-aware operators (covered in detail in Module 2)
String? input;
String result = input ?? 'default';
print('$label $result');
}
void main() {
int score = 82;
// if / else
if (score >= 90) {
print('A');
} else if (score >= 75) {
print('B');
} else {
print('C');
}
// for loop
for (int i = 1; i <= 3; i++) {
print('Round $i');
}
// for-in loop
for (final fruit in ['apple', 'banana', 'mango']) {
print(fruit);
}
// while loop
int n = 3;
while (n > 0) {
print('Countdown: $n');
n--;
}
// switch (classic)
String grade = 'B';
switch (grade) {
case 'A':
print('Excellent');
case 'B':
print('Good');
default:
print('Keep going');
}
}
In Dart 3, a case body no longer needs a break. There is no implicit fall-through.
// Standard function
int add(int a, int b) {
return a + b;
}
// Arrow (expression-bodied) function
int multiply(int a, int b) => a * b;
// Functions are first-class values
void main() {
final op = add;
print(op(2, 3)); // 5
// Anonymous function (lambda)
final square = (int n) => n * n;
print(square(4)); // 16
// Passing a function as an argument
final numbers = [1, 2, 3, 4];
final doubled = numbers.map((n) => n * 2).toList();
print(doubled); // [2, 4, 6, 8]
}
class Person {
String name;
int age;
Person(this.name, this.age);
void introduce() {
print('Hi, I am $name and I am $age years old.');
}
}
void main() {
final p = Person('Sita', 22);
p.introduce();
}
Dart has no public, private, or protected keywords. Prefix a member with an underscore (_balance) to make it private to its library (usually the file).
Write a Dart program that:
Student class with name and a list of marksPass if the average is 40 or above, otherwise FailDart's type system separates nullable from non-nullable types. This eliminates a whole class of runtime crashes.
void main() {
String name = 'prajwal';
// name = null; // Compile error: String cannot be null
String? nickname; // Nullable: may be null
print(nickname); // null
}
void main() {
String? nickname;
// ?? : fallback value if null
print(nickname ?? 'No nickname');
// ??= : assign only if currently null
nickname ??= 'Rana';
print(nickname); // Rana
// ?. : safe member access
String? maybe;
print(maybe?.length); // null, no crash
// ! : assert non-null (use sparingly)
String sure = nickname!;
print(sure.length);
// ?[] : safe index access
List<int>? scores;
print(scores?[0]); // null
}
Dart "remembers" null checks:
void greet(String? name) {
if (name == null) {
print('Hello, stranger');
return;
}
// Here, name is promoted to String (non-null)
print('Hello, ${name.toUpperCase()}');
}
late VariablesUse late for non-nullable values that cannot be set in the constructor but will be set before use:
class Database {
late final String connection;
void init() {
connection = 'db://localhost';
}
}
Accessing a late variable before assignment throws an error at runtime, so use it carefully.
Rule of thumb: prefer non-nullable types. Use ? only when "no value" is a meaningful state.
Flutter relies heavily on named parameters (every widget uses them), so master this.
// Positional parameters (required, order matters)
int sum(int a, int b) => a + b;
// Optional positional parameters: [ ]
String greet(String name, [String? title]) {
return title == null ? 'Hello, $name' : 'Hello, $title $name';
}
// Named parameters: { }
void createUser({required String name, int age = 18, String? email}) {
print('$name, $age, ${email ?? 'no email'}');
}
void main() {
print(sum(1, 2));
print(greet('Sita'));
print(greet('Sita', 'Dr.'));
createUser(name: 'Ram');
createUser(name: 'Gita', age: 30, email: 'gita@example.com');
createUser(age: 40, name: 'Hari'); // order does not matter
}
required forces callers to pass a named parameter.required must be nullable or have a default.Text('Hi', style: ..., maxLines: 2).class Product {
final String name;
final double price;
final int quantity;
// 1. Generative constructor with initializing formals and named params
const Product({
required this.name,
required this.price,
this.quantity = 1,
});
// 2. Named constructor
Product.free(String name) : this(name: name, price: 0);
// 3. Constructor with initializer list
Product.fromMap(Map<String, dynamic> map)
: name = map['name'] as String,
price = (map['price'] as num).toDouble(),
quantity = map['quantity'] as int? ?? 1;
// 4. Factory constructor: may return an existing or different instance
factory Product.sample() => const Product(name: 'Sample', price: 9.99);
double get total => price * quantity;
@override
String toString() => 'Product($name, $price x $quantity)';
}
void main() {
const a = Product(name: 'Book', price: 12.5, quantity: 2);
final b = Product.free('Sticker');
final c = Product.fromMap({'name': 'Pen', 'price': 3, 'quantity': 5});
final d = Product.sample();
print(a.total); // 25.0
print(b); // Product(Sticker, 0.0 x 1)
print(c);
print(d);
}
Why const constructors matter in Flutter: a const widget is created once at compile time and reused, which helps Flutter skip unnecessary rebuilds (see Module 4).
abstract class Shape {
double area();
}
class Circle extends Shape {
Circle(this.radius);
final double radius;
@override
double area() => 3.14159 * radius * radius;
}
class Rectangle extends Shape {
Rectangle(this.width, this.height);
final double width, height;
@override
double area() => width * height;
}
void main() {
final fruits = <String>['apple', 'banana', 'mango'];
fruits.add('orange');
fruits.remove('banana');
print(fruits.first); // apple
print(fruits.length); // 3
print(fruits.contains('mango'));
final numbers = [5, 3, 8, 1];
numbers.sort();
print(numbers); // [1, 3, 5, 8]
// Functional style
final evens = numbers.where((n) => n.isEven).toList();
final squares = numbers.map((n) => n * n).toList();
final total = numbers.fold<int>(0, (sum, n) => sum + n);
print(evens); // [8]
print(squares);
print(total); // 17
}
void main() {
final tags = {'flutter', 'dart', 'flutter'};
print(tags); // {flutter, dart}
final a = {1, 2, 3};
final b = {3, 4};
print(a.union(b)); // {1, 2, 3, 4}
print(a.intersection(b)); // {3}
}
void main() {
final capitals = <String, String>{
'Nepal': 'Kathmandu',
'India': 'New Delhi',
};
capitals['Japan'] = 'Tokyo';
print(capitals['Nepal']); // Kathmandu
print(capitals['Mars']); // null (lookup returns nullable)
for (final entry in capitals.entries) {
print('${entry.key} -> ${entry.value}');
}
}
These make building lists declarative. You will use them constantly when building widget lists.
void main() {
final base = [1, 2, 3];
final extended = [0, ...base, 4]; // spread
print(extended); // [0, 1, 2, 3, 4]
final isAdmin = true;
final menu = [
'Home',
'Profile',
if (isAdmin) 'Admin Panel', // collection if
];
final labels = [
for (final n in base) 'Item $n', // collection for
];
print(menu);
print(labels);
}
Extensions add functionality to existing types without modifying or subclassing them.
extension StringX on String {
String capitalize() =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
bool get isValidEmail =>
RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(this);
}
extension IntX on int {
Duration get seconds => Duration(seconds: this);
Duration get milliseconds => Duration(milliseconds: this);
}
extension ListX<T> on List<T> {
T? get secondOrNull => length >= 2 ? this[1] : null;
}
void main() {
print('flutter'.capitalize()); // Flutter
print('a@b.com'.isValidEmail); // true
print(3.seconds); // 0:00:03.000000
print([10, 20, 30].secondOrNull); // 20
}
In Flutter, extensions are great for things like context.theme or context.screenWidth helpers.
(String, int) getUser() => ('prajwal', 30);
({String name, int age}) getNamedUser() => (name: 'prajwal', age: 30);
void main() {
final user = getUser();
print(user.$1); // prajwal
print(user.$2); // 30
final named = getNamedUser();
print(named.name);
}
Records are handy when a function needs to return more than one value without defining a class.
void main() {
final (name, age) = ('Sita', 22);
print('$name is $age');
final {'id': id, 'title': title} = {'id': 1, 'title': 'Intro'};
print('$id: $title');
final [first, second, ...rest] = [1, 2, 3, 4, 5];
print('$first $second $rest'); // 1 2 [3, 4, 5]
}
String describe(int n) => switch (n) {
0 => 'zero',
1 || 2 || 3 => 'small',
> 3 && < 100 => 'medium',
_ => 'large',
};
void main() {
print(describe(0)); // zero
print(describe(50)); // medium
print(describe(500)); // large
}
This pattern is extremely useful for UI state (loading, success, error), which you will use heavily in later phases.
sealed class Result<T> {}
class Success<T> extends Result<T> {
Success(this.data);
final T data;
}
class Failure<T> extends Result<T> {
Failure(this.message);
final String message;
}
class Loading<T> extends Result<T> {}
String render(Result<String> result) => switch (result) {
Success(:final data) => 'Got: $data',
Failure(:final message) => 'Error: $message',
Loading() => 'Loading...',
};
void main() {
print(render(Loading()));
print(render(Success('Hello')));
print(render(Failure('No internet')));
}
Because the class is sealed, the compiler forces you to handle every subtype. If you add a new one later, every switch that misses it becomes a compile error.
if-case and Guardsvoid check(Object value) {
if (value case int n when n > 10) {
print('Big int: $n');
} else if (value case String s) {
print('String of length ${s.length}');
} else {
print('Something else');
}
}
String that returns true if the string is a palindrome.PaymentMethod sealed class with Card, Cash, and Wallet subtypes. Write a switch expression that returns a fee for each.Product objects, use where, map, and fold to compute the total price of items costing more than 100.Apps wait for things constantly: network responses, file reads, database queries, user input. Dart uses a single-threaded event loop, so waiting must never block the thread. That is what asynchronous programming is for.
A Future<T> represents a value that will be available later (or an error).
Future<String> fetchGreeting() {
return Future.delayed(
const Duration(seconds: 2),
() => 'Hello from the future!',
);
}
void main() {
print('Start');
fetchGreeting().then((value) {
print(value);
});
print('End');
}
Output:
Start
End
Hello from the future!
Notice that End prints before the result. The program did not wait.
async/await lets you write asynchronous code that reads like synchronous code.
Future<String> fetchUserName() async {
await Future.delayed(const Duration(seconds: 1));
return 'Sita';
}
Future<int> fetchUserAge() async {
await Future.delayed(const Duration(seconds: 1));
return 22;
}
Future<void> main() async {
print('Loading...');
final name = await fetchUserName();
final age = await fetchUserAge();
print('$name is $age'); // takes about 2 seconds total
}
The code above runs sequentially (about 2 seconds). If calls are independent, run them in parallel:
Future<void> main() async {
final results = await Future.wait([
fetchUserName(),
fetchUserAge(),
]);
print(results); // [Sita, 22], takes about 1 second
}
For different return types, use records (Dart 3):
Future<void> main() async {
final (name, age) = await (fetchUserName(), fetchUserAge()).wait;
print('$name is $age');
}
Use try/catch/finally exactly as you would with synchronous code.
Future<String> riskyCall() async {
await Future.delayed(const Duration(milliseconds: 500));
throw Exception('Server unreachable');
}
Future<void> main() async {
try {
final data = await riskyCall();
print(data);
} on FormatException catch (e) {
print('Bad format: ${e.message}');
} on Exception catch (e) {
print('Something failed: $e');
} finally {
print('Cleanup always runs');
}
}
class NetworkException implements Exception {
NetworkException(this.message, {this.statusCode});
final String message;
final int? statusCode;
@override
String toString() => 'NetworkException($statusCode): $message';
}
Future<String> loadProfile() async {
await Future.delayed(const Duration(milliseconds: 300));
throw NetworkException('Not found', statusCode: 404);
}
Future<void> main() async {
try {
await loadProfile();
} on NetworkException catch (e) {
if (e.statusCode == 404) {
print('Profile does not exist');
}
}
}
Future<void> main() async {
try {
final value = await fetchUserName()
.timeout(const Duration(milliseconds: 200));
print(value);
} on TimeoutException {
print('Took too long');
}
}
Add import 'dart:async'; at the top for TimeoutException.
await, so you receive a Future instead of a valueasync on a function but never awaiting anything insideA Future gives you one value later. A Stream gives you many values over time: location updates, chat messages, button clicks, WebSocket events.
async*Stream<int> countdown(int from) async* {
for (int i = from; i >= 0; i--) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
Future<void> main() async {
await for (final value in countdown(3)) {
print(value); // 3, 2, 1, 0 (one per second)
}
print('Liftoff!');
}
void main() {
final subscription = countdown(5).listen(
(value) => print('Got $value'),
onError: (error) => print('Error: $error'),
onDone: () => print('Stream finished'),
);
// Later, stop listening:
Future.delayed(const Duration(seconds: 2), () {
subscription.cancel();
});
}
Future<void> main() async {
final stream = countdown(10)
.where((n) => n.isEven)
.map((n) => 'Even: $n')
.take(3);
await for (final item in stream) {
print(item);
}
}
Use a controller when you want to push values into a stream manually.
import 'dart:async';
class Counter {
final _controller = StreamController<int>.broadcast();
int _value = 0;
Stream<int> get stream => _controller.stream;
void increment() {
_value++;
_controller.add(_value);
}
void dispose() => _controller.close();
}
void main() {
final counter = Counter();
counter.stream.listen((v) => print('Listener A: $v'));
counter.stream.listen((v) => print('Listener B: $v'));
counter.increment();
counter.increment();
counter.dispose();
}
A single-subscription stream (the default) allows only one listener. A broadcast stream allows many.
Important Flutter note: always cancel subscriptions and close controllers in dispose() to avoid memory leaks. You will practice this in Module 4.
Future<int> fetchScore(String player) that delays one second and returns a score. Fetch three players in parallel with Future.wait.try/catch and retry up to three times before giving up.Stream<int> that emits the current second every second for 5 seconds, then use await for to print only odd values.In Flutter, UI is built by composing small, immutable descriptions called widgets: text, buttons, padding, alignment, even the app itself.
Here is the smallest useful Flutter app:
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello, Flutter!'),
),
),
),
);
}
This builds a tree: MaterialApp → Scaffold → Center → Text.
Widgets nest to form a widget tree. Parent widgets decide how children are laid out.
MaterialApp
└── Scaffold
├── AppBar
│ └── Text('Home')
└── Column
├── Text('Welcome')
└── ElevatedButton
└── Text('Start')
Flutter maintains three related trees internally:
You write widgets. Flutter reconciles them with elements, and only updates render objects that actually changed. That is why rebuilding widgets is cheap.
Use it when the UI depends only on the data passed in.
class GreetingCard extends StatelessWidget {
const GreetingCard({
super.key,
required this.name,
this.subtitle,
});
final String name;
final String? subtitle;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Hello, $name',
style: Theme.of(context).textTheme.headlineSmall,
),
if (subtitle != null) ...[
const SizedBox(height: 8),
Text(subtitle!),
],
],
),
),
);
}
}
Notice everything from Module 2 in action: named parameters, required, nullable types, const constructors, collection-if, and spread.
Use it when the UI changes over time because of internal state.
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text(
'Count: $_count',
style: Theme.of(context).textTheme.displayMedium,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
setState() tells Flutter "my state changed, call build again." Changing _count without setState updates the variable but not the screen.
class LifecycleDemo extends StatefulWidget {
const LifecycleDemo({super.key, required this.title});
final String title;
@override
State<LifecycleDemo> createState() => _LifecycleDemoState();
}
class _LifecycleDemoState extends State<LifecycleDemo> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
// Called ONCE when the State is created.
// Initialize controllers, subscriptions, start timers.
_controller = TextEditingController();
debugPrint('initState');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Called after initState, and when an InheritedWidget
// (Theme, MediaQuery, ...) this widget depends on changes.
debugPrint('didChangeDependencies');
}
@override
void didUpdateWidget(covariant LifecycleDemo oldWidget) {
super.didUpdateWidget(oldWidget);
// Called when the parent rebuilds with new constructor args.
if (oldWidget.title != widget.title) {
debugPrint('title changed');
}
}
@override
Widget build(BuildContext context) {
// Called whenever the UI needs to be (re)built.
// Must be fast and free of side effects.
debugPrint('build');
return TextField(controller: _controller);
}
@override
void deactivate() {
// Removed from the tree (may be reinserted).
debugPrint('deactivate');
super.deactivate();
}
@override
void dispose() {
// Called ONCE when permanently removed.
// Release controllers, cancel subscriptions and timers.
_controller.dispose();
debugPrint('dispose');
super.dispose();
}
}
Order for a new widget: createState → initState → didChangeDependencies → build.
Order on removal: deactivate → dispose.
Golden rule: whatever you create in initState, you release in dispose.
build()setState after the widget is disposed (check if (!mounted) return; after an await)Future<void> _load() async {
final data = await fetchData();
if (!mounted) return; // widget may be gone by now
setState(() => _data = data);
}
This one sentence explains most layout bugs. Flutter's layout rule:
Column(
mainAxisAlignment: MainAxisAlignment.center, // along the column (vertical)
crossAxisAlignment: CrossAxisAlignment.start, // across the column (horizontal)
children: [
const Text('Title'),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.star),
const SizedBox(width: 8),
const Text('4.8'),
],
),
],
)
Row(
children: [
Expanded(flex: 2, child: Container(height: 50, color: Colors.blue)),
Expanded(flex: 1, child: Container(height: 50, color: Colors.orange)),
],
)
The blue box takes two-thirds of the width, and the orange box one-third.
// Problem: Text inside a Row is too long, causing a yellow/black overflow stripe
Row(
children: [
Icon(Icons.info),
Text('A very very very very long sentence that does not fit on screen'),
],
)
// Fix: let the text take only the remaining space
Row(
children: [
const Icon(Icons.info),
const SizedBox(width: 8),
const Expanded(
child: Text(
'A very very very very long sentence that does not fit on screen',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
)
Why it happens: a Row gives its children unbounded width. The Text wants all the room it needs, so it overflows. Expanded hands it bounded constraints.
This occurs when you put a ListView directly inside a Column. The Column gives unbounded height, and the ListView tries to fill infinity. Fix:
Column(
children: [
const Text('Header'),
Expanded(
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
),
),
],
)
Padding, Align, Center, SizedBox, ContainerStack + Positioned for overlapping layersConstrainedBox, LimitedBox for explicit constraintsSingleChildScrollView, ListView, GridView for scrollingSafeArea to avoid notches and system barsLayoutBuilder and MediaQuery for responsive layoutsLayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 600;
return isWide ? const Text('Tablet layout') : const Text('Phone layout');
},
)
A rebuild means Flutter calls build() again to produce a new widget description. Rebuilds are normal and fast, but unnecessary rebuilds of large subtrees waste work.
What triggers a rebuild:
setState() on a StateInheritedWidget the widget depends on changing (e.g. theme, MediaQuery)const// Rebuilt every time the parent rebuilds
Padding(padding: EdgeInsets.all(8), child: Text('Static'))
// Canonicalized at compile time; Flutter can skip it
const Padding(padding: EdgeInsets.all(8), child: Text('Static'))
// Bad: the whole page rebuilds on every tap
class PageA extends StatefulWidget {
const PageA({super.key});
@override
State<PageA> createState() => _PageAState();
}
class _PageAState extends State<PageA> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
const ExpensiveHeader(), // rebuilt unnecessarily
Text('$_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Add'),
),
],
);
}
}
// Better: isolate the changing part into its own widget
class CounterSection extends StatefulWidget {
const CounterSection({super.key});
@override
State<CounterSection> createState() => _CounterSectionState();
}
class _CounterSectionState extends State<CounterSection> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Add'),
),
],
);
}
}
// In the page:
// Column(children: [const ExpensiveHeader(), const CounterSection()])
Now only CounterSection rebuilds on tap.
// Avoid: _buildHeader() is rebuilt as part of the parent each time
Widget _buildHeader() => const Text('Header');
// Prefer: a separate widget class gets its own element and can be const
class Header extends StatelessWidget {
const Header({super.key});
@override
Widget build(BuildContext context) => const Text('Header');
}
Keys help Flutter match widgets to the right state when items are reordered or removed.
ListView(
children: [
for (final todo in todos)
TodoTile(key: ValueKey(todo.id), todo: todo),
],
)
Without keys, removing the first item can cause Flutter to reuse the wrong state for the remaining tiles, which shows up as checkboxes or text fields appearing "swapped."
Add a print in build to observe when it fires:
@override
Widget build(BuildContext context) {
debugPrint('CounterSection rebuilt');
...
}
You can also use the Widget Rebuild Stats in DevTools (Module 5).
ElevatedButton(
onPressed: () => debugPrint('Tapped'),
child: const Text('Click me'),
)
TextButton(onPressed: () {}, child: const Text('Cancel'))
IconButton(onPressed: () {}, icon: const Icon(Icons.favorite))
GestureDetector(
onTap: () => debugPrint('Container tapped'),
child: Container(width: 100, height: 100, color: Colors.teal),
)
Switch(value: true, onChanged: (v) {})
Row and Expanded.StatefulWidget that starts a Timer.periodic in initState and cancels it in dispose. Display the elapsed seconds.flutter/bin to your PATH.flutter doctor
flutter doctor reports what is installed and what is missing, with a checkmark or an X per item. Fix each issue it lists. For Android licenses:
flutter doctor --android-licenses
flutter create my_first_app
cd my_first_app
flutter devices # list connected devices and emulators
flutter run # run on the selected device
my_first_app/
├── lib/
│ └── main.dart # your Dart code lives here
├── test/ # automated tests
├── android/ # Android host project
├── ios/ # iOS host project
├── assets/ # (you create) images, fonts, data files
├── pubspec.yaml # dependencies and configuration
└── analysis_options.yaml # lint rules
While flutter run is active in your terminal:
r (hot reload): injects updated code into the running app, keeps current state. Best for UI tweaks.R (hot restart): restarts the app from main(), resetting all state. Needed after changing main(), initState logic, global variables, or enum/class structure.q: quit.Typical experiment: change the color in your counter app and press r. The counter value stays. Now press R, and the counter resets to zero.
In an IDE, saving a file triggers hot reload automatically.
If hot reload does not reflect your change, try hot restart. If you added a new dependency or native config, do a full stop and flutter run again.
Launch from your IDE or run:
dart devtools
Key tabs for beginners:
debugPrint, framework logs, and errors.Practice: turn on Debug Paint in the Inspector to visualize padding, alignment, and widget boundaries. This is the fastest way to understand an unexpected layout.
name: my_first_app
description: A Phase 0 practice app.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.5.0
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
http: ^1.2.0
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/data/sample.json
fonts:
- family: Poppins
fonts:
- asset: assets/fonts/Poppins-Regular.ttf
- asset: assets/fonts/Poppins-Bold.ttf
weight: 700
Your SDK constraint and package versions will differ depending on your installed Flutter version. Keep what flutter create generated and only add what you need.
Important: YAML is whitespace-sensitive. Use two spaces for indentation (never tabs), and keep indentation consistent. A single misaligned line is the most common cause of pubspec errors.
^1.2.0 means "1.2.0 or higher, but below 2.0.0" (compatible updates)any allows anything (avoid)1.2.0 are rarely neededdependencies: packages your app needs at runtimedev_dependencies: tools needed only during development (testing, linting, code generation)flutter pub add http # add a dependency
flutter pub add --dev build_runner
flutter pub remove http
flutter pub get # fetch packages after editing pubspec.yaml
flutter pub upgrade # upgrade within constraints
flutter pub outdated # see what has newer versions
Use the package in code:
import 'package:http/http.dart' as http;
Future<void> fetchPost() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
debugPrint('Status: ${response.statusCode}');
}
On Android, networking requires the INTERNET permission in android/app/src/main/AndroidManifest.xml for release builds (debug builds usually include it already):
<uses-permission android:name="android.permission.INTERNET" />
Choosing packages wisely: on pub.dev, check the likes, pub points, popularity, last-updated date, null-safety/Dart 3 support, and platform support. Prefer well-maintained packages with a verified publisher.
assets/images/, assets/data/pubspec.yaml (see above)flutter pub get (and usually hot restart)// Image
Image.asset(
'assets/images/logo.png',
width: 120,
fit: BoxFit.contain,
)
// Text/JSON file
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
Future<List<dynamic>> loadSample() async {
final raw = await rootBundle.loadString('assets/data/sample.json');
return jsonDecode(raw) as List<dynamic>;
}
Common mistake: adding files but not listing the folder (with the trailing slash) in pubspec.yaml. This yields the error "Unable to load asset."
assets/fonts/flutter: fonts: (see pubspec.yaml above)MaterialApp(
theme: ThemeData(
fontFamily: 'Poppins',
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const MyHome(),
)
Or use a font on a single widget:
const Text(
'Custom font',
style: TextStyle(fontFamily: 'Poppins', fontWeight: FontWeight.w700),
)
If bold text does not look bold, ensure you declared a font file with the matching weight.
debugPrint rather than print for long output.flutter analyze to catch problems before running.flutter clean followed by flutter pub get when the build behaves strangely.Let's combine everything: Dart features, widget composition, state, navigation, form validation, and a reusable widget.
Requirements:
Navigator.popTaskTile widgetCreate a new project (flutter create task_tracker) and replace lib/main.dart with:
import 'package:flutter/material.dart';
void main() => runApp(const TaskApp());
// ---------- Model (immutable, with copyWith) ----------
class Task {
const Task({required this.title, this.done = false});
final String title;
final bool done;
Task copyWith({String? title, bool? done}) =>
Task(title: title ?? this.title, done: done ?? this.done);
}
// ---------- App root ----------
class TaskApp extends StatelessWidget {
const TaskApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Task Tracker',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const TaskListScreen(),
);
}
}
// ---------- Screen 1: List ----------
class TaskListScreen extends StatefulWidget {
const TaskListScreen({super.key});
@override
State<TaskListScreen> createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State<TaskListScreen> {
final List<Task> _tasks = [
const Task(title: 'Install Flutter SDK', done: true),
const Task(title: 'Run flutter doctor'),
const Task(title: 'Build my first widget'),
];
Future<void> _openAddScreen() async {
final title = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (_) => const AddTaskScreen()),
);
if (!mounted || title == null) return;
setState(() => _tasks.add(Task(title: title)));
}
void _toggle(int index) {
setState(() {
_tasks[index] = _tasks[index].copyWith(done: !_tasks[index].done);
});
}
@override
Widget build(BuildContext context) {
final completed = _tasks.where((t) => t.done).length;
return Scaffold(
appBar: AppBar(
title: Text('Tasks ($completed/${_tasks.length} done)'),
),
body: _tasks.isEmpty
? const Center(child: Text('No tasks yet. Add one!'))
: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
return TaskTile(
task: task,
onToggle: () => _toggle(index),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _openAddScreen,
icon: const Icon(Icons.add),
label: const Text('Add task'),
),
);
}
}
// ---------- Reusable widget ----------
class TaskTile extends StatelessWidget {
const TaskTile({super.key, required this.task, required this.onToggle});
final Task task;
final VoidCallback onToggle;
@override
Widget build(BuildContext context) {
return CheckboxListTile(
value: task.done,
onChanged: (_) => onToggle(),
title: Text(
task.title,
style: TextStyle(
decoration: task.done ? TextDecoration.lineThrough : null,
),
),
);
}
}
// ---------- Screen 2: Form with validation ----------
class AddTaskScreen extends StatefulWidget {
const AddTaskScreen({super.key});
@override
State<AddTaskScreen> createState() => _AddTaskScreenState();
}
class _AddTaskScreenState extends State<AddTaskScreen> {
final _formKey = GlobalKey<FormState>();
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
String? _validate(String? value) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Please enter a task title';
if (text.length < 3) return 'Title must be at least 3 characters';
return null; // null means valid
}
void _submit() {
if (_formKey.currentState!.validate()) {
Navigator.pop(context, _controller.text.trim());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('New Task')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _controller,
autofocus: true,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
labelText: 'Task title',
border: OutlineInputBorder(),
),
validator: _validate,
onFieldSubmitted: (_) => _submit(),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _submit,
child: const Text('Save'),
),
),
],
),
),
),
);
}
}
Run it with flutter run.
String? return type in _validate; title == null check after navigationrequired: Task, TaskTilecopyWith: safe state updatesawait Navigator.push(...) returns a Future that completes when the screen popsmounted check after an awaitdispose() releases the TextEditingControllerTaskTile is a small reusable widget with its own file-level classsetState is only used where the data livesDismissible (remember to give each item a ValueKey).assets/data/tasks.json using rootBundle.ThemeData.SegmentedButton and pattern matching in a switch expression.Task into its own file and organize code into models/, screens/, and widgets/ folders.Before moving to Phase 1, you should be able to do the following without looking at notes:
Dart
var, final, const, and late?, ??, ?., and ! correctlymap, where, and fold on a listswitch expression over a sealed classAsync
Future is and why await doesn't block the UItry/catch/finallyStream, and cancel the subscriptionFlutter
Row overflow and an unbounded-height ListViewinitState, build, and disposeconst and smaller widgetsNavigator.push and pass data backTooling
flutter doctor and interpret its output// Variables
var x = 1; final y = 2; const z = 3; late String s;
// Null safety
String? a; a ?? 'x'; a?.length; a!; a ??= 'x';
// Named params
void f({required int a, int b = 0, String? c}) {}
// Collections
[1, 2, ...other, if (cond) 3, for (final i in xs) i * 2]
// Async
Future<T> f() async { final v = await g(); return v; }
Stream<int> s() async* { yield 1; }
// Pattern matching
final r = switch (v) { 0 => 'zero', > 0 => 'pos', _ => 'neg' };
final (a, b) = (1, 2);
// Flutter
setState(() => _x++);
Navigator.push(context, MaterialPageRoute(builder: (_) => Page()));
Navigator.pop(context, result);
# CLI
flutter doctor flutter create app flutter run
flutter pub add pkg flutter pub get flutter analyze
flutter clean # r = hot reload, R = hot restart
Phase 1 builds on this foundation with navigation and screen-state preservation, structured state management, unit testing, accessibility, localization, and keyboard handling. The habits you built here (small widgets, immutable models, disposing resources, and reading error messages carefully) are the ones that will carry you through.
Happy coding, and keep building small things every day.
If you found this useful, follow for the next part of the series: Flutter Phase 1.
No comments yet. Start the conversation below.