Interactive Flutter Course

Flutter Programming: Zero to Infinity

Begin with your first line of Dart, understand Flutter widgets and state, and finish by building a real application that retrieves Star Wars characters from SWAPI.

Your Zero-to-Infinity Journey

Stage What You Learn Result
Zero Dart syntax and program structure You can write small Dart programs.
Beginner Flutter widgets and layouts You can design application screens.
Intermediate State, forms, lists and navigation You can build interactive applications.
Advanced Futures, JSON and REST APIs You can use live online data.
Infinity Complete SWAPI Flutter project You combine everything in one application.
DartPad setup: Every code block below is a complete main.dart program and has its own DartPad.

0 What Is Flutter?

Learning objectives: understand Flutter, Dart, widgets and cross-platform application development.

Flutter is a user-interface toolkit used to create applications for Android, iOS, web, Windows, macOS and Linux. Flutter programs are written using the Dart programming language.

Dart

The programming language used to write the application logic.

Flutter

The framework that provides widgets, layouts, themes and application tools.

Widget

A building block of the interface, such as text, a button, a row or an entire screen.

Remember: In Flutter, almost everything displayed on the screen is a widget.

1 Dart Foundations

Learning objectives: variables, data types, operators, conditions and loops.
void main() {
  String student = 'Champak';
  int lessonsCompleted = 3;
  double score = 87.5;
  bool isLearningFlutter = true;

  print('Student: $student');
  print('Lessons: $lessonsCompleted');
  print('Score: $score');
  print('Learning Flutter: $isLearningFlutter');
}
DartPad 1: Dart FoundationsOpen DartPad separately

Practice

  1. Create variables for your name and age.
  2. Store marks for three subjects.
  3. Calculate and print the total.
  4. Print whether the total is greater than 150.

2 Functions and Classes

Learning objectives: reusable functions, objects, constructors and methods.
class Student {
  final String name;
  final int marks;

  Student(this.name, this.marks);

  String result() {
    return marks >= 40 ? 'Pass' : 'Fail';
  }
}

void main() {
  Student student = Student('Ravi', 72);

  print(student.name);
  print(student.result());
}
DartPad 2: Classes and ObjectsOpen DartPad separately

Practice

Create a Book class containing title, author and price. Add a method that displays the book information.

3 Your First Flutter Application

Learning objectives: main(), runApp(), MaterialApp, Scaffold and Text.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My First Flutter App'),
        ),
        body: const Center(
          child: Text(
            'Hello Flutter!',
            style: TextStyle(fontSize: 28),
          ),
        ),
      ),
    );
  }
}
DartPad 3: Hello FlutterOpen DartPad separately

Practice

  1. Change the application title.
  2. Display your name in the centre.
  3. Change the text size to 36.
  4. Add a background colour to the screen.

4 Widgets and Layouts

Learning objectives: Container, Row, Column, Padding, Expanded and Card.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Course Layout',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CoursePage(),
    );
  }
}

