Custom Widgets trong Flutter
1. Tại sao tạo Custom Widgets?
- Tái sử dụng code
- Đọc hiểu dễ hơn
- Maintain dễ dàng
- Test riêng biệt
2. Custom StatelessWidget
class PrimaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
const PrimaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(text),
);
}
}
// Sử dụng
PrimaryButton(
text: 'Submit',
onPressed: () => print('Pressed'),
isLoading: false,
)3. Custom StatefulWidget
class ExpandableText extends StatefulWidget {
final String text;
final int maxLines;
const ExpandableText({
super.key,
required this.text,
this.maxLines = 3,
});
@override
State<ExpandableText> createState() => _ExpandableTextState();
}
class _ExpandableTextState extends State<ExpandableText> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.text,
maxLines: _isExpanded ? null : widget.maxLines,
overflow: _isExpanded ? null : TextOverflow.ellipsis,
),
TextButton(
onPressed: () => setState(() => _isExpanded = !_isExpanded),
child: Text(_isExpanded ? 'Show less' : 'Read more'),
),
],
);
}
}4. Widget với Slots (children)
Single child
class CustomCard extends StatelessWidget {
final Widget child;
final Color? backgroundColor;
const CustomCard({
super.key,
required this.child,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: child,
);
}
}Multiple children
class InfoCard extends StatelessWidget {
final Widget header;
final Widget body;
final Widget? footer;
const InfoCard({
super.key,
required this.header,
required this.body,
this.footer,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
header,
SizedBox(height: 8),
body,
if (footer != null) ...[
SizedBox(height: 16),
footer!,
],
],
),
),
);
}
}
// Sử dụng
InfoCard(
header: Text('Title', style: TextStyle(fontWeight: FontWeight.bold)),
body: Text('Description...'),
footer: ElevatedButton(onPressed: () {}, child: Text('Action')),
)5. Widget với Builder Pattern
class DataList<T> extends StatelessWidget {
final List<T> items;
final Widget Function(T item) itemBuilder;
final Widget? emptyWidget;
const DataList({
super.key,
required this.items,
required this.itemBuilder,
this.emptyWidget,
});
@override
Widget build(BuildContext context) {
if (items.isEmpty) {
return emptyWidget ?? Center(child: Text('No items'));
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => itemBuilder(items[index]),
);
}
}
// Sử dụng
DataList<User>(
items: users,
itemBuilder: (user) => ListTile(title: Text(user.name)),
emptyWidget: Column(
children: [
Icon(Icons.inbox, size: 48),
Text('No users found'),
],
),
)6. Composite Widgets
Kết hợp nhiều widgets thành một:
class UserListTile extends StatelessWidget {
final User user;
final VoidCallback? onTap;
final VoidCallback? onDelete;
const UserListTile({
super.key,
required this.user,
this.onTap,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: CircleAvatar(
backgroundImage: user.avatarUrl != null
? NetworkImage(user.avatarUrl!)
: null,
child: user.avatarUrl == null ? Text(user.name[0]) : null,
),
title: Text(user.name),
subtitle: Text(user.email),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: onDelete,
),
onTap: onTap,
);
}
}7. Widget với Theming
class AppButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final AppButtonStyle style;
const AppButton({
super.key,
required this.text,
this.onPressed,
this.style = AppButtonStyle.primary,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: _getBackgroundColor(theme),
foregroundColor: _getForegroundColor(theme),
),
child: Text(text),
);
}
Color _getBackgroundColor(ThemeData theme) {
switch (style) {
case AppButtonStyle.primary:
return theme.primaryColor;
case AppButtonStyle.secondary:
return theme.colorScheme.secondary;
case AppButtonStyle.danger:
return Colors.red;
}
}
Color _getForegroundColor(ThemeData theme) {
return Colors.white;
}
}
enum AppButtonStyle { primary, secondary, danger }8. Best Practices
1. Const constructors
// Good
const CustomWidget({super.key, required this.title});
// Allows
const CustomWidget(title: 'Hello')2. Required vs optional parameters
// Required: cần thiết để widget hoạt động
required this.title,
// Optional với default
this.isLoading = false,
// Optional nullable
this.onPressed,3. Tách thành nhiều widgets nhỏ
// Bad: một widget lớn
class ComplexScreen extends StatelessWidget {
Widget build(context) {
return Column(
children: [
// 100 lines of header code
// 100 lines of body code
// 100 lines of footer code
],
);
}
}
// Good: tách thành components
class ComplexScreen extends StatelessWidget {
Widget build(context) {
return Column(
children: [
_Header(),
_Body(),
_Footer(),
],
);
}
}📝 Tóm tắt
| Pattern | Khi nào dùng |
|---|---|
| Stateless | UI tĩnh, chỉ dựa vào props |
| Stateful | Có internal state |
| Slots | Cho phép truyền widgets con |
| Builder | Render items động |
| Composite | Kết hợp nhiều widgets |
Last updated on