XB Scaffold

基于 Provider 封装的 Flutter 脚手架,集成路由、主题、dialog、toast、actionSheet 等常用控件,提供完整的 MVVM 架构解决方案。

pub package License

特性

  • 🏗️ 完整的 MVVM 架构:基于 Provider 的状态管理
  • 🎨 主题系统:支持多主题切换和自定义主题
  • 🧭 路由管理:简化的路由操作和生命周期管理
  • 📱 丰富的 UI 组件:内置常用组件和工具类
  • 🔄 生命周期管理:完整的页面和组件生命周期
  • 🌐 跨平台支持:支持 iOS、Android、Web、Desktop
  • 📋 悬浮列表:支持分组头部悬浮的 ListView
  • 🛠️ 工具集合:网络请求、事件总线、定时器等实用工具

安装

pubspec.yaml 中添加依赖:

dependencies:
  xb_scaffold: ^0.1.42

然后运行:

flutter pub get

快速开始

1. 初始化应用

import 'package:flutter/material.dart';
import 'package:xb_scaffold/xb_scaffold.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'XB Scaffold Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      // 添加路由观察者
      navigatorObservers: [xbRouteObserver],
      home: XBScaffold(
        // 配置主题
        themeConfigs: [
          XBThemeConfig(
            primaryColor: Colors.blue,
            imgPrefix: "assets/images/theme1/",
          ),
          XBThemeConfig(
            primaryColor: Colors.red,
            imgPrefix: "assets/images/theme2/",
          ),
        ],
        // 自定义 Loading 样式(可选)
        loadingBuilder: (context, msg) {
          return Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                CircularProgressIndicator(),
                if (msg != null) ...[
                  SizedBox(height: 16),
                  Text(msg),
                ],
              ],
            ),
          );
        },
        // Toast 背景颜色(可选)
        toastBackgroundColor: Colors.black87,
        child: const HomePage(),
      ),
    );
  }
}

2. 创建页面

使用 XBPage(推荐用于页面)

import 'package:flutter/material.dart';
import 'package:xb_scaffold/xb_scaffold.dart';

class HomePage extends XBPage<HomePageVM> {
  const HomePage({super.key});

  @override
  HomePageVM generateVM(BuildContext context) {
    return HomePageVM(context: context);
  }

  @override
  String setTitle(HomePageVM vm) => "首页";

  @override
  Widget buildPage(HomePageVM vm, BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          Text('计数器: ${vm.counter}'),
          SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: vm.increment,
                child: Text('增加'),
              ),
              ElevatedButton(
                onPressed: vm.decrement,
                child: Text('减少'),
              ),
            ],
          ),
          SizedBox(height: 20),
          ElevatedButton(
            onPressed: () => vm.showToast('Hello XB Scaffold!'),
            child: Text('显示 Toast'),
          ),
        ],
      ),
    );
  }

  // 自定义 AppBar(可选)
  @override
  List<Widget>? actions(HomePageVM vm) {
    return [
      IconButton(
        icon: Icon(Icons.settings),
        onPressed: vm.openSettings,
      ),
    ];
  }

  // 页面配置(可选)
  @override
  bool needSafeArea(HomePageVM vm) => true;

  @override
  bool needAdaptKeyboard(HomePageVM vm) => true;
}

class HomePageVM extends XBPageVM<HomePage> {
  HomePageVM({required super.context});

  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    notify(); // 通知 UI 更新
  }

  void decrement() {
    _counter--;
    notify();
  }

  void showToast(String message) {
    xbToast(message);
  }

  void openSettings() {
    push(const SettingsPage());
  }
}

使用 XBWidget(用于组件)

class CounterWidget extends XBWidget<CounterWidgetVM> {
  const CounterWidget({super.key});

  @override
  CounterWidgetVM generateVM(BuildContext context) {
    return CounterWidgetVM(context: context);
  }

  @override
  Widget buildWidget(CounterWidgetVM vm, BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          Text('计数: ${vm.count}'),
          ElevatedButton(
            onPressed: vm.increment,
            child: Text('点击'),
          ),
        ],
      ),
    );
  }
}

class CounterWidgetVM extends XBVM<CounterWidget> {
  CounterWidgetVM({required super.context});

  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notify();
  }
}

使用 XBVMLessWidget(无需自定义 VM)

class SimpleWidget extends XBVMLessWidget {
  const SimpleWidget({super.key});

  @override
  Widget buildWidget(XBVM vm, BuildContext context) {
    return Container(
      child: Text('简单组件'),
    );
  }
}

核心功能

VM 访问方式

XB Scaffold 提供了多种访问 VM 的方式:

1. 传统方式(通过参数)

@override
Widget buildPage(HomePageVM vm, BuildContext context) {
  return Text('计数: ${vm.counter}');
}

2. 使用 XBWidget 的方法