class CoursePage extends StatelessWidget {
  const CoursePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Widgets and Layouts'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              'Flutter Course',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 20),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: const [
                    Icon(
                      Icons.flutter_dash,
                      size: 60,
                      color: Colors.blue,
                    ),
                    SizedBox(height: 12),
                    Text(
                      'Learn one widget at a time',
                      textAlign: TextAlign.center,
                      style: TextStyle(fontSize: 18),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
DartPad 4: Widgets and LayoutsOpen DartPad separately

Practice

Create three course cards for Dart, Flutter and API Programming. Give every card an icon, heading and short description.

4A Building a Horizontal UI with Row

Learning objectives: arrange widgets horizontally, control alignment and use Expanded to share available width.

A Row places its children from left to right. The mainAxisAlignment property controls the horizontal direction, while crossAxisAlignment controls the vertical direction.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Row UI'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(20),
          child: Row(
            children: [
              Expanded(
                child: Container(
                  height: 120,
                  color: Colors.blue,
                  child: const Center(
                    child: Text(
                      'Dart',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 22,
                      ),
                    ),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Container(
                  height: 120,
                  color: Colors.teal,
                  child: const Center(
                    child: Text(
                      'Flutter',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 22,
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
DartPad 5: Row UIOpen DartPad separately

Practice

  1. Create three equal boxes in one row.
  2. Give every box a different icon and label.
  3. Change the spacing using spaceEvenly.

4B Building a Vertical UI with Column

Learning objectives: arrange widgets vertically, add spacing and create a simple profile screen.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Column UI'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: const [
              CircleAvatar(
                radius: 48,
                child: Icon(Icons.person, size: 52),
              ),
              SizedBox(height: 16),
              Text(
                'Champak Roy',
                style: TextStyle(
                  fontSize: 26,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SizedBox(height: 8),
              Text('Flutter Programming Instructor'),
              SizedBox(height: 20),
              Text('Learn one widget at a time.'),
            ],
          ),
        ),
      ),
    );
  }
}
DartPad 6: Column UIOpen DartPad separately

Practice

Add an email address, location and an ElevatedButton below the profile details.

4C Container, Padding, Margin and Decoration

Learning objectives: control size, spacing, colour, rounded corners, borders and shadows.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.blueGrey.shade50,
        appBar: AppBar(
          title: const Text('Container Design'),
        ),
        body: Center(
          child: Container(
            width: 300,
            margin: const EdgeInsets.all(20),
            padding: const EdgeInsets.all(24),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(18),
              border: Border.all(color: Colors.blue),
              boxShadow: const [
                BoxShadow(
                  blurRadius: 12,
                  spreadRadius: 2,
                  offset: Offset(0, 6),
                  color: Colors.black26,
                ),
              ],
            ),
            child: const Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.flutter_dash, size: 70, color: Colors.blue),
                SizedBox(height: 14),
                Text(
                  'Flutter UI Card',
                  style: TextStyle(
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  'Padding creates inner space. Margin creates outer space.',
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
DartPad 7: Container DesignOpen DartPad separately

Practice

Create a student identity card with a border, rounded corners, shadow, name, course and student number.

4D Buttons, Icons and Images

Learning objectives: use common button types, Material icons and network images.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Buttons and Images'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(16),
                child: Image.network(
                  'https://picsum.photos/700/300',
                  height: 180,
                  fit: BoxFit.cover,
                ),
              ),
              const SizedBox(height: 20),
              ElevatedButton.icon(
                onPressed: () {},
                icon: const Icon(Icons.play_arrow),
                label: const Text('Start Lesson'),
              ),
              OutlinedButton.icon(
                onPressed: () {},
                icon: const Icon(Icons.bookmark_border),
                label: const Text('Save Lesson'),
              ),
              TextButton(
                onPressed: () {},
                child: const Text('Read More'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
DartPad 8: Buttons, Icons and ImagesOpen DartPad separately

Practice

Add buttons for Like, Share and Download. Use a suitable icon for each button.

4E Stack and Positioned Widgets

Learning objectives: place widgets on top of one another and create overlay badges.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Stack UI'),
        ),
        body: Center(
          child: Stack(
            clipBehavior: Clip.none,
            children: [
              Container(
                width: 290,
                height: 180,
                decoration: BoxDecoration(
                  color: Colors.indigo,
                  borderRadius: BorderRadius.circular(18),
                ),
                child: const Center(
                  child: Text(
                    'Flutter Course',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 26,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              Positioned(
                top: -12,
                right: -12,
                child: Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 7,
                  ),
                  decoration: BoxDecoration(
                    color: Colors.orange,
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: const Text(
                    'NEW',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
DartPad 9: Stack and PositionedOpen DartPad separately

Practice

Add a price badge at the bottom-left and a favourite icon at the top-left of the course card.

4F Responsive UI with LayoutBuilder

Learning objectives: adapt a layout to narrow and wide screens using available width.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Responsive Flutter UI'),
        ),
        body: LayoutBuilder(
          builder: (context, constraints) {
            final bool isWide = constraints.maxWidth >= 650;

            final widgets = [
              const CourseBox(title: 'Dart', icon: Icons.code),
              const CourseBox(title: 'Flutter', icon: Icons.flutter_dash),
              const CourseBox(title: 'API', icon: Icons.cloud),
            ];

            return Padding(
              padding: const EdgeInsets.all(20),
              child: isWide
                  ? Row(
                      children: widgets
                          .map(
                            (widget) => Expanded(
                              child: Padding(
                                padding: const EdgeInsets.all(8),
                                child: widget,
                              ),
                            ),
                          )
                          .toList(),
                    )
                  : Column(
                      children: widgets
                          .map(
                            (widget) => Padding(
                              padding: const EdgeInsets.only(bottom: 12),
                              child: widget,
                            ),
                          )
                          .toList(),
                    ),
            );
          },
        ),
      ),
    );
  }
}

class CourseBox extends StatelessWidget {
  final String title;
  final IconData icon;

  const CourseBox({
    super.key,
    required this.title,
    required this.icon,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(icon, size: 52),
            const SizedBox(height: 10),
            Text(title, style: const TextStyle(fontSize: 20)),
          ],
        ),
      ),
    );
  }
}
DartPad 10: Responsive LayoutBuilder UIOpen DartPad separately

Practice

Change the breakpoint and test the program by resizing DartPad's output area.

4G Creating Reusable Custom Widgets

Learning objectives: reduce repeated code by creating a configurable custom widget.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Reusable Widgets'),
        ),
        body: const Padding(
          padding: EdgeInsets.all(20),
          child: Column(
            children: [
              CourseTile(
                title: 'Dart Foundations',
                subtitle: 'Variables, functions and classes',
                icon: Icons.code,
              ),
              CourseTile(
                title: 'Flutter UI',
                subtitle: 'Rows, columns and responsive layouts',
                icon: Icons.flutter_dash,
              ),
              CourseTile(
                title: 'REST API',
                subtitle: 'JSON, Future and live data',
                icon: Icons.cloud,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class CourseTile extends StatelessWidget {
  final String title;
  final String subtitle;
  final IconData icon;

  const CourseTile({
    super.key,
    required this.title,
    required this.subtitle,
    required this.icon,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        leading: CircleAvatar(child: Icon(icon)),
        title: Text(title),
        subtitle: Text(subtitle),
        trailing: const Icon(Icons.chevron_right),
      ),
    );
  }
}
DartPad 11: Reusable Custom WidgetsOpen DartPad separately

Practice

Add colour and an onTap callback as constructor parameters of CourseTile.

5 Stateful Applications

Learning objectives: mutable data, StatefulWidget and setState().
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Counter',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CounterPage(),
    );
  }
}

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;

  void increase() {
    setState(() {
      count++;
    });
  }

  void decrease() {
    if (count > 0) {
      setState(() {
        count--;
      });
    }
  }

  void reset() {
    setState(() {
      count = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Stateful Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Current count'),
            Text(
              '$count',
              style: const TextStyle(
                fontSize: 56,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 20),
            Wrap(
              spacing: 12,
              children: [
                ElevatedButton.icon(
                  onPressed: decrease,
                  icon: const Icon(Icons.remove),
                  label: const Text('Decrease'),
                ),
                ElevatedButton.icon(
                  onPressed: reset,
                  icon: const Icon(Icons.refresh),
                  label: const Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increase,
        tooltip: 'Increase',
        child: const Icon(Icons.add),
      ),
    );
  }
}
DartPad 12: Stateful CounterOpen DartPad separately

Practice

  1. Add a decrease button.
  2. Add a reset button.
  3. Prevent the counter from becoming negative.

6 Text Input and Forms

Learning objectives: text controllers, text fields, buttons and validation.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Student Form',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const StudentFormPage(),
    );
  }
}

class StudentFormPage extends StatefulWidget {
  const StudentFormPage({super.key});

  @override
  State<StudentFormPage> createState() => _StudentFormPageState();
}

class _StudentFormPageState extends State<StudentFormPage> {
  final formKey = GlobalKey<FormState>();
  final nameController = TextEditingController();
  final emailController = TextEditingController();
  String message = 'Complete the form below.';

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    super.dispose();
  }

  void submitForm() {
    if (formKey.currentState!.validate()) {
      setState(() {
        message =
            'Welcome ${nameController.text}! Your email is ${emailController.text}.';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Text Input and Forms')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Form(
          key: formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              TextFormField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: 'Name',
                  prefixIcon: Icon(Icons.person),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Please enter your name.';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || !value.contains('@')) {
                    return 'Please enter a valid email.';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: submitForm,
                child: const Text('Submit'),
              ),
              const SizedBox(height: 20),
              Text(
                message,
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 18),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
DartPad 13: Text Input and FormsOpen DartPad separately

Practice

Build a student form containing name, email and course. Display the entered information after the user presses Submit.

7 Lists and Data Models

Learning objectives: lists, model classes, ListView.builder() and reusable cards.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Course List',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CourseListPage(),
    );
  }
}

class Course {
  final String title;
  final String duration;
  final IconData icon;

  const Course({
    required this.title,
    required this.duration,
    required this.icon,
  });
}

class CourseListPage extends StatelessWidget {
  const CourseListPage({super.key});

  static const courses = [
    Course(
      title: 'Dart Foundations',
      duration: '4 lessons',
      icon: Icons.code,
    ),
    Course(
      title: 'Flutter Widgets',
      duration: '6 lessons',
      icon: Icons.widgets,
    ),
    Course(
      title: 'State Management',
      duration: '5 lessons',
      icon: Icons.sync,
    ),
    Course(
      title: 'REST API Programming',
      duration: '7 lessons',
      icon: Icons.cloud,
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Lists and Data Models')),
      body: ListView.builder(
        padding: const EdgeInsets.all(12),
        itemCount: courses.length,
        itemBuilder: (context, index) {
          final course = courses[index];
          return Card(
            child: ListTile(
              leading: CircleAvatar(child: Icon(course.icon)),
              title: Text(course.title),
              subtitle: Text(course.duration),
              trailing: const Icon(Icons.arrow_forward),
            ),
          );
        },
      ),
    );
  }
}
DartPad 14: Lists and ModelsOpen DartPad separately

Practice

Convert the list of strings into a list of Course objects containing title, duration and difficulty level.

8 Navigation Between Screens

Learning objectives: routes, screens, Navigator.push() and passing data.
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Navigation',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    const lessonTitle = 'Navigation Between Screens';

    return Scaffold(
      appBar: AppBar(title: const Text('Home Page')),
      body: Center(
        child: ElevatedButton.icon(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => const DetailsPage(
                  lessonTitle: lessonTitle,
                ),
              ),
            );
          },
          icon: const Icon(Icons.open_in_new),
          label: const Text('Open Lesson Details'),
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  final String lessonTitle;

  const DetailsPage({
    super.key,
    required this.lessonTitle,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details Page')),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Text(
            'You opened: $lessonTitle',
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 26),
          ),
        ),
      ),
    );
  }
}
DartPad 15: NavigationOpen DartPad separately

