Skip to Content
Flutter⚡ Nâng cao🧩 Custom Widgets

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

PatternKhi nào dùng
StatelessUI tĩnh, chỉ dựa vào props
StatefulCó internal state
SlotsCho phép truyền widgets con
BuilderRender items động
CompositeKết hợp nhiều widgets
Last updated on