Widget _buildCounter(BuildContext context) {
  final vm = vmOf(context); // 不监听变化
  final vmWatch = vmWatchOf(context); // 监听变化
  return Text('计数: ${vm.counter}');
}

3. 使用 BuildContext 扩展(推荐)

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 监听变化,会触发 rebuild
    final vm = context.vmWatch<HomePageVM>();
    return Text('计数: ${vm.counter}');
  }
}

class CounterButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 不监听变化,不会触发 rebuild
    final vm = context.vmOf<HomePageVM>();
    return ElevatedButton(
      onPressed: vm.increment,
      child: Text('增加'),
    );
  }
}

4. 安全访问

Widget build(BuildContext context) {
  final vm = context.vmOfOrNull<HomePageVM>();
  if (vm == null) {
    return Text('VM 不存在');
  }
  return Text('计数: ${vm.counter}');
}

路由管理

// 跳转到新页面
push(const DetailPage());

// 带参数跳转
push(DetailPage(id: 123));

// 替换当前页面
replace(const NewPage());

// 返回上一页
pop();

// 返回并传递结果
pop('result');

// 返回到根页面
popToRoot();

// 返回到指定类型的页面
popUtilType<HomePage>();

// 返回到指定页面实例
popUtilWidget(homePage);

// 检查顶部页面类型
if (topIsType<HomePage>()) {
  // 当前顶部是 HomePage
}

主题管理

// 切换主题(索引对应初始化时的 themeConfigs)
XBThemeVM().changeTheme(1);

// 获取当前主题
final theme = XBThemeVM().currentTheme;

// 在组件中使用主题颜色
Container(
  color: colors.primary, // 使用主题色
  child: Text('主题文本'),
)

// 扩展主题颜色
extension CustomColors on XBThemeColor {
  Color get customBlue => Color(0xFF2196F3);
  Color get customGreen => Color(0xFF4CAF50);
}

Dialog 和弹窗

// 显示确认对话框
xbDialog(
  title: '提示',
  msg: '确定要删除吗?',
  onConfirm: () {
    // 确认操作
  },
);

// 显示输入对话框
xbDialogInput(
  title: '输入',
  hint: '请输入内容',
  onConfirm: (text) {
    print('输入的内容: $text');
  },
);

// 显示 ActionSheet
xbActionSheet(
  actions: [
    XBActionSheetAction(
      title: '拍照',
      onTap: () {
        // 拍照操作
      },
    ),
    XBActionSheetAction(
      title: '从相册选择',
      onTap: () {
        // 选择照片操作
      },
    ),
  ],
);

// 显示 Toast
xbToast('操作成功');

Loading 管理

class MyPageVM extends XBPageVM<MyPage> {
  // 显示 Loading
  void loadData() async {
    showLoading(msg: '加载中...');
    try {
      // 执行异步操作
      await Future.delayed(Duration(seconds: 2));
    } finally {
      hideLoading();
    }
  }
}

// 页面级 Loading 配置
@override
bool needLoading(MyPageVM vm) => true;

@override
bool needInitLoading(MyPageVM vm) => true; // 页面初始化时显示 Loading

网络请求

// 配置网络请求
XBHttp.instance.init(
  baseUrl: 'https://api.example.com',
  connectTimeout: 5000,
  receiveTimeout: 3000,
);

// GET 请求
final response = await XBHttp.instance.get('/users');

// POST 请求
final response = await XBHttp.instance.post(
  '/users',
  data: {'name': 'John', 'age': 30},
);

// 在 VM 中使用
class UserListVM extends XBPageVM<UserListPage> {
  List<User> users = [];

  Future<void> loadUsers() async {
    showLoading();
    try {
      final response = await XBHttp.instance.get('/users');
      users = (response.data as List)
          .map((json) => User.fromJson(json))
          .toList();
      notify();
    } catch (e) {
      xbToast('加载失败: $e');
    } finally {
      hideLoading();
    }
  }
}

事件总线

// 定义事件
class UserLoginEvent {
  final String username;
  UserLoginEvent(this.username);
}

// 在 VM 中监听事件
class HomePageVM extends XBPageVM<HomePage> {
  @override
  void didCreated() {
    super.didCreated();
    // 监听用户登录事件
    listen<UserLoginEvent>((event) {
      print('用户 ${event.username} 已登录');
      // 处理登录后的逻辑
    });
  }
}

// 发送事件
XBEventBus.fire(UserLoginEvent('john_doe'));

工具类

定时器

final timer = XBTimer();

// 延时执行
timer.once(
  duration: Duration(seconds: 2),
  onTick: () {
    print('2秒后执行');
  },
);

// 重复执行
timer.repeat(
  duration: Duration(seconds: 1),
  onTick: () {
    print('每秒执行一次');
  },
);

// 取消定时器
timer.cancel();

防重复点击

XBPreventMultiTask.run(
  key: 'submit_button',
  task: () async {
    // 提交操作
    await submitData();
  },
  onError: () {
    xbToast('请勿重复点击');
  },
);

