🧩 Custom Widgets
1. Tại sao tạo Custom Widgets?
Phần tiêu đề “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
Phần tiêu đề “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ụngPrimaryButton( text: 'Submit', onPressed: () => print('Pressed'), isLoading: false,)3. Custom StatefulWidget
Phần tiêu đề “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)
Phần tiêu đề “4. Widget với Slots (children)”Single child
Phần tiêu đề “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
Phần tiêu đề “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ụngInfoCard( header: Text('Title', style: TextStyle(fontWeight: FontWeight.bold)), body: Text('Description...'), footer: ElevatedButton(onPressed: () {}, child: Text('Action')),)5. Widget với Builder Pattern
Phần tiêu đề “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ụngDataList<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
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “8. Best Practices”1. Const constructors
Phần tiêu đề “1. Const constructors”// Goodconst CustomWidget({super.key, required this.title});
// Allowsconst CustomWidget(title: 'Hello')2. Required vs optional parameters
Phần tiêu đề “2. Required vs optional parameters”// Required: cần thiết để widget hoạt độngrequired this.title,
// Optional với defaultthis.isLoading = false,
// Optional nullablethis.onPressed,3. Tách thành nhiều widgets nhỏ
Phần tiêu đề “3. Tách thành nhiều widgets nhỏ”// Bad: một widget lớnclass 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 componentsclass ComplexScreen extends StatelessWidget { Widget build(context) { return Column( children: [ _Header(), _Body(), _Footer(), ], ); }}📝 Tóm tắt
Phần tiêu đề “📝 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 |