flutter_better_ui 2.0.8
flutter_better_ui: ^2.0.8 copied to clipboard
A modern Flutter UI component library that provides beautiful and easy-to-use widgets, with theme customization and responsive design. Actively maintained.
Better UI #
A modern Flutter UI component library that provides beautiful and easy-to-use widgets, with theme customization and responsive design. Actively maintained.
✨ Features #
- 🎨 Modern design - Built with Material Design 3
- 🌙 Theming - Light/Dark theme switching
- 📱 Responsive - Adapts to different screen sizes
- ⚡ High performance - Optimized rendering
- 🛠️ Extensible - Modular and easy to customize
🎥 Preview #
📦 Components #
Basic Components #
- BetterButton - Enhanced button with multiple styles and states
- BetterTextButton - Text button
- BetterCell - List cell item
Feedback Components #
- BetterToast - Lightweight toast with multiple positions and styles
- BetterPopup - Popup layer with multiple presentation styles
Form Components #
- BetterPicker - Picker supporting single, multiple, and cascading selections
- BetterSwitch - Customizable switch with loading state and async control
- BetterDatePicker - Date picker with flexible column types and formatting options
- BetterTimePicker - Time picker with flexible column types and formatting options
Feedback component #
- BetterSwipeCell - Swipeable cell with left and right action buttons
Display component #
- BetterSwiper - Used to loop through a set of images or content
- BetterMarquee - Used for looping and displaying a set of message notifications.
- BetterCollapse - Collapse panel for showing and hiding grouped content
Utilities #
- BetterScreenUtil - Screen adaptation utilities
- ColorUtil - Color utilities
- BetterAssets - Generates Dart asset constant classes from image folders
🚀 Quick Start #
Installation #
Add the dependency in pubspec.yaml:
dependencies:
flutter_better_ui: ^lastversion
Initialize #
void main() async {
runApp(BetterUi(designWidth: 375, designHeight: 812, child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: BetterUi.navigatorKey,
home: HomePage();
);
}
}
📖 Usage Guide #
BetterButton - Button #
// Basic button
BetterButton(
text: "Click me",
textStyle: TextStyle(color: Colors.red),
onClick: () {
print("Button clicked");
},
)
// Primary button
BetterButton(
type: BetterButtonType.primary,
text: "Primary Button",
onClick: () {},
)
// Loading state button
BetterButton(
text: "Loading",
loading: true,
onClick: () {},
)
// Plain button
BetterButton(
type: BetterButtonType.primary,
plain: true,
text: "Plain Button",
onClick: () {},
)
//Customer button
BetterButton(
decoration: BoxDecoration(
color: Colors.red,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, color: Colors.white),
Text('Customer', style: TextStyle(color: Colors.white)),
],
),
)
BetterToast - Toast #
// Basic toast
BetterToast.show(
message: "Operation succeeded",
);
// Custom style
BetterToast.show(
message: "Custom toast",
backgroundColor: Colors.blue,
textColor: Colors.white,
position: BetterToastPosition.center,
duration: Duration(seconds: 3),
);
// Loading toast
BetterToast.showLoading();
BetterToast.hideLoading();
BetterPopup - Popup #
// Bottom popup
BetterPopup.show(
position: BetterPopupPosition.bottom,
child: Container(
height: 300,
child: Center(child: Text("Bottom popup content")),
),
);
// Center popup
BetterPopup.show(
position: BetterPopupPosition.center,
child: Container(
width: 300,
height: 200,
child: Center(child: Text("Centered popup content")),
),
);
BetterPicker - Picker #
// Single-column picker
BetterPicker.show(
columns: [
BetterPickerItem(text: 'Option 1', value: 'option1'),
BetterPickerItem(text: 'Option 2', value: 'option2'),
BetterPickerItem(text: 'Option 3', value: 'option3'),
],
onConfirm: (items) {
print("Selected: ${items.first.text}");
},
);
// Multi-column picker
BetterPicker.show(
columns: [
[
BetterPickerItem(text: 'Monday', value: 'Monday'),
BetterPickerItem(text: 'Tuesday', value: 'Tuesday'),
BetterPickerItem(text: 'Wednesday', value: 'Wednesday'),
],
[
BetterPickerItem(text: 'Morning', value: 'Morning'),
BetterPickerItem(text: 'Afternoon', value: 'Afternoon'),
BetterPickerItem(text: 'Evening', value: 'Evening'),
],
],
onConfirm: (items) {
print("Selected: ${items.map((item) => item.text).join(', ')}");
},
);
// Cascading picker
BetterPicker.show(
columns: [
BetterPickerItem(
text: 'Zhejiang',
value: 'Zhejiang',
children: [
BetterPickerItem(
text: 'Hangzhou',
value: 'Hangzhou',
children: [
BetterPickerItem(text: 'Xihu District', value: 'Xihu'),
BetterPickerItem(text: 'Yuhang District', value: 'Yuhang'),
],
),
BetterPickerItem(
text: 'Wenzhou',
value: 'Wenzhou',
children: [
BetterPickerItem(text: 'Lucheng District', value: 'Lucheng'),
BetterPickerItem(text: 'Ouhai District', value: 'Ouhai'),
],
),
],
),
],
onConfirm: (items) {
print("Selected: ${items.map((item) => item.text).join(' - ')}");
},
);
BetterCell - List Cell #
BetterCell(
height: 44.bw,
titleText: 'Cell',
isShowBorder: true,
isShowArrowRight: true,
onClick(){
print("on click")
}
),
BetterSwitch - Switch #
// Basic switch
BetterSwitch(
defaultValue: false,
onChanged: (value) {
print("Switch value: $value");
},
)
// Loading state switch
BetterSwitch(
loading: true,
onChanged: (value) {
print("Switch value: $value");
},
)
// Custom size and colors
BetterSwitch(
width: 44.bw,
height: 26.bw,
defaultValue: true,
activeBackgroundColor: Colors.red,
inactiveBackgroundColor: Colors.grey,
onChanged: (value) {
print("Switch value: $value");
},
)
// Custom ball widget
BetterSwitch(
width: 50.bw,
height: 30.bw,
ballWidget: Container(
width: 26.bw,
height: 26.bw,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Icon(
Icons.check,
color: Theme.of(context).primaryColor,
size: 16.bw,
),
),
onChanged: (value) {
print("Switch value: $value");
},
)
// Disabled switch
BetterSwitch(
disabled: true,
onChanged: (value) {
print("Switch is disabled");
},
)
// Async control switch
BetterSwitch(
onUpdateChange: () async {
// Show confirmation dialog
final result = await showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text('Confirm'),
content: Text('Do you want to toggle the switch?'),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context, false),
child: Text('Cancel'),
),
CupertinoDialogAction(
onPressed: () => Navigator.pop(context, true),
child: Text('Confirm'),
),
],
),
);
return result ?? false;
},
)
BetterSwipeCell - Swipeable Cell #
// Basic swipe cell with left and right actions
BetterSwipeCell(
leftActions: [
BetterSwipeCellAction(
width: 60.bw,
onClick: (value) async {
return true;
},
child: Container(
color: Colors.blue,
height: 54.bw,
alignment: Alignment.center,
child: Text(
'Favorite',
style: TextStyle(color: Colors.white, fontSize: 14.bsp),
),
),
),
],
rightActions: [
BetterSwipeCellAction(
width: 60.bw,
onClick: (value) async {
return true;
},
child: Container(
color: Colors.red,
height: 54.bw,
alignment: Alignment.center,
child: Text(
'Delete',
style: TextStyle(color: Colors.white, fontSize: 14.bsp),
),
),
),
BetterSwipeCellAction(
width: 60.bw,
onClick: (value) async {
return true;
},
child: Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text(
'Favorite',
style: TextStyle(color: Colors.white, fontSize: 14.bsp),
),
),
),
],
//enable width extension
//isStretch:true,
child: BetterCell(
height: 54.bw,
titleText: 'Swipeable Cell',
valueText: 'Content',
),
)
//async controll
BetterSwipeCell(
rightActions: [
BetterSwipeCellAction(
width: 60.bw,
value: '收藏',
child: Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text(
'收藏',
style: TextStyle(color: Colors.white, fontSize: 14.bsp),
),
),
onClick: (value) async {
final result = await showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text('标题'),
content: Text('是否收藏'),
actions: [
CupertinoDialogAction(
child: Text(
'取消',
style: TextStyle(
fontSize: 14.bsp,
color: Theme.of(
context,
).textTheme.bodyMedium?.color,
),
),
onPressed: () => Navigator.pop(context, false),
),
CupertinoDialogAction(
child: Text(
'确定',
style: TextStyle(
fontSize: 14.bsp,
color: Theme.of(
context,
).textTheme.bodyMedium?.color,
),
),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
return result ?? false;
},
),
],
child: BetterCell(height: 54.bw, titleText: '异步控制'),
);
BetterDatePicker - Date Picker #
// Basic date picker
BetterDatePicker.show(
title: "选择日期",
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected date: ${selectedValues.map((e) => e.value).join('-')}");
},
);
// Date picker with custom range
BetterDatePicker.show(
title: "选择日期",
minDate: [2022, 1, 1],
maxDate: [2024, 12, 31],
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected date: ${selectedValues.map((e) => e.value).join('-')}");
},
);
// Date picker with custom formatting
BetterDatePicker.show(
title: "选择日期",
formatter: (BetterDatePickerFormatterOption option) {
if (option.columnType == BetterDatePickerColumnType.year) {
return "${option.text}年";
}
if (option.columnType == BetterDatePickerColumnType.month) {
return "${option.text}月";
}
if (option.columnType == BetterDatePickerColumnType.day) {
return "${option.text}日";
}
return option.text;
},
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected date: ${selectedValues.map((e) => e.value).join('-')}");
},
);
// Date picker with specific column types (year and month only)
BetterDatePicker.show(
title: "选择年月",
columnTypes: [
BetterDatePickerColumnType.year,
BetterDatePickerColumnType.month,
],
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected year-month: ${selectedValues.map((e) => e.value).join('-')}");
},
);
// Date picker with default value
BetterDatePicker.show(
title: "选择日期",
defaultValue: [2025, 9, 8],
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected date: ${selectedValues.map((e) => e.value).join('-')}");
},
);
// Date picker with filtering (e.g., only show months divisible by 6)
BetterDatePicker.show(
title: "选择日期",
columnTypes: [
BetterDatePickerColumnType.year,
BetterDatePickerColumnType.month,
],
filter: (BetterDatePickerFilterOption option) {
if (option.columnType == BetterDatePickerColumnType.month) {
return option.value % 6 == 0; // Only show months 6 and 12
}
return true;
},
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected date: ${selectedValues.map((e) => e.value).join('-')}");
},
);
// Date picker without default today
BetterDatePicker.show(
title: "选择日期",
isDefaultShowToday: false,
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected date: ${selectedValues.map((e) => e.value).join('-')}");
},
);
BetterTimePicker - Time Picker #
// Basic time picker
BetterTimePicker.show(
title: "选择时间",
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected time: ${selectedValues.map((e) => e.value).join(':')}");
},
);
// Time picker with custom range
BetterTimePicker.show(
title: "选择时间",
minDate: [10, 0, 0],
maxDate: [18, 59, 59],
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected time: ${selectedValues.map((e) => e.value).join(':')}");
},
);
// Time picker with custom formatting
BetterTimePicker.show(
title: "选择时间",
formatter: (BetterTimePickerFormatterOption option) {
if (option.columnType == BetterTimePickerColumnType.hour) {
return "${option.text}时";
}
if (option.columnType == BetterTimePickerColumnType.minute) {
return "${option.text}分";
}
if (option.columnType == BetterTimePickerColumnType.second) {
return "${option.text}秒";
}
return option.text;
},
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected time: ${selectedValues.map((e) => e.value).join(':')}");
},
);
// Time picker with specific column types (hour and minute only)
BetterTimePicker.show(
title: "选择时分",
columnTypes: [
BetterTimePickerColumnType.hour,
BetterTimePickerColumnType.minute,
],
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected hour-minute: ${selectedValues.map((e) => e.value).join(':')}");
},
);
// Time picker with default value
BetterTimePicker.show(
title: "选择时间",
defaultValue: [14, 30, 0],
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected time: ${selectedValues.map((e) => e.value).join(':')}");
},
);
// Time picker with filtering (e.g., only show minutes divisible by 5)
BetterTimePicker.show(
title: "选择时间",
columnTypes: [
BetterTimePickerColumnType.hour,
BetterTimePickerColumnType.minute,
],
filter: (BetterTimePickerFilterOption option) {
if (option.columnType == BetterTimePickerColumnType.minute) {
return option.value % 5 == 0; // Only show minutes 0, 5, 10, 15, etc.
}
return true;
},
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected time: ${selectedValues.map((e) => e.value).join(':')}");
},
);
// Time picker without default current time
BetterTimePicker.show(
title: "选择时间",
isDefaultShowNow: false,
onConfirm: (List<BetterPickerItem> selectedValues) {
print("Selected time: ${selectedValues.map((e) => e.value).join(':')}");
},
);
BetterSwiper #
PageController pageController = PageController();
BetterSwiper(
controller: pageController,
height: 200.bw,
autoplay: true,
loop: true,
scrollDirection: Axis.horizontal,
children: [
Container(width: double.infinity, color: Colors.red),
Container(width: double.infinity, color: Colors.blue),
Container(width: double.infinity, color: Colors.green),
],
),
BetterCollapse - Collapse Panel #
// Basic collapse
BetterCollapse(
children: BetterCollapseItem(
title: Text('Title'),
children: [
Text('Content 1'),
Text('Content 2'),
],
),
)
// Multiple panels
Column(
children: [
BetterCollapse(
children: BetterCollapseItem(
title: Text('Title 1'),
children: [Text('Content 1')],
),
),
BetterCollapse(
children: BetterCollapseItem(
title: Text('Title 2'),
children: [Text('Content 2')],
),
),
],
)
// Control expand and collapse from outside
class CollapseDemo extends StatefulWidget {
const CollapseDemo({super.key});
@override
State<CollapseDemo> createState() => _CollapseDemoState();
}
class _CollapseDemoState extends State<CollapseDemo> {
late final ExpansibleController _controller;
@override
void initState() {
super.initState();
_controller = ExpansibleController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
ElevatedButton(
onPressed: _controller.expand,
child: Text('Expand'),
),
ElevatedButton(
onPressed: _controller.collapse,
child: Text('Collapse'),
),
],
),
BetterCollapse(
expansibleController: _controller,
children: BetterCollapseItem(
title: Text('Title'),
children: [Text('Content')],
),
),
],
);
}
}
// Custom title area, colors, ripple, and icon colors
BetterCollapse(
minTitleHeight: 0,
titleMinVerticalPadding: 0,
titlePadding: EdgeInsets.symmetric(horizontal: 16.bw, vertical: 12.bw),
background: Colors.white,
collapsedBackground: Colors.white,
iconColor: Colors.blue,
collapsedIconColor: Colors.grey,
splashColor: Colors.blue.withAlpha(20),
contentPadding: EdgeInsets.symmetric(horizontal: 16.bw, vertical: 12.bw),
showDivider: true,
children: BetterCollapseItem(
title: Text('Custom title'),
children: [Text('Custom content')],
),
)
BetterCollapse Theme
ThemeData(
extensions: [
BetterThemeExtension(
// ...other theme fields
collapseTheme: BetterCollapseTheme(
backgroundColor: Colors.white,
collapsedBackground: Colors.white,
iconColor: Colors.blue,
collapsedIconColor: Colors.grey,
splashColor: Colors.blue.withAlpha(20),
),
),
],
)
| Property | Description |
|---|---|
expansibleController |
Controls the panel from outside, such as expand and collapse |
titlePadding |
Padding of the title area |
minTitleHeight |
Minimum height of the title area |
titleMinVerticalPadding |
Removes or customizes the internal vertical padding of the title ListTile |
background |
Background color when expanded |
collapsedBackground |
Background color when collapsed |
iconColor |
Arrow icon color when expanded |
collapsedIconColor |
Arrow icon color when collapsed |
splashColor |
Ripple color when tapping the title area |
contentPadding |
Padding of the expanded content |
BetterMarquee #
BetterMarquee(
height: 40.bw,
leftWidget: Icon(
BetterIcon.volumeO,
size: 16.bsp,
color: ColorUtil.hexToColor("#ed6a0c"),
),
textList: ["hello world"],
),
BetterIndexBar #
List<String> azList = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
BetterIndexBar(
indexBarColor: ColorUtil.hexToColor("#323233"),
indexBarActiveColor: ColorUtil.hexToColor("#1989fa"),
headerSlivers: [
SliverToBoxAdapter(
child: Container(
height: 100.bw,
alignment: Alignment.center,
color:ColorUtil.hexToColor("#fff"),
child: Text("我是自定义内容"),
),
),
],
items: [
for (var item in azList)
BetterIndexBarItem(
header: BetterIndexBarHeader(
anchor: item,
height: 32.bw,
titleWidget: Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 16.bw),
decoration: BoxDecoration(
color:ColorUtil.hexToColor("#F7F8FA"),
),
child: Text(
item,
),
),
),
list: [
for (var i in List.generate(10, (index) => index))
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.bw),
child: Container(
height: 44.bw,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: i==9 ? BorderSide.none : BorderSide(
color:ColorUtil.hexToColor("#E5E5E5"),
width: 1.bw,
),
),
),
child: Text("$item-text"),
),
),
],
),
],
)
🔧 Utilities #
BetterScreenUtil - Screen Adaptation #
// Get screen information
double screenWidth = BetterScreenUtil.screenWidth;
double screenHeight = BetterScreenUtil.screenHeight;
double statusBarHeight = BetterScreenUtil.statusBarHeight;
// Responsive sizes
double responsiveWidth = 100.bw;
double responsiveHeight = 50.bh;
double responsiveFont = 16.bsp;
ColorUtil - Color Utilities #
// Color conversion
Color hexColor = ColorUtil.hexToColor("#FF0000");
BetterAssets - Asset Constants Generator #
BetterAssets scans an directory and generates a Dart class with static asset path constants.
import 'package:flutter_better_ui/utils/better_assets.dart';
void main() async {
test('RefreshImages', () async {
await BetterAssets.generate(
projectPath: '.', // 可选,默认向上查找最近的 pubspec.yaml 所在目录
imagePath: 'assets/images',
codePath: 'lib/app_res',
codeName: 'app_image',
className: 'AppImages',
);
}
}
Questions #
- Why is the click area of buttons in lists such as ListView very large?Please use its properties or components, such as Align
📋 Example Project #
See the example/ directory for full usage examples:
better_button_page.dart- Button examplesbetter_toast_page.dart- Toast examplesbetter_popup_page.dart- Popup examplesbetter_picker_page.dart- Picker examplesbetter_switch_page.dart- Switch examplesbetter_cell_page.dart- List cell examplesbetter_swipe_action_page.dart- Swipe cell examplesbetter_date_picker_page.dart- Date picker examplesbetter_time_picker_page.dart- Time picker examplesbetter_swiper_page.dart- swiper examplesbetter_marquee_page.dart- marquee examplesbetter_collapse_page.dart- collapse examples
🤝 Contributing #
Issues and Pull Requests are welcome!
📄 License #
This project is licensed under the MIT License — see the LICENSE file for details.