Dart
The programming language used to write the application logic.
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.
| 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. |
main.dart program and has its own DartPad.
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.
The programming language used to write the application logic.
The framework that provides widgets, layouts, themes and application tools.
A building block of the interface, such as text, a button, a row or an entire screen.
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');
}
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());
}
Create a Book class containing title,
author and price. Add a method that displays the book
information.
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),
),
),
),
);
}
}
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),
),
],
),
),
),
],
),
),
);
}
}
Create three course cards for Dart, Flutter and API Programming. Give every card an icon, heading and short description.
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,
),
),
),
),
),
],
),
),
),
);
}
}
spaceEvenly.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.'),
],
),
),
),
);
}
}
Add an email address, location and an
ElevatedButton below the profile details.
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,
),
],
),
),
),
),
);
}
}
Create a student identity card with a border, rounded corners, shadow, name, course and student number.
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'),
),
],
),
),
),
);
}
}
Add buttons for Like, Share and Download. Use a suitable icon for each button.
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),
),
),
),
],
),
),
),
);
}
}
Add a price badge at the bottom-left and a favourite icon at the top-left of the course 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,
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)),
],
),
),
);
}
}
Change the breakpoint and test the program by resizing DartPad's output area.
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),
),
);
}
}
Add colour and an onTap callback as
constructor parameters of CourseTile.
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),
),
);
}
}
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),
),
],
),
),
),
);
}
}
Build a student form containing name, email and course. Display the entered information after the user presses Submit.
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),
),
);
},
),
);
}
}
Convert the list of strings into a list of
Course objects containing title,
duration and difficulty level.
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),
),
),
),
);
}
}
A real application normally contains several screens. For example, the SWAPI project will have a character list screen and a character details screen.
Create a home screen and a lesson details screen. Pass the lesson title from the first screen to the second screen.
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');
}
}
An API request does not normally finish immediately.
Flutter therefore represents the future result using a
Future.
A value that will become available later.
Marks a function as asynchronous.
Waits for an asynchronous operation to finish.
A text format commonly returned by web APIs.
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]),
);
},
);
},
),
);
}
}
pubspec.yaml.
This project combines widgets, classes, state, lists, asynchronous programming, JSON parsing, HTTP requests, loading indicators, error handling and search.
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'),
),
],
),
),
);
}
}
package:http in the available DartPad
environment, run it as a normal Flutter project and add
the HTTP package with
flutter pub add http.