A real application normally contains several screens. For example, the SWAPI project will have a character list screen and a character details screen.

Practice

Create a home screen and a lesson details screen. Pass the lesson title from the first screen to the second screen.

9 Futures, Asynchronous Programming and JSON

Learning objectives: Future, async, await, JSON decoding and error handling.
Future<String> loadLesson() async {
  await Future.delayed(const Duration(seconds: 2));
  return 'Flutter lesson loaded successfully.';
}

Future<void> main() async {
  print('Loading lesson...');

  try {
    final message = await loadLesson();
    print(message);
  } catch (error) {
    print('Error: $error');
  }
}
DartPad 16: Futures and AsyncOpen DartPad separately

An API request does not normally finish immediately. Flutter therefore represents the future result using a Future.

Future

A value that will become available later.

async

Marks a function as asynchronous.

await

Waits for an asynchronous operation to finish.

JSON

A text format commonly returned by web APIs.

10 Calling a REST API

Learning objectives: HTTP requests, response codes, JSON conversion, loading states and failure states.

A REST API allows an application to request data from a server. Our final application requests Star Wars character data from SWAPI.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const ApiLessonApp());
}

class ApiLessonApp extends StatelessWidget {
  const ApiLessonApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'SWAPI Request',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ApiLessonPage(),
    );
  }
}

