Flutter Snippets
About Flutter
Flutter lets you build mobile, desktop, and web apps from one Dart codebase. This track covers project setup, core widgets, async data loading, and a simple testing loop.
Create a New Flutter App
Generate a starter app, enter the project, and run it on a connected simulator or device.
flutter create habit_flow
cd habit_flow
flutter pub add http
flutter runRecommended Project Structure
Split features, data, and widgets early so the app stays navigable as screens grow.
lib/
main.dart
app.dart
features/
habits/
data/
habit_service.dart
models/
habit.dart
presentation/
habit_list_screen.dart
widgets/
habit_tile.dart
shared/
theme/
app_theme.dart
widgets/
app_scaffold.dart
test/
widget_test.dartBootstrap MaterialApp (lib/main.dart)
Keep app-level theme and routes near the entrypoint so every screen inherits the same setup.
import 'package:flutter/material.dart';
void main() {
runApp(const HabitFlowApp());
}
class HabitFlowApp extends StatelessWidget {
const HabitFlowApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Habit Flow',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const HabitListScreen(),
);
}
}
class HabitListScreen extends StatelessWidget {
const HabitListScreen({super.key});
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: Text('Your habits go here')),
);
}
}Reusable Stateless Widget
Move repeated UI into small widgets with explicit inputs instead of rebuilding layout inline.
import 'package:flutter/material.dart';
class HabitTile extends StatelessWidget {
const HabitTile({
super.key,
required this.name,
required this.streak,
required this.onTap,
});
final String name;
final int streak;
final VoidCallback onTap;
Widget build(BuildContext context) {
return ListTile(
title: Text(name),
subtitle: Text('Streak: $streak days'),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}Form State with StatefulWidget
Keep small, screen-local input state inside a StatefulWidget before reaching for heavier state libraries.
import 'package:flutter/material.dart';
class AddHabitScreen extends StatefulWidget {
const AddHabitScreen({super.key});
State<AddHabitScreen> createState() => _AddHabitScreenState();
}
class _AddHabitScreenState extends State<AddHabitScreen> {
final _controller = TextEditingController();
void dispose() {
_controller.dispose();
super.dispose();
}
void _submit() {
final name = _controller.text.trim();
if (name.isEmpty) return;
Navigator.pop(context, name);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add Habit')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(controller: _controller),
const SizedBox(height: 12),
ElevatedButton(onPressed: _submit, child: const Text('Save')),
],
),
),
);
}
}Model JSON Data with a Dart Class
Parsing at the edge keeps your widgets focused on rendering instead of data cleanup.
class Habit {
const Habit({
required this.id,
required this.name,
required this.streak,
});
final String id;
final String name;
final int streak;
factory Habit.fromJson(Map<String, dynamic> json) {
return Habit(
id: json['id'] as String,
name: json['name'] as String,
streak: json['streak'] as int,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'streak': streak,
};
}Fetch Remote Data in a Service
Wrap network calls in a service so screens depend on a clear API instead of raw HTTP details.
import 'dart:convert';
import 'package:http/http.dart' as http;
class HabitService {
Future<List<Habit>> fetchHabits() async {
final response = await http.get(Uri.parse('https://example.com/api/habits'));
if (response.statusCode != 200) {
throw Exception('Failed to load habits');
}
final data = jsonDecode(response.body) as List<dynamic>;
return data
.map((item) => Habit.fromJson(item as Map<String, dynamic>))
.toList();
}
}Render Async Data with FutureBuilder
FutureBuilder is a simple starting point for one-shot loading states and error handling.
class HabitListScreen extends StatelessWidget {
HabitListScreen({super.key});
final HabitService service = HabitService();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Habits')),
body: FutureBuilder<List<Habit>>(
future: service.fetchHabits(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final habits = snapshot.data ?? const [];
return ListView.builder(
itemCount: habits.length,
itemBuilder: (context, index) {
final habit = habits[index];
return HabitTile(
name: habit.name,
streak: habit.streak,
onTap: () {},
);
},
);
},
),
);
}
}Push to a Named Route
Start with Navigator APIs directly before adopting a routing package for larger apps.
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HabitListScreen(),
'/add-habit': (context) => const AddHabitScreen(),
},
)
FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/add-habit'),
child: const Icon(Icons.add),
)Widget Test for a Screen
Test visible behavior first so refactors do not break user-facing flows silently.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('shows add habit button', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
),
),
),
);
expect(find.byIcon(Icons.add), findsOneWidget);
});
}Next Learning Upgrades
Once the basics are comfortable, expand into production-ready Flutter patterns.
1. Add freezed or sealed classes for richer state models.
2. Add Riverpod or Bloc when local widget state stops scaling.
3. Add Dio interceptors for auth, retries, and logging.
4. Add integration tests for navigation and forms.
5. Add offline caching with Hive or Drift.