Flutter in 30 Minutes
What Is Flutter?
Flutter is Google's open-source UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase. It uses the Dart programming language and renders every pixel itself rather than wrapping native platform controls.
Three things make Flutter distinctive:
- Declarative UI — You describe what the screen should look like, and Flutter figures out how to update it efficiently.
- Hot Reload — Change your code and see the result in under a second, without losing app state.
- One codebase, many platforms — iOS, Android, web, Windows, macOS, and Linux from the same Dart source files.
Dart in Five Minutes
Dart is a strongly typed, object-oriented language. If you know Java, JavaScript, or Kotlin, Dart will feel familiar.
Variables and Types
String name = 'Flutter'; // Explicit type
var count = 42; // Inferred as int
final createdAt = DateTime.now(); // Set once at runtime
const pi = 3.14159; // Compile-time constant
Functions
int add(int a, int b) => a + b;
String greet({required String name, String title = 'friend'}) {
return 'Hello, $title $name!';
}
Classes
class Dog {
final String name;
int age;
Dog(this.name, this.age);
void bark() => print('$name says woof!');
}
Async / Await
Future<String> fetchUser() async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
return response.body;
}
Null Safety
Dart enforces null safety. A variable cannot be null unless you explicitly allow it.
String name = 'Alice'; // Cannot be null
String? nickname; // Can be null
print(nickname?.length); // Safe access — returns null if nickname is null
print(nickname!.length); // Force unwrap — crashes if null
The Widget Tree
Everything in Flutter is a widget. Widgets are small, composable building blocks that nest inside each other to form a widget tree.
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Center(
child: Text('Hello, Flutter!'),
),
),
)
This tree is:
- MaterialApp contains Scaffold
- Scaffold contains AppBar and Center
- Center contains Text
When something changes, Flutter compares the new tree to the old one and only repaints what is different. This is why Flutter is fast.
StatelessWidget vs StatefulWidget
StatelessWidget — No mutable state
Use when the widget's output depends only on its constructor arguments.
class Greeting extends StatelessWidget {
final String name;
const Greeting({required this.name});
@override
Widget build(BuildContext context) {
return Text('Hello, $name!');
}
}
StatefulWidget — Has mutable state
Use when the widget needs to change over time (counters, toggles, form input).
class Counter extends StatefulWidget {
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: Text('Increment'),
),
],
);
}
}
The key rule: call setState() whenever you change state, so Flutter knows to rebuild.
Layout Essentials
Flutter uses three core layout widgets for most screens:
Row (horizontal) and Column (vertical)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.star),
Icon(Icons.star),
Icon(Icons.star),
],
)
Stack (layered)
Stack(
children: [
Image.asset('background.png'),
Positioned(bottom: 16, right: 16, child: Text('Overlay')),
],
)
Expanded and Flexible
These tell a Row or Column how to divide available space.
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.red)),
Expanded(flex: 1, child: Container(color: Colors.blue)),
],
)
Lists and Scrolling
For a few items, use ListView directly. For many items, use ListView.builder — it only builds the items currently visible on screen.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].name),
subtitle: Text(items[index].description),
onTap: () => navigateToDetail(items[index]),
);
},
)
Navigation
Flutter uses a Navigator to manage a stack of screens (routes).
// Push a new screen onto the stack
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailScreen(item: item)),
);
// Pop back to the previous screen
Navigator.pop(context);
For larger apps, consider a routing package like GoRouter that uses URL-based paths.
User Input and Forms
final _controller = TextEditingController();
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
)
For validated forms, wrap fields in a Form widget with a GlobalKey<FormState>:
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: TextFormField(
validator: (value) => value!.isEmpty ? 'Required' : null,
),
)
// Validate on submit
if (_formKey.currentState!.validate()) { /* proceed */ }
State Management
For small apps, setState is fine. As your app grows, you will want a dedicated state management approach.
Provider is the most common starting point:
// 1. Define a model
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => _items;
void add(Item item) {
_items.add(item);
notifyListeners(); // Tell listeners to rebuild
}
}
// 2. Provide it near the top of the tree
ChangeNotifierProvider(create: (_) => CartModel(), child: MyApp())
// 3. Consume it in any descendant widget
final cart = context.watch<CartModel>();
Text('Items: ${cart.items.length}')
Other popular options include Riverpod, Bloc, and GetX.
HTTP and APIs
Use the http package to fetch data from REST APIs.
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => Post.fromJson(json)).toList();
}
throw Exception('Failed to load posts');
}
Display async data with FutureBuilder:
FutureBuilder<List<Post>>(
future: fetchPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (_, i) => ListTile(title: Text(snapshot.data![i].title)),
);
},
)
Theming and Styling
Define a consistent look with ThemeData:
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
textTheme: TextTheme(
headlineLarge: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
),
)
Access theme values anywhere:
final colors = Theme.of(context).colorScheme;
Container(color: colors.primaryContainer)
Testing
Flutter has three levels of testing:
- Unit tests — Pure Dart logic, no widgets.
- Widget tests — Build and interact with widgets in a test harness.
- Integration tests — Run the full app on a device or emulator.
// Widget test example
testWidgets('Counter increments', (tester) async {
await tester.pumpWidget(MyApp());
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
Where to Go from Here
This crash course covers the highlights. The full course goes deep on each topic across 20 modules:
| Modules | Focus |
|---|---|
| 0-4 | Dart language + widget fundamentals |
| 5-9 | Layout, interaction, lists, forms |
| 10-14 | Navigation, theming, state, APIs, storage |
| 15-19 | Custom widgets, animations, packages, testing, capstone |
Start with Module 0: What Is Flutter and work through the foundations. Each module builds on the previous one, with hands-on exercises and a capstone project at the end.