class ApiLessonPage extends StatefulWidget {
  const ApiLessonPage({super.key});

  @override
  State<ApiLessonPage> createState() => _ApiLessonPageState();
}

class _ApiLessonPageState extends State<ApiLessonPage> {
  late Future<List<String>> characterNames;

  @override
  void initState() {
    super.initState();
    characterNames = fetchCharacters();
  }

  Future<List<String>> fetchCharacters() async {
    final response = await http.get(
      Uri.parse('https://swapi.dev/api/people/'),
    );

    if (response.statusCode != 200) {
      throw Exception('Unable to load characters.');
    }

    final Map<String, dynamic> data = jsonDecode(response.body);
    final List<dynamic> results = data['results'] ?? [];

    return results
        .map((character) => character['name'].toString())
        .toList();
  }

  void reload() {
    setState(() {
      characterNames = fetchCharacters();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Calling a REST API'),
        actions: [
          IconButton(
            onPressed: reload,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: FutureBuilder<List<String>>(
        future: characterNames,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Center(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Text('Error: ${snapshot.error}'),
              ),
            );
          }

          final names = snapshot.data ?? [];
          return ListView.builder(
            itemCount: names.length,
            itemBuilder: (context, index) {
              return ListTile(
                leading: const Icon(Icons.person),
                title: Text(names[index]),
              );
            },
          );
        },
      ),
    );
  }
}
DartPad 17: REST API RequestOpen DartPad separately
DartPad supports Flutter and selected packages only. When a package is unavailable in DartPad, run the project locally after adding the package to pubspec.yaml.
Infinity Project

