Common Widgets
You now know how to write your own widgets and arrange them. Most of the time, though, your widgets are built from Flutter's ready-made ones: text, icons, images, buttons, input fields, cards and dialogs. This lesson is a tour of the widgets you will use on almost every screen, with the modern Material 3 versions.
All examples import package:flutter/material.dart and assume they are placed inside a Scaffold in a MaterialApp, as in earlier lessons.
Text, Icon and Image
Text shows a string. TextStyle changes its size, weight and colour, and maxLines with overflow controls long text. Icon shows one of the built-in Material icons from the Icons class. Image.network loads a picture from a URL and Image.asset loads one bundled with the app (see Flutter Project Structure).
import 'package:flutter/material.dart';
class DishCard extends StatelessWidget {
const DishCard({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
'https://example.com/images/dosa.jpg',
height: 140,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stack) =>
const SizedBox(height: 140, child: Center(child: Icon(Icons.broken_image))),
),
Text('Masala Dosa', style: textTheme.titleLarge),
const Text(
'Crispy rice crepe with spiced potato filling, sambar and coconut chutney.',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Row(
children: [
Icon(Icons.star, color: Colors.amber, size: 18),
Text(' 4.6'),
SizedBox(width: 12),
Icon(Icons.timer_outlined, size: 18),
Text(' 20 min'),
],
),
],
);
}
}The card shows a picture 140 pixels tall stretched across the width, the title "Masala Dosa" in a large style, a description cut to two lines with "...", and a row with a yellow star, "4.6", a timer icon and "20 min". BoxFit.cover fills the box and crops the picture if needed. If the image cannot load (no internet, wrong URL), errorBuilder shows a broken-image icon instead. On Android, network images in a release build also need the internet permission; see Platform Permissions.
Use styles from the theme, such as textTheme.titleLarge and textTheme.bodyMedium, instead of hard-coding font sizes everywhere; the next lesson, Material 3 Theming, explains why.
Buttons
Material 3 has a family of buttons for different levels of importance:
| Widget | Use for |
|---|---|
FilledButton | The main action on a screen ("Pay ₹499") |
FilledButton.tonal | Important but secondary actions |
ElevatedButton | Actions that need to stand out from a patterned background |
OutlinedButton | Medium-emphasis actions ("Cancel order") |
TextButton | Low-emphasis actions, often in dialogs ("Not now") |
IconButton | A single icon, such as a delete or favourite button |
FloatingActionButton | The primary action of a screen, floating at the bottom |
import 'package:flutter/material.dart';
class CheckoutButtons extends StatelessWidget {
const CheckoutButtons({super.key, required this.onPay});
final VoidCallback onPay;
@override
Widget build(BuildContext context) {
return Column(
spacing: 8,
children: [
FilledButton.icon(
onPressed: onPay,
icon: const Icon(Icons.lock),
label: const Text('Pay ₹499'),
),
OutlinedButton(onPressed: () {}, child: const Text('Apply coupon')),
TextButton(onPressed: null, child: const Text('Pay later (not available)')),
IconButton(
onPressed: () {},
tooltip: 'Add to wishlist',
icon: const Icon(Icons.favorite_border),
),
],
);
}
}The column shows a filled button with a lock icon and "Pay ₹499", an outlined "Apply coupon" button, a greyed-out "Pay later (not available)" text button, and a heart icon button. Every button takes onPressed; passing null disables it. Add a tooltip to icon buttons: screen readers announce it and it appears on long press.
Text input with TextField
TextField lets the user type. To read or change its text from code, give it a TextEditingController, created in initState (or as a field) and disposed in dispose.
import 'package:flutter/material.dart';
class PincodeBox extends StatefulWidget {
const PincodeBox({super.key});
@override
State<PincodeBox> createState() => _PincodeBoxState();
}
class _PincodeBoxState extends State<PincodeBox> {
final _controller = TextEditingController();
String _message = '';
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _check() {
final pin = _controller.text.trim();
setState(() {
_message = pin.length == 6 ? 'Delivery available to $pin' : 'Enter a 6-digit PIN code';
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: const InputDecoration(
labelText: 'PIN code',
prefixIcon: Icon(Icons.location_on_outlined),
border: OutlineInputBorder(),
),
onSubmitted: (_) => _check(),
),
FilledButton(onPressed: _check, child: const Text('Check delivery')),
Text(_message),
],
);
}
}A bordered input labelled "PIN code" with a location icon appears, with a character counter "0/6" under it. Typing 124001 and tapping Check delivery shows "Delivery available to 124001"; typing fewer digits shows "Enter a 6-digit PIN code". keyboardType: TextInputType.number opens the number keyboard on phones. For several fields with validation, use a Form, which is covered in Forms & Validation.
Switch, Checkbox and RadioGroup
Selection controls show a value and report changes through onChanged; your State stores the value.
import 'package:flutter/material.dart';
enum Delivery { standard, express }
class OrderOptions extends StatefulWidget {
const OrderOptions({super.key});
@override
State<OrderOptions> createState() => _OrderOptionsState();
}
class _OrderOptionsState extends State<OrderOptions> {
bool _giftWrap = false;
bool _smsUpdates = true;
Delivery _delivery = Delivery.standard;
@override
Widget build(BuildContext context) {
return Column(
children: [
CheckboxListTile(
title: const Text('Gift wrap (+₹40)'),
value: _giftWrap,
onChanged: (v) => setState(() => _giftWrap = v ?? false),
),
SwitchListTile(
title: const Text('Order updates by SMS'),
value: _smsUpdates,
onChanged: (v) => setState(() => _smsUpdates = v),
),
RadioGroup<Delivery>(
groupValue: _delivery,
onChanged: (v) => setState(() => _delivery = v ?? Delivery.standard),
child: const Column(
children: [
RadioListTile(value: Delivery.standard, title: Text('Standard (3-5 days)')),
RadioListTile(value: Delivery.express, title: Text('Express (next day, +₹99)')),
],
),
),
],
);
}
}The screen shows an unticked "Gift wrap (+₹40)" checkbox, an "Order updates by SMS" switch that is on, and two radio options with "Standard" selected. Tapping a row toggles its control. RadioGroup holds the selected value for all the radio buttons inside it, so each RadioListTile only needs its own value.
Cards, ListTile, SnackBar and dialogs
Card is a surface with rounded corners, and ListTile is a ready-made row with a leading widget, a title, a subtitle and a trailing widget. A SnackBar is a short message at the bottom of the screen, shown through ScaffoldMessenger. A dialog is shown with showDialog, which returns a Future with the user's answer.
import 'package:flutter/material.dart';
class BookingTile extends StatelessWidget {
const BookingTile({super.key});
Future<void> _cancel(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Cancel booking?'),
content: const Text('₹120 cancellation charge will be deducted.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Keep')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Cancel booking')),
],
),
);
if (confirmed == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Booking cancelled. Refund in 5-7 days.')),
);
}
}
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: const Icon(Icons.train),
title: const Text('12015 Shatabdi, Coach C3, Seat 42'),
subtitle: const Text('New Delhi → Jaipur, 6 Dec'),
trailing: IconButton(
icon: const Icon(Icons.close),
tooltip: 'Cancel booking',
onPressed: () => _cancel(context),
),
),
);
}
}A card shows a train icon, the booking details and a close button. Tapping the close button opens a dialog titled "Cancel booking?" with Keep and Cancel booking buttons. Choosing Cancel booking closes the dialog and shows a snack bar at the bottom: "Booking cancelled. Refund in 5-7 days." Navigator.pop(context, true) closes the dialog and sends true back to showDialog. After an await, always check context.mounted before using context again, because the widget may have left the screen while the dialog was open.
Other widgets you will meet soon: CircularProgressIndicator and LinearProgressIndicator (loading), Divider (a thin line), CircleAvatar, Chip, Badge, Tooltip, NavigationBar (bottom tabs) and Drawer (side menu).
Common mistakes
- Forgetting to dispose a
TextEditingController. It leaks memory; dispose it indispose. - Using
contextafterawaitwithout checkingcontext.mounted. The analyzer warns about this for good reason. - Calling
Scaffold.of(context).showSnackBar. That API was removed; useScaffoldMessenger.of(context).showSnackBar. - Hard-coding colours and font sizes. Read them from
Theme.of(context)so the app stays consistent and supports dark mode.
Key points
Text,IconandImage.network/Image.assetdisplay content; give network images anerrorBuilder.- Material 3 buttons:
FilledButton,ElevatedButton,OutlinedButton,TextButton,IconButton,FloatingActionButton;onPressed: nulldisables them. TextFieldwith aTextEditingControllerreads typed text; dispose the controller.CheckboxListTile,SwitchListTileandRadioGroupwithRadioListTilehandle choices.CardandListTilebuild list rows;ScaffoldMessengershows SnackBars;showDialogreturns the user's answer.
Exercise
Build a FeedbackForm stateful widget for a college canteen: a TextField for comments (3 lines tall, label "Your feedback"), a SwitchListTile "Stay anonymous" (off at first), and a FilledButton "Send". When Send is tapped with empty comments, show a SnackBar "Please write something first". Otherwise show a SnackBar "Thanks, Priya!" (or "Thanks!" when anonymous) and clear the text field.
Show solution
import 'package:flutter/material.dart';
class FeedbackForm extends StatefulWidget {
const FeedbackForm({super.key, this.name = 'Priya'});
final String name;
@override
State<FeedbackForm> createState() => _FeedbackFormState();
}
class _FeedbackFormState extends State<FeedbackForm> {
final _comments = TextEditingController();
bool _anonymous = false;
@override
void dispose() {
_comments.dispose();
super.dispose();
}
void _send() {
final messenger = ScaffoldMessenger.of(context);
if (_comments.text.trim().isEmpty) {
messenger.showSnackBar(const SnackBar(content: Text('Please write something first')));
return;
}
final thanks = _anonymous ? 'Thanks!' : 'Thanks, ${widget.name}!';
messenger.showSnackBar(SnackBar(content: Text(thanks)));
_comments.clear();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _comments,
maxLines: 3,
decoration: const InputDecoration(labelText: 'Your feedback', border: OutlineInputBorder()),
),
SwitchListTile(
title: const Text('Stay anonymous'),
value: _anonymous,
onChanged: (v) => setState(() => _anonymous = v),
),
FilledButton(onPressed: _send, child: const Text('Send')),
],
);
}
}maxLines: 3 makes the field three lines tall. The switch value lives in the State, and _comments.clear() empties the field after sending. The controller is disposed when the form leaves the screen.
Quick Quiz
1. How do you disable a Flutter button?
Buttons are disabled when their onPressed callback is null.
2. Which call shows a SnackBar in current Flutter?
SnackBars are managed by ScaffoldMessenger since Flutter 2.
3. What should you do with a TextEditingController when the State is removed?
Controllers hold resources and listeners, so dispose them.
4. In current Flutter, which widget manages the selected value for a set of radio buttons?
RadioGroup replaced per-radio groupValue and onChanged.
Learn Flutter & Dart with a mentor
Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
