Last updated: August 27, 2026
After completing this codelab, students will be able to:
StatelessWidget, StatefulWidget, Container, Row, Column, and Expanded.In a declarative approach, code describes the interface based on the current state. When state changes, Flutter rebuilds the relevant UI. Developers focus on the relationship between data and presentation instead of manually changing each UI element.
Widget build(BuildContext context) {
return Text('Hello, $name');
}
StatelessWidget is suitable when output is determined by parent configuration.StatefulWidget has a State object for data that changes during its lifecycle.Container combines size, padding, margin, decoration, and a child.Row arranges children horizontally, while Column arranges them vertically.Expanded shares available space inside a Row or Column.MaterialApp and Material 3 widgets support Android and cross-platform design patterns. CupertinoApp and Cupertino widgets provide iOS-style experiences. Choose components based on user experience requirements, not only platform names.
Responsive layouts adapt their structure to the available space. Use LayoutBuilder or MediaQuery to read dimensions and change the number of columns or layout direction. Avoid fixed pixel sizes for elements that must adapt.
A theme centralizes colors, typography, and component shapes. Provide theme and darkTheme on MaterialApp. For accessibility, use sufficient contrast, meaningful labels, readable text sizes, and do not communicate information through color alone.
Before starting, observe the target of this codelab: a student dashboard that shows one column on narrow screens and two columns on wide screens, with consistent light and dark themes.
Before the responsive dashboard, practice the basic widgets by building a simple profile card. Create a new project or temporarily replace lib/main.dart:
import 'package:flutter/material.dart';
void main() => runApp(const ProfileApp());
class ProfileApp extends StatelessWidget {
const ProfileApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(child: ProfileCard()),
),
);
}
}
class ProfileCard extends StatelessWidget {
const ProfileCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 320,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const CircleAvatar(child: Icon(Icons.person)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Student Name',
style: TextStyle(fontWeight: FontWeight.bold)),
Text('...type your name here...'),
],
),
),
],
),
const SizedBox(height: 12),
const Row(children: [
Expanded(child: Text('Student ID')),
Text('...type your student ID here...'),
]),
const Row(children: [
Expanded(child: Text('Class')),
Text('...type your class here...'),
]),
],
),
);
}
}
Expanded from the name row, observe the overflow warning or layout behavior, then restore it.mainAxisSize: MainAxisSize.min with the default value and observe the card height change.Email) using the same Row + Expanded pattern.flutter create responsive_dashboard
cd responsive_dashboard
flutter run
Open lib/main.dart. Create the following simple profile dashboard and run it on an emulator or physical device.
import 'package:flutter/material.dart';
void main() => runApp(const DashboardApp());
class DashboardApp extends StatelessWidget {
const DashboardApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark, colorSchemeSeed: Colors.indigo),
themeMode: ThemeMode.system,
home: const DashboardPage(),
);
}
}
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Student Dashboard')),
body: LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth >= 700 ? 2 : 1;
return GridView.count(
padding: const EdgeInsets.all(16),
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2.6,
children: const [
DashboardCard(title: 'Assignments', value: '8'),
DashboardCard(title: 'Attendance', value: '92%'),
DashboardCard(title: 'Portfolio', value: 'Ready'),
DashboardCard(title: 'Current week', value: '02'),
],
);
},
),
);
}
}
class DashboardCard extends StatelessWidget {
const DashboardCard({required this.title, required this.value, super.key});
final String title;
final String value;
@override
Widget build(BuildContext context) {
return Card(child: Padding(padding: const EdgeInsets.all(20), child: Row(children: [Expanded(child: Text(title)), Text(value, style: Theme.of(context).textTheme.headlineSmall)])));
}
}
So far the dashboard is still a StatelessWidget. Convert DashboardApp into a StatefulWidget and add a CupertinoSwitch (a Cupertino widget) to the AppBar to toggle the theme manually — while directly comparing Material and Cupertino components:
class DashboardApp extends StatefulWidget {
const DashboardApp({super.key});
@override
State<DashboardApp> createState() => _DashboardAppState();
}
class _DashboardAppState extends State<DashboardApp> {
bool isDark = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark, colorSchemeSeed: Colors.indigo),
themeMode: isDark ? ThemeMode.dark : ThemeMode.light,
home: DashboardPage(
isDark: isDark,
onDarkChanged: (value) => setState(() => isDark = value),
),
);
}
}
Adjust DashboardPage to receive the state and callback:
class DashboardPage extends StatelessWidget {
const DashboardPage({
required this.isDark,
required this.onDarkChanged,
super.key,
});
final bool isDark;
final ValueChanged<bool> onDarkChanged;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Student Dashboard'),
actions: [
Row(
children: [
Icon(isDark ? Icons.dark_mode : Icons.light_mode),
const SizedBox(width: 4),
CupertinoSwitch(
value: isDark,
onChanged: onDarkChanged,
),
const SizedBox(width: 12),
],
),
],
),
body: LayoutBuilder(
// ... the previous GridView code, unchanged
),
);
}
}
700 breakpoint and observe the column count.themeMode to ThemeMode.dark, then restore ThemeMode.system.Semantics or meaningful labels to important screen-reader elements.Extend the dashboard into an Academic Overview page:
Row, Column, Expanded, and Container.CupertinoSwitch or Switch.adaptive).screenshots/.After completing the independent implementation, use AI only to compare two layout alternatives. Work through the following challenge:
GridView version and a LayoutBuilder + Column version. Explain the responsive and accessibility trade-offs."Expanded actually causes an overflow inside a Row; show failing example code and its fix."Passing criteria: the AI suggestion you adopt actually works, remains responsive, does not reduce accessibility, and you can explain every decision during code review, not merely copy the AI output.
Once the main assignment works, clean up your code:
InfoCard) that receives title and value, removing widget duplication.Theme.of(context) so they follow the light/dark theme automatically.const kWideBreakpoint = 700;) so it is defined only once.flutter analyze and ensure there are no new errors or warnings.Add widget tests in the test/ folder to verify the responsive behavior. Override the screen size using tester.view:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:responsive_dashboard/main.dart';
void main() {
testWidgets('Dashboard shows one column on a narrow screen', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(const DashboardApp());
final width = tester.getSize(find.byType(Card)).width;
expect(width, lessThan(700));
});
testWidgets('Dashboard shows two columns on a wide screen', (tester) async {
tester.view.physicalSize = const Size(1200, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(const DashboardApp());
final width = tester.getSize(find.byType(Card)).width;
expect(width, greaterThan(500));
});
}
Run them with flutter test. Both tests must pass before submitting the assignment. Store the tests in the test/ folder of this week's assignment.
flutter analyze produces no errors.flutter test passes all responsive widget tests.test/ folder, and README are stored in the Week 2 assignment folder.Expanded help, and when can it cause a layout error?