Flutter Widgets — Complete Guide to Building UI
In this tutorial, you will learn about Flutter Widgets. We cover key concepts, practical examples, and best practices to help you master this topic.
Flutter widgets are the building blocks of the user interface, forming a hierarchical tree where every visual element from a button to a layout container is a widget.
What You Will Learn
- The widget tree and how Flutter renders UI
- StatelessWidget vs StatefulWidget
- Common Material Design widgets (AppBar, Scaffold, Card, Button)
- Text, Image, and Icon widgets
- Input widgets (TextField, Switch, Checkbox)
- State management with setState
- Widget lifecycle methods
Why It Matters
In Flutter, everything is a widget. Unlike native Android (XML layouts) or iOS (Storyboards), Flutter uses a single unified widget system for all visual elements. Understanding how widgets compose, how state updates trigger rebuilds, and how the widget lifecycle works is essential for building any Flutter application. Widgets are immutable configurations that are lightweight to create and cheap to rebuild.
Real-World Use
The DodaTech Flutter app's home screen is composed of dozens of nested widgets: a Scaffold with an AppBar, a SingleChildScrollView containing a Column of Card widgets, each with a ListTile containing Text and Icon widgets. When the user taps a card, setState updates the selection state and Flutter efficiently rebuilds only the affected widgets.
Learning Path
flowchart LR A[Flutter Setup] --> B[Flutter Widgets\nYou are here] B --> C[Flutter Layout] style B fill:#f90,color:#fff
Everything is a Widget
In Flutter, the entire UI is composed of widgets nested inside other widgets:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Widgets'),
),
body: Center(
child: Text('Hello, Widgets!'),
),
),
),
);
}
MaterialApp is a widget that configures Material Design theming. Scaffold provides the app structure. AppBar, Center, and Text are all widgets. The entire tree is a single composable expression.
StatelessWidget
A StatelessWidget is immutable. It describes a part of the UI that does not change after creation:
import 'package:flutter/material.dart';
class GreetingWidget extends StatelessWidget {
final String name;
const GreetingWidget({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
color: Colors.lightBlue.shade50,
child: Text(
'Hello, $name!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
);
}
}
// Usage in another widget:
// GreetingWidget(name: 'Alice')
StatelessWidgets receive configuration parameters through their constructor. The build method returns the widget tree. Since the widget has no mutable state, it builds exactly once or when the parent forces a rebuild.
StatefulWidget
A StatefulWidget has mutable state that can change over time. It consists of two classes: the widget (immutable configuration) and the state (mutable):
import 'package:flutter/material.dart';
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $_count', style: TextStyle(fontSize: 32)),
SizedBox(height: 16),
ElevatedButton(
onPressed: _increment,
child: Text('Increment'),
),
],
);
}
}
setState tells Flutter that the state has changed and triggers a rebuild of the widget. Flutter compares the new widget tree with the old tree and updates only the parts that changed.
Widget Lifecycle
StatefulWidgets follow a lifecycle:
class LifecycleWidget extends StatefulWidget {
const LifecycleWidget({super.key});
@override
State<LifecycleWidget> createState() => _LifecycleWidgetState();
}
class _LifecycleWidgetState extends State<LifecycleWidget> {
@override
void initState() {
super.initState();
print('1. initState: Widget created');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('2. didChangeDependencies: Dependencies changed');
}
@override
void didUpdateWidget(LifecycleWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print('3. didUpdateWidget: Widget configuration changed');
}
@override
Widget build(BuildContext context) {
print('4. build: Building widget tree');
return Container();
}
@override
void dispose() {
print('5. dispose: Widget removed from tree');
super.dispose();
}
}
The lifecycle order: initState (once), didChangeDependencies (once, then on dependency changes), build (every time the UI needs to update), didUpdateWidget (when parent rebuilds with new config), dispose (when removed).
Common Material Widgets
Flutter provides a rich set of Material Design widgets:
class WidgetShowcase extends StatelessWidget {
const WidgetShowcase({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Widget Showcase')),
body: ListView(
padding: EdgeInsets.all(16),
children: [
// Card
Card(
child: ListTile(
leading: Icon(Icons.star, color: Colors.amber),
title: Text('Featured'),
subtitle: Text('This is a card with a list tile'),
trailing: Icon(Icons.chevron_right),
),
),
SizedBox(height: 16),
// Buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {},
child: Text('Elevated'),
),
OutlinedButton(
onPressed: () {},
child: Text('Outlined'),
),
TextButton(
onPressed: () {},
child: Text('Text'),
),
],
),
SizedBox(height: 16),
// Input
TextField(
decoration: InputDecoration(
labelText: 'Enter your name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
SizedBox(height: 16),
// Switch
SwitchListTile(
title: Text('Enable notifications'),
value: true,
onChanged: (value) {},
),
SizedBox(height: 16),
// Chip
Wrap(
spacing: 8,
children: [
Chip(label: Text('Dart'), avatar: Icon(Icons.code)),
Chip(label: Text('Flutter'), avatar: Icon(Icons.phone_android)),
Chip(label: Text('Widgets'), avatar: Icon(Icons.widgets)),
],
),
],
),
);
}
}
Material widgets follow the Material Design specification out of the box. They include proper elevation, ink splash effects, and Accessibility support.
Text and Styling
The Text widget renders text with configurable styling:
Text(
'Styled Text Example',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
letterSpacing: 2.0,
shadows: [
Shadow(
offset: Offset(2, 2),
blurRadius: 4,
color: Colors.black26,
),
],
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
Use Theme.of(context).textTheme to access the app's typography theme instead of hardcoding styles. This ensures consistency and supports accessibility features like font scaling.
Images and Icons
Display images and icons in Flutter:
import 'package:flutter/material.dart';
class MediaWidget extends StatelessWidget {
const MediaWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Network image
Image.network(
'https://picsum.photos/200/150',
width: 200,
height: 150,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.error, color: Colors.red);
},
),
SizedBox(height: 16),
// Icons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.home, size: 40, color: Colors.blue),
Icon(Icons.favorite, size: 40, color: Colors.red),
Icon(Icons.settings, size: 40, color: Colors.grey),
Icon(Icons.person, size: 40, color: Colors.green),
],
),
SizedBox(height: 16),
// Local image (from assets)
// Image.asset('assets/images/logo.png'),
],
);
}
}
Use loadingBuilder for network images to show progress indicators. Use errorBuilder to handle load failures gracefully.
Buttons and Callbacks
Buttons accept callback functions for user interaction:
class ButtonExamples extends StatelessWidget {
const ButtonExamples({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
print('Elevated button pressed');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: Text('Elevated Button'),
),
SizedBox(height: 12),
IconButton(
onPressed: () {},
icon: Icon(Icons.favorite),
color: Colors.red,
tooltip: 'Add to favorites',
),
SizedBox(height: 12),
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
],
);
}
}
Setting onPressed to null disables the button. Always provide tooltips for IconButton for accessibility.
Input Fields
TextField collects user input:
class InputExample extends StatefulWidget {
const InputExample({super.key});
@override
State<InputExample> createState() => _InputExampleState();
}
class _InputExampleState extends State<InputExample> {
final _nameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hello, ${_nameController.text}!')),
);
}
},
child: Text('Submit'),
),
],
),
),
);
}
}
Always dispose TextEditingController in dispose(). Use Form and TextFormField with validators for form validation.
Common Mistakes
Creating widgets in build method that depend on mutable state: State should be held in
StatefulWidgetor state management solutions, not recreated inbuild.Using
setStateoutside the State class:setStateis only available insideState<T>. Callbacks from child widgets should use callback functions.Not disposing controllers and streams:
TextEditingController,AnimationController, and stream subscriptions must be disposed to prevent memory leaks.Building deep widget trees in a single build method: Break large build methods into smaller private methods or separate widgets.
Using
constconstructors: Always addconstto widgets that do not change. This allows Flutter to reuse the widget instance and skip rebuilding.
Practice Questions
- What is the difference between StatelessWidget and StatefulWidget?
- How does
setStatetrigger a UI update? - Why must you dispose controllers and subscriptions?
- What is the widget lifecycle order in StatefulWidget?
- Challenge: Build a login screen with email and password TextField widgets, a login button, and a loading indicator that shows while "logging in". Use
setStateto toggle between the form and loading state.
Mini Project
Build a profile card app:
- ProfileCard StatelessWidget showing a name, email, and avatar
- EditProfileScreen StatefulWidget with text fields for name and email
- Navigation between screens using Navigator.push
- Theme switching between light and dark mode
- Form validation for email format
FAQ
What is Next
Now that you understand widgets, learn about layout and arrangement. Proceed to Flutter Layout for Row, Column, Stack, and other layout widgets. Then explore Flutter Scrolling for ListView and GridView.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro