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 examples
  • better_toast_page.dart - Toast examples
  • better_popup_page.dart - Popup examples
  • better_picker_page.dart - Picker examples
  • better_switch_page.dart - Switch examples
  • better_cell_page.dart - List cell examples
  • better_swipe_action_page.dart - Swipe cell examples
  • better_date_picker_page.dart - Date picker examples
  • better_time_picker_page.dart - Time picker examples
  • better_swiper_page.dart - swiper examples
  • better_marquee_page.dart - marquee examples
  • better_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.