EthicsComputerCourses
Join training +91 90532 10052

Common Widgets

Flutter & Dart · Lesson 16 of 29 Intermediate 7 min read Updated

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).

Dart
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:

WidgetUse for
FilledButtonThe main action on a screen ("Pay ₹499")
FilledButton.tonalImportant but secondary actions
ElevatedButtonActions that need to stand out from a patterned background
OutlinedButtonMedium-emphasis actions ("Cancel order")
TextButtonLow-emphasis actions, often in dialogs ("Not now")
IconButtonA single icon, such as a delete or favourite button
FloatingActionButtonThe primary action of a screen, floating at the bottom
Dart
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.

Dart
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.

Dart
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.

Dart
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 in dispose.
  • Using context after await without checking context.mounted. The analyzer warns about this for good reason.
  • Calling Scaffold.of(context).showSnackBar. That API was removed; use ScaffoldMessenger.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, Icon and Image.network/Image.asset display content; give network images an errorBuilder.
  • Material 3 buttons: FilledButton, ElevatedButton, OutlinedButton, TextButton, IconButton, FloatingActionButton; onPressed: null disables them.
  • TextField with a TextEditingController reads typed text; dispose the controller.
  • CheckboxListTile, SwitchListTile and RadioGroup with RadioListTile handle choices.
  • Card and ListTile build list rows; ScaffoldMessenger shows SnackBars; showDialog returns 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
Dart
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?

2. Which call shows a SnackBar in current Flutter?

3. What should you do with a TextEditingController when the State is removed?

4. In current Flutter, which widget manages the selected value for a set of radio buttons?

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