Learn how to build custom widgets in Flutter to improve code reusability, maintainability, and abstraction. This guide covers creating custom stateless and stateful widgets, best practices, and more.
In Flutter, widgets are the building blocks of your application’s user interface. While Flutter provides a rich set of pre-built widgets, there are times when you’ll need to create custom widgets to meet specific design requirements or to encapsulate complex UI logic. Building custom widgets not only enhances code reusability and maintainability but also allows for greater abstraction and modularity in your app’s architecture.
Creating custom widgets offers several benefits:
When planning your app’s architecture, think in terms of widgets. Consider how you can break down your UI into smaller, reusable components that can be easily managed and updated.
Stateless widgets are immutable and do not hold any state. They are ideal for UI components that do not change over time or in response to user interactions.
Let’s create a custom stateless widget called MyCustomButton
. This widget will accept parameters such as text
, onPressed
, and color
.
Define the Widget Class:
Start by defining a class that extends StatelessWidget
.
import 'package:flutter/material.dart';
class MyCustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final Color color;
MyCustomButton({
required this.text,
required this.onPressed,
this.color = Colors.blue,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(primary: color),
child: Text(text),
);
}
}
Explanation:
MyCustomButton
class extends StatelessWidget
.text
, onPressed
, and color
.build
method returns an ElevatedButton
with the specified properties.Use the Custom Widget in Your App:
You can now use MyCustomButton
in your app like any other widget.
import 'package:flutter/material.dart';
import 'my_custom_button.dart'; // Import the custom widget
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Custom Button Example')),
body: Center(
child: MyCustomButton(
text: 'Click Me',
onPressed: () {
print('Button Pressed!');
},
color: Colors.green,
),
),
),
);
}
}
Explanation:
MyCustomButton
is used within the MyApp
widget.Stateful widgets are mutable and can hold state that may change over time or in response to user interactions.
Let’s create a custom stateful widget called MyToggleSwitch
. This widget will maintain its own state to toggle between on and off.
Define the Stateful Widget Class:
Start by defining a class that extends StatefulWidget
.
import 'package:flutter/material.dart';
class MyToggleSwitch extends StatefulWidget {
final ValueChanged<bool> onChanged;
MyToggleSwitch({required this.onChanged});
@override
_MyToggleSwitchState createState() => _MyToggleSwitchState();
}
class _MyToggleSwitchState extends State<MyToggleSwitch> {
bool _isOn = false;
void _toggleSwitch() {
setState(() {
_isOn = !_isOn;
});
widget.onChanged(_isOn);
}
@override
Widget build(BuildContext context) {
return Switch(
value: _isOn,
onChanged: (value) {
_toggleSwitch();
},
);
}
}
Explanation:
MyToggleSwitch
class extends StatefulWidget
.onChanged
to notify the parent widget of state changes._MyToggleSwitchState
class manages the internal state _isOn
.Use the Custom Stateful Widget in Your App:
You can now use MyToggleSwitch
in your app and respond to its state changes.
import 'package:flutter/material.dart';
import 'my_toggle_switch.dart'; // Import the custom widget
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Toggle Switch Example')),
body: Center(
child: MyToggleSwitch(
onChanged: (isOn) {
print('Switch is now: $isOn');
},
),
),
),
);
}
}
Explanation:
MyToggleSwitch
is used within the MyApp
widget.onChanged
callback.Composition involves building complex widgets by combining simpler ones. This approach promotes code reuse and modularity.
Example:
You can create a custom card widget by composing existing widgets like Container
, Text
, and Image
.
class MyCustomCard extends StatelessWidget {
final String title;
final String subtitle;
final String imageUrl;
MyCustomCard({
required this.title,
required this.subtitle,
required this.imageUrl,
});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Image.network(imageUrl),
ListTile(
title: Text(title),
subtitle: Text(subtitle),
),
],
),
);
}
}
Explanation:
MyCustomCard
widget is composed of an Image
and a ListTile
.Inheritance can be used to extend existing widgets, but it should be used sparingly. Prefer composition over inheritance for better flexibility and maintainability.
Example:
You might extend a Text
widget to add custom styling.
class MyStyledText extends Text {
MyStyledText(String data)
: super(
data,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
);
}
Explanation:
MyStyledText
widget extends Text
to apply custom styling.To illustrate how custom widgets fit into the widget tree, consider the following diagram:
graph TD; A[MyApp] --> B[Scaffold] B --> C[AppBar] B --> D[Body] D --> E[MyCustomButton] D --> F[MyToggleSwitch]
Explanation:
MyCustomButton
and MyToggleSwitch
are integrated into the widget tree under the Scaffold
.Before refactoring:
Column(
children: [
ElevatedButton(
onPressed: () {},
child: Text('Button 1'),
),
ElevatedButton(
onPressed: () {},
child: Text('Button 2'),
),
],
)
After refactoring into custom widgets:
Column(
children: [
MyCustomButton(
text: 'Button 1',
onPressed: () {},
),
MyCustomButton(
text: 'Button 2',
onPressed: () {},
),
],
)
Explanation:
MyCustomButton
to reduce duplication and improve maintainability.Challenge: Refactor the following widget tree into smaller custom widgets.
Column(
children: [
Container(
padding: EdgeInsets.all(16.0),
child: Text('Title', style: TextStyle(fontSize: 24)),
),
Container(
padding: EdgeInsets.all(16.0),
child: Text('Subtitle', style: TextStyle(fontSize: 16)),
),
ElevatedButton(
onPressed: () {},
child: Text('Action'),
),
],
)
Task:
TitleText
widget and a SubtitleText
widget.Solution:
class TitleText extends StatelessWidget {
final String text;
TitleText({required this.text});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: Text(text, style: TextStyle(fontSize: 24)),
);
}
}
class SubtitleText extends StatelessWidget {
final String text;
SubtitleText({required this.text});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: Text(text, style: TextStyle(fontSize: 16)),
);
}
}
// Refactored widget tree
Column(
children: [
TitleText(text: 'Title'),
SubtitleText(text: 'Subtitle'),
MyCustomButton(
text: 'Action',
onPressed: () {},
),
],
)
Explanation:
TitleText
and SubtitleText
widgets encapsulate the styling logic, making the code cleaner and more modular.Building custom widgets in Flutter is a powerful way to enhance your app’s architecture by promoting code reusability, maintainability, and abstraction. By following best practices and leveraging composition, you can create modular and scalable UIs that are easy to manage and extend. As you continue to develop with Flutter, consider how custom widgets can simplify your codebase and improve the overall quality of your applications.