Flutter SWAPI Character Explorer

This project combines widgets, classes, state, lists, asynchronous programming, JSON parsing, HTTP requests, loading indicators, error handling and search.

Complete Flutter Program

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const StarWarsApp());
}

class StarWarsApp extends StatelessWidget {
  const StarWarsApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'SWAPI Character Explorer',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const CharacterPage(),
    );
  }
}

class StarWarsCharacter {
  final String name;
  final String height;
  final String mass;
  final String gender;
  final String birthYear;
  final String eyeColor;

  const StarWarsCharacter({
    required this.name,
    required this.height,
    required this.mass,
    required this.gender,
    required this.birthYear,
    required this.eyeColor,
  });

  factory StarWarsCharacter.fromJson(
    Map<String, dynamic> json,
  ) {
    return StarWarsCharacter(
      name: json['name'] ?? 'Unknown',
      height: json['height'] ?? 'Unknown',
      mass: json['mass'] ?? 'Unknown',
      gender: json['gender'] ?? 'Unknown',
      birthYear: json['birth_year'] ?? 'Unknown',
      eyeColor: json['eye_color'] ?? 'Unknown',
    );
  }
}

class CharacterService {
  static const String endpoint =
      'https://swapi.dev/api/people/';

  Future<List<StarWarsCharacter>>
      fetchCharacters() async {
    final response = await http.get(
      Uri.parse(endpoint),
    );

    if (response.statusCode != 200) {
      throw Exception(
        'Server returned status ${response.statusCode}',
      );
    }

    final Map<String, dynamic> decoded =
        jsonDecode(response.body);

    final List<dynamic> results =
        decoded['results'] ?? [];

    return results
        .map(
          (item) => StarWarsCharacter.fromJson(
            item as Map<String, dynamic>,
          ),
        )
        .toList();
  }
}

class CharacterPage extends StatefulWidget {
  const CharacterPage({super.key});

  @override
  State<CharacterPage> createState() =>
      _CharacterPageState();
}

class _CharacterPageState
    extends State<CharacterPage> {
  final CharacterService service =
      CharacterService();

  final TextEditingController searchController =
      TextEditingController();

  late Future<List<StarWarsCharacter>>
      characterFuture;

  String searchText = '';

  @override
  void initState() {
    super.initState();
    characterFuture = service.fetchCharacters();
  }

  @override
  void dispose() {
    searchController.dispose();
    super.dispose();
  }

  void reload() {
    setState(() {
      characterFuture = service.fetchCharacters();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'SWAPI Character Explorer',
        ),
        actions: [
          IconButton(
            onPressed: reload,
            tooltip: 'Reload',
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: TextField(
              controller: searchController,
              decoration: InputDecoration(
                labelText: 'Search characters',
                hintText: 'Example: Luke',
                prefixIcon:
                    const Icon(Icons.search),
                suffixIcon: searchText.isEmpty
                    ? null
                    : IconButton(
                        onPressed: () {
                          searchController.clear();

                          setState(() {
                            searchText = '';
                          });
                        },
                        icon: const Icon(Icons.clear),
                      ),
                border: const OutlineInputBorder(),
              ),
              onChanged: (value) {
                setState(() {
                  searchText =
                      value.trim().toLowerCase();
                });
              },
            ),
          ),
          Expanded(
            child: FutureBuilder<
                List<StarWarsCharacter>>(
              future: characterFuture,
              builder: (context, snapshot) {
                if (snapshot.connectionState ==
                    ConnectionState.waiting) {
                  return const Center(
                    child:
                        CircularProgressIndicator(),
                  );
                }

                if (snapshot.hasError) {
                  return ErrorPanel(
                    message: snapshot.error.toString(),
                    onRetry: reload,
                  );
                }

                final characters =
                    snapshot.data ?? [];

                final visibleCharacters =
                    characters.where((character) {
                  return character.name
                      .toLowerCase()
                      .contains(searchText);
                }).toList();

                if (visibleCharacters.isEmpty) {
                  return const Center(
                    child: Text(
                      'No matching characters found.',
                    ),
                  );
                }

                return ListView.builder(
                  padding: const EdgeInsets.fromLTRB(
                    12,
                    0,
                    12,
                    20,
                  ),
                  itemCount:
                      visibleCharacters.length,
                  itemBuilder: (context, index) {
                    final character =
                        visibleCharacters[index];

                    return CharacterCard(
                      character: character,
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class CharacterCard extends StatelessWidget {
  final StarWarsCharacter character;

  const CharacterCard({
    super.key,
    required this.character,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(
        vertical: 7,
      ),
      child: ListTile(
        contentPadding: const EdgeInsets.all(16),
        leading: CircleAvatar(
          child: Text(
            character.name.isEmpty
                ? '?'
                : character.name[0],
          ),
        ),
        title: Text(
          character.name,
          style: const TextStyle(
            fontWeight: FontWeight.bold,
          ),
        ),
        subtitle: Text(
          'Birth year: ${character.birthYear}',
        ),
        trailing:
            const Icon(Icons.chevron_right),
        onTap: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return CharacterDetailsPage(
                  character: character,
                );
              },
            ),
          );
        },
      ),
    );
  }
}

class CharacterDetailsPage
    extends StatelessWidget {
  final StarWarsCharacter character;

  const CharacterDetailsPage({
    super.key,
    required this.character,
  });

  Widget detail(
    BuildContext context,
    String label,
    String value,
  ) {
    return Card(
      child: ListTile(
        title: Text(label),
        subtitle: Text(
          value,
          style: Theme.of(context)
              .textTheme
              .titleMedium,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(character.name),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Icon(
            Icons.person,
            size: 90,
          ),
          const SizedBox(height: 20),
          detail(
            context,
            'Name',
            character.name,
          ),
          detail(
            context,
            'Height',
            '${character.height} cm',
          ),
          detail(
            context,
            'Mass',
            '${character.mass} kg',
          ),
          detail(
            context,
            'Gender',
            character.gender,
          ),
          detail(
            context,
            'Birth year',
            character.birthYear,
          ),
          detail(
            context,
            'Eye colour',
            character.eyeColor,
          ),
        ],
      ),
    );
  }
}

class ErrorPanel extends StatelessWidget {
  final String message;
  final VoidCallback onRetry;

  const ErrorPanel({
    super.key,
    required this.message,
    required this.onRetry,
  });

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(
              Icons.error_outline,
              size: 60,
            ),
            const SizedBox(height: 14),
            const Text(
              'Unable to load characters',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 10),
            Text(
              message,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 18),
            ElevatedButton.icon(
              onPressed: onRetry,
              icon: const Icon(Icons.refresh),
              label: const Text('Try Again'),
            ),
          ],
        ),
      ),
    );
  }
}
DartPad 18: SWAPI Character ExplorerOpen DartPad separately
Possible DartPad limitation: if the final example cannot import package:http in the available DartPad environment, run it as a normal Flutter project and add the HTTP package with flutter pub add http.

Infinity Challenges

  1. Load the next SWAPI results page.
  2. Add filters for gender and eye colour.
  3. Display planets and starships.
  4. Add favourite characters.
  5. Store favourites locally.
  6. Add light and dark themes.
  7. Add pagination or infinite scrolling.
  8. Create a separate service and model file.
  9. Add unit and widget tests.
  10. Deploy the application as Flutter Web.

Course Completion Checklist