等待任务

final waitTask = XBWaitTask();

// 等待多个异步任务完成
waitTask.wait([
  loadUserData(),
  loadConfigData(),
  loadNotifications(),
]).then((_) {
  print('所有任务完成');
});

高级功能

悬浮头部列表

XBHoveringHeaderList(
  itemCount: items.length,
  headerBuilder: (context, section) {
    return Container(
      height: 40,
      color: Colors.grey[200],
      child: Text('分组 $section'),
    );
  },
  itemBuilder: (context, indexPath) {
    return ListTile(
      title: Text('项目 ${indexPath.item}'),
    );
  },
  sectionCount: sections.length,
  itemCountInSection: (section) => sections[section].items.length,
)

自定义组件

按钮组件

XBButton(
  text: '点击按钮',
  onTap: () {
    print('按钮被点击');
  },
  backgroundColor: Colors.blue,
  textColor: Colors.white,
  borderRadius: 8,
  disable: false, // 是否禁用
)

图片组件

XBImage(
  imageUrl: 'https://example.com/image.jpg',
  width: 100,
  height: 100,
  placeholder: CircularProgressIndicator(),
  errorWidget: Icon(Icons.error),
)

页面配置选项

class MyPage extends XBPage<MyPageVM> {
  // 是否需要安全区域
  @override
  bool needSafeArea(MyPageVM vm) => true;

  // 是否需要适配键盘
  @override
  bool needAdaptKeyboard(MyPageVM vm) => true;

  // 是否启用 Android 物理返回键
  @override
  bool onAndroidPhysicalBack(MyPageVM vm) => true;

  // 是否启用 iOS 侧滑返回
  @override
  bool needIosGestureBack(MyPageVM vm) => true;

  // 屏幕方向改变时是否重新构建
  @override
  bool needRebuildWhileOrientationChanged(MyPageVM vm) => false;

  // 主题改变时是否重新构建
  @override
  bool needRebuildWhileAppThemeChanged(MyPageVM vm) => true;

  // 页面背景色
  @override
  Color? backgroundColor(MyPageVM vm) => Colors.white;

  // 导航栏背景色
  @override
  Color? navigationBarBGColor(MyPageVM vm) => Colors.blue;

  // 导航栏标题颜色
  @override
  Color? navigationBarTitleColor(MyPageVM vm) => Colors.white;
}

最佳实践

1. VM 生命周期管理

class MyPageVM extends XBPageVM<MyPage> {
  StreamSubscription? _subscription;

  @override
  void didCreated() {
    super.didCreated();
    // 页面创建时的初始化操作
    _initData();
  }

  @override
  void widgetDidBuilt() {
    super.widgetDidBuilt();
    // 页面构建完成后的操作
    _startListening();
  }

  void _startListening() {
    _subscription = someStream.listen((data) {
      // 处理数据
    });
  }

  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }
}

2. 错误处理

class ApiService {
  static Future<T> safeRequest<T>(Future<T> Function() request) async {
    try {
      return await request();
    } catch (e) {
      xbToast('网络请求失败: $e');
      rethrow;
    }
  }
}

// 使用
await ApiService.safeRequest(() => XBHttp.instance.get('/api/data'));

3. 状态管理

class UserVM extends XBVM<UserWidget> {
  UserState _state = UserState.loading;
  UserState get state => _state;

  User? _user;
  User? get user => _user;

  void loadUser() async {
    _state = UserState.loading;
    notify();

    try {
      _user = await userRepository.getUser();
      _state = UserState.success;
    } catch (e) {
      _state = UserState.error;
    }
    notify();
  }
}

enum UserState { loading, success, error }

常见问题

Q: 如何在子组件中访问父页面的 VM?

A: 使用 BuildContext 扩展:

class ChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final parentVM = context.vmOf<ParentPageVM>();
    return Text(parentVM.someData);
  }
}

Q: 如何处理页面间的数据传递?

A: 可以通过构造函数、路由参数或事件总线:

// 方式1: 构造函数
push(DetailPage(userId: 123));

// 方式2: 事件总线
XBEventBus.fire(DataUpdateEvent(data));

// 方式3: 返回结果
final result = await push(SelectPage());

Q: 如何自定义主题?

A: 使用扩展:

extension MyThemeColors on XBThemeColor {
  Color get customPrimary => Color(0xFF1976D2);
  Color get customAccent => Color(0xFFFF4081);
}

// 使用
Container(color: colors.customPrimary)

更新日志

查看 CHANGELOG.md 了解详细的版本更新信息。

许可证

本项目基于 MIT 许可证开源。查看 LICENSE 文件了解更多信息。

贡献

欢迎提交 Issue 和 Pull Request 来帮助改进这个项目。

支持

如果这个项目对您有帮助,请给它一个 ⭐️!

Libraries

xb_scaffold