likes
comments
collection
share

一个简单的Flutter快速开发框架

作者站长头像
站长
· 阅读数 39

其实在写这篇文章的时候,我看过了好多关于框架以及状态管理使用的文章了。对于Getx与其他状态管理的的pk,对于我而言,我是比较偏向ProviderBLoc的。我先声明一下,我对Getx没有任何的想法,我知道它也是非常好的一个快速开发的一个工具。只是我想说的是如果你使用某些框架久了(我也曾用gf去做过几个项目,写完之后只是觉得很奇怪,因为丢掉了好多东西,只能用作者封装的工具跟方法),忘掉了原本最初的一些书写方法跟书写习惯,对于自己来说是比较可怕的,因为你丢弃了很多最原始的东西。再一个就是如果你习惯了Getx的开发,如果框架出现了问题,那么怎么办呢?当然这个还得取决于你们个人自己的公司跟团队的决策,毕竟我们只是一个打工仔,说那么多也没啥用啊。哎...(说到这里就比较伤感了)

不说了,上正文。。。

项目结构

一个简单的Flutter快速开发框架

common

一个简单的Flutter快速开发框架

这里主要是放了一些我们封装的基本内容以及工具跟全局的一些东西

components

不用说,这里就是我们要封装的公共组件

generated

这里是自动生成的文件夹,下面主要是包括intl国际化跟model的实体类工具(我一般用的是FlutterJsonBeanFactory)

https

这里主要是网络请求的封装

I10n

国际化的配置

pages

这里很明显就是我们写的页面了

如何使用

这里就是大家最关心的了,对于开发者来说,我可不管你封装的啥样子,我就看使用起来简单不简单。

简单使用 BaseViewModel

Page的定义

class SplashPage extends BasePage<SplashViewModel> {
  SplashPage() : super(SplashViewModel());

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text('Splash'),
      ),
    );
  }
}

ViewModel的定义(其实平时我们在书写的时候Model层的数据变化也在这个文件里面书写)

class SplashViewModel extends BaseViewModel {
  bool _canPush = true;

  final _timerCountDown = TimerCountDown();

  void goRoot() {
    if (_canPush) {
      _canPush = false;
    }
  }

  @override
  void viewModelInitForContext(BuildContext context) {
    super.viewModelInitForContext(context);
    _timerCountDown.setTimerCount(1);
    _timerCountDown.startTimer(() {
      pushAndRemoveUntil(RootPage());
    });
  }

  @override
  void viewModelDispose() {
    super.dispose();
    _timerCountDown.destroyTimer();
  }
}

Model的定义

一般我在项目中,model的定义就是后端返回的数据结构,因为这里我们并没有使用到相关的接口数据所以暂不定义了。


以上就是一个简单的示例,因为对于启动页或者闪屏页来说就是一个倒计时然后就进到我们的主页面了~ 文件结构也很简单

一个简单的Flutter快速开发框架

以上是简单的使用,不牵扯到跟后端交互的页面。那么反过来说,要是跟后端交互那咋办呢?我只想说好办,继续看。

BaseSingleViewModel

这个可以看作是有接口的预请求,类似于点进去详情页,会请求一个详情接口。

Page的定义

class WorkBenchPage extends BasePage<WorkBenchViewModel> {
  WorkBenchPage() : super(WorkBenchViewModel());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: MyAppBar.build(context, title: '工作台'),
      body: ConsumerWidget<WorkBenchViewModel>(
        // isMock: true,
        builder: (context, vm, _) {
          return SingleChildScrollView(
            child: Column(
              children: [
                ElevatedButton(
                    onPressed: () {
                      vm.goTest();
                    },
                    child: const Text('ElevatedButton')),
                const Text('工作台'),
              ],
            ),
          );
        },
      ),
    );
  }
}

相比较前面的BaseViewModel对应的Page定义,我们这里是用了ConsumerWidget进行了包裹。ConsumerWidget是我们封装的根据请求不同的state(init、loading、success、error...)进行不同阶段的显示。

ViewModel的定义

class WorkBenchViewModel extends BaseSingleViewModel<UserInfoEntity> {
  void goTest() {
    push(TestPage());
  }

  @override
  RequestConfig setRequestConfig() {
    return RequestConfig.fromGet('getXXX');
  }
}

相比较BaseViewModel,BaseSingleViewModel通过 setRequestConfig() 方法来进行请求的配置(这个配置的内容后面说)。

Model的定义

@JsonSerializable()
class UserInfoEntity {
  int? id = 0;
  String? isCertified = '';
  String? isSign = '';
  String? nickname = '';
  String? name = '';
  String? avatar = '';
  String? mobile = '';
  int? point = 0;

  UserInfoEntity();

  factory UserInfoEntity.fromJson(Map<String, dynamic> json) => $UserInfoEntityFromJson(json);

  Map<String, dynamic> toJson() => $UserInfoEntityToJson(this);

  @override
  String toString() {
   return jsonEncode(this);
  }
}

很明显的Model的定义就是我们通过调用接口返回的数据对应Dart的实体类。

BaseListViewModel

这个跟 BaseSingleViewModel 其实是一样的,不过 BaseListViewModel 对应的是列表的请求。

Page的定义

class HallPage extends BasePage<HallViewModel> {
  HallPage() : super(HallViewModel());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: ConsumerWidget<HallViewModel>(
        isMock: true,
        builder: (context, vm, _) {
          return MyListView<HallViewModel>(
            viewModel: vm,
            child: ListView.builder(
              itemCount: 10,
              itemBuilder: (context, index) {
                return GestureDetector(
                  onTap: () {
                    vm.push(TestPage());
                  },
                  child: Container(
                    padding: EdgeInsets.symmetric(vertical: 10.h),
                    margin: EdgeInsets.only(bottom: 10.h),
                    width: double.infinity,
                    height: 50.h,
                    color: Colors.amber,
                    child: Container(
                      alignment: Alignment.center,
                      child: Text('${index + 1} 点击 set'),
                    ),
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

没啥区别,显示的Widget也是通过ConsumerWidget进行包裹,不过内部是一个MyListView,这个也是我自己封装的一个上拉加载下拉刷新的列表组件。

ViewModel的定义

class HallViewModel extends BaseListViewModel<MissionsEntity, MissionsList> {
  @override
  RequestConfig initRequestConfig() {
    return RequestConfig.fromGet('/job/worker/mission/getMissions');
  }

  @override
  List<MissionsList> formatResponseList(MissionsEntity model) {
    return model.list ?? [];
  }
}

Model的定义就不在详说了,都是一样的


使用总结

文件结构跟内容其实都差不多一样,都是匹配的,一个page对应一个viewmodel文件(model看情况随便)。

一个简单的Flutter快速开发框架 每次我们在使用的时候,我们的页面 page 通过 继承 BasePage 这个类即可,其他主要使用的生命周期函数也存在,对应的就是state的生命周期,内容跟功能也是一样对应。不过还是不建议在page里面进行setState刷新页面操作。

viewmodel文件就那么三个,BaseViewModel、BaseSingleViewModel、BaseListViewModel,分别对应的就是没有请求、请求详情、请求列表。平时开发也可以BaseViewModel,等开始对接口的时候根据请求的分工来选择 BaseSingleViewModel 还是 BaseListViewModel,还是可以自定义的扩展。我们的逻辑写在viewmodel文件下即可(一定要写在viewmodel文件的内容下面,不然就没有意义了)。

model文件根据后端接口返回数据进行创建(可以直接使用工具)。

其实主要的文件就是page跟viewmodel,只要存在那么就OK了。

如果你做过原生开发的话,这个应该不难理解,封装只是为了更简单的使用以及公用部分的处理。

BasePage

abstract class BasePage<VM extends BaseViewModel> extends BaseView
    with RouteAware, _RouterHandler {
  /// 个人觉得这个实例抛出去有点奇怪  所以做为私有变量~~~
  final VM _viewModel;

  /// 路由名字
  final String? pageName;

  BasePage(this._viewModel, {this.pageName});

  /// page数据的集合
  static final List<BasePage> _pageList = [];

  List<Type> get pageList => _pageList.map((e) => e.runtimeType).toList();

  /// loading控制器(暂未用到,暂时混入了toastMixin)
  final LoadingController loadingController = LoadingController();

  /// 初始化widget 在navigator进行操作入栈的时候 必须初始化此基类
  Widget get initWidget => _createWidget();

  /// 修改路由初始化的过程生成的基础 base page
  Widget _createWidget() {
    return _PageWidget<VM>(
      basePage: this,
      viewModel: _viewModel,
    );
  }

  //重写底层视图
  //Widget child:build之后的试图
  Widget buildBuilder(BuildContext context, Widget child) {
    return child;
  }

  /// 保证页面活跃 这个在页面初始化的时候调用方法 然后在state注册的时候进行操作
  void setKeepAlive() => isAutoKeepAlive = true;

  @override
  void initState() {
    super.initState();

    /// 添加当前页面的路由
    _pageList.add(this);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    /// 路由订阅
    routeObserver.subscribe(
        this, ModalRoute.of(state.context) as PageRoute<dynamic>);
  }

  @override
  void dispose() {
    super.dispose();
    if (_pageList.contains(this)) {
      /// 删除当前页面的路由
      _pageList.remove(this);
    }

    /// 取消路由订阅
    routeObserver.unsubscribe(this);
  }

  /// 路由观察者的操作
  @override
  void didPush() {
    super.didPush();
    handleDidPush();
  }

  @override
  void didPop() {
    super.didPop();
    handleDidPop();
  }

  @override
  void didPopNext() {
    super.didPopNext();
    handleDidPopNext();
  }

  @override
  void didPushNext() {
    super.didPushNext();
    handleDidPushNext();
  }
}

我们通过继承 BaseView 以及实现了RouteAware, -RouterHandler这两个混入的类来实现了对BasePage的定义。

BaseView的定义

abstract class BaseView {
  /// 是否保持页面活跃
  bool isAutoKeepAlive = false;

  /// context
  BuildContext? get context => _state.context;
 
  /// state
  late BaseState _state;

  BaseState get state => _state;

  /// 注册state
  void registerState(BaseState state) {
    _state = state;
    _state.isAutoKeepAlive = isAutoKeepAlive;

    /// 页面是否保活
  }

  /// 刷新页面的操作 封装setState之后取名相同
  void setState(VoidCallback fn) {
    _state.refreshState(fn);
  }

  /// page或者widget的状态是否初始化 或者 销毁
  bool isActive = false;

  /// 生命周期
  /// 暂时只需要这么几个 其他有需要自己添加
  /// */
  @mustCallSuper
  void initState() {
    isActive = true;
  }

  @protected
  @mustCallSuper
  void didChangeDependencies() {}

  @mustCallSuper
  Widget build(BuildContext context);

  @mustCallSuper
  void reassemble() {}

  @mustCallSuper
  void dispose() {
    isActive = true;
  }

  /// 初始化获取到context
  void initStateForContext(BuildContext context) {}

  Widget get spacer => const Spacer();

  Widget gapX(double width) => SizedBox(
        width: width,
      );

  Widget gapY(double height) => SizedBox(
        height: height,
      );

  void logd(String msg) => debugPrint(msg);
}

/// 给ListView/GridView 空状态使用
Widget emptyWidgetForScrollView<T extends BaseListViewModel>(
    BuildContext context,
    {double? height,
    double? width,
    String? showText}) {
  return RefreshIndicator(
    color: Colors.blue,
    backgroundColor: Colors.white,
    onRefresh: () async => context.read<T>().onRefresh(),
    child: ListView(
      children: [
        SizedBox(height: 60.h),
        SizedBox(
          width: 185.h,
          height: 185.h,
          child: Image.asset(
            'assets/images/image/no_data.png',
            fit: BoxFit.cover,
          ),
        ),
        Center(
          child: Text(
            showText ?? "无数据",
            style: TextStyle(
                fontSize: 16.sp,
                color: Colors.black,
                fontWeight: FontWeight.w500),
          ),
        ),
      ],
    ),
  );
}

以上就是BaseView的定义,我们通过 registerState 来注册,使得这个类可以使用state相关的方法,然后通过模拟定义了生命周期对应的方法来注入到widget中。

这时候你会问,我根本就没有看到widget相关的内容,它是怎么操作的。

在BasePage中有一个 _createWidget() 方法

一个简单的Flutter快速开发框架 是它返回了一个widget

class _PageWidget<VM extends BaseViewModel> extends StatefulWidget {
  final BasePage basePage;
  final VM viewModel;

  const _PageWidget(
      {super.key, required this.basePage, required this.viewModel});

  @override
  PageWidgetState<VM> createState() => PageWidgetState<VM>();
}

class PageWidgetState<VM extends BaseViewModel>
    extends PageState<_PageWidget<VM>> {
  late BasePage _basePage;

  @override
  void initState() {
    super.initState();
    _basePage = widget.basePage;
    _basePage.registerState(this);
    _basePage.initState();
    widget.viewModel.registerState(this);
    widget.viewModel.viewModelInit();
    widget.viewModel.initData();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      /// 给viewModel实例放置context
      widget.viewModel.setContext(context);

      /// 保证initEvent的context初始化成功
      widget.viewModel.viewModelInitForContext(context);

      ///
      _basePage.initStateForContext(context);
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _basePage.didChangeDependencies();
  }

  // /// 个人觉得这个生命周期不用组装跟修改(除非你要修改setState的刷新算法)
  // @override
  // void didUpdateWidget(covariant _PageWidget oldWidget) {
  //   super.didUpdateWidget(oldWidget);
  // }
  @override
  void didUpdateWidget(covariant _PageWidget<VM> oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
  }

  @override
  void reassemble() {
    // TODO: implement reassemble
    super.reassemble();
    _basePage.reassemble();
    widget.viewModel.reassemble();
  }

  @override
  void dispose() {
    super.dispose();

    /// 取消当前所有的toast
    widget.viewModel.clearAllToast();
    widget.viewModel.viewModelDispose();
    _basePage.dispose();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ChangeNotifierProvider<VM>(
      create: (BuildContext context) => widget.viewModel,
      child: _ReBuildWidget(
        builder: (context, _) => _basePage.build(context),
      ),
    );
  }
}

class _ReBuildWidget extends SingleChildStatelessWidget {
  const _ReBuildWidget({
    super.key,
    required this.builder,
    super.child,
  });

  final Widget Function(
    BuildContext context,
    Widget? child,
  ) builder;

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    return builder(
      context,
      child,
    );
  }
}

好了��就到了我们平时写Flutter常用到的StatefulWidget了,可以看到在主要的生命周期跟方法里面我们通过BasePage的示例的方法调用来组装起来了一个widget

以上就是BasePage的封装

BaseViewModel

一个简单的Flutter快速开发框架 由于代码太多,这里不在过多的描述,通过类的定义我们就可以看出,我们混入了ChangeNotifier以及其他的工具,包括请求、路由跳转、toast、eventbus、sp存储(你也可以继续扩展)。

BaseSingleViewModel

abstract class BaseSingleViewModel<T> extends BaseViewModel {
  late T _data;

  T get data => _data;

  RequestConfig get requestConfig => setRequestConfig();

  @override
  Future<void> initData() async {
    if (await checkNet()) {
      setNoNet(message: "没有网络");
      notifyListeners();
      return;
    }
    try {
      final ResponseModel responseModel = await loadData();
      final T? modelForT = JsonConvert.fromJsonAsT<T>(responseModel.data);
      if (modelForT == null) {
        /// 一般不会出现转换错误
        setOther("数据转换错误");
      } else {
        _data = modelForT;
        onComplete(_data);
      }
      notifyListeners();
    } catch (e) {
      if (e is DioException) {
        if (e is DioUnAuthorizedError) {
        } else {
          setResponseError(message: e.message ?? "系统错误~");
        }
      } else {
        /// 这里出现问题 一般就是代码问题 请自查
        setOther(e.toString());
      }
      notifyListeners();
    }
  }

  /// 需要重写这个方法来调用sendRequest来获取数据
  Future<ResponseModel> loadData() async {
    return await sendRequest(requestConfig);
  }

  /// 数据获取之后做的处理 ~
  void onComplete(T data) {}

  /// 初始化request config
  RequestConfig setRequestConfig();
}

这里主要就是通过initData来调用后端给到的接口进行请求(viewmodel的initData在BasePage组装Widget的时候,在initState生命周期中调用)。不难看出就是一个接口的请求在widget的initState时候。

BaseListViewModel

abstract class BaseListViewModel<M, T> extends BaseViewModel {
  /// 分页页码
  int _page = 1;

  int get page => _page;

  /// 分页条目数量
  int _pageSize = 10;

  int get pageSize => _pageSize;

  void setPageSize(int pageSize) {
    _pageSize = pageSize;
    notifyListeners();
  }

  /// 刷新的控制器
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  /// 向外暴露的controller
  RefreshController get refreshController => _refreshController;

  /// list数据
  final List<T> _list = [];

  /// 向外暴露的list
  List<T> get list => _list;

  /// 设置请求的config配置
  late RequestConfig _requestConfig;

  void setRequestConfigParams({required Map<String, dynamic> otherParams}) {
    if (_requestConfig.method.toUpperCase() == 'GET') {
      final Map<String, dynamic> queryParameters =
          (_requestConfig.queryParameters ?? {});
      _requestConfig.queryParameters = queryParameters..addAll(otherParams);
    }
    if (_requestConfig.method.toUpperCase() == 'POST') {
      final Map<String, dynamic> data = _requestConfig.data ?? {};
      _requestConfig.data = data..addAll(otherParams);
    }

    _list.clear();
    notifyListeners();
    _page = 1;

    /// 重新加载数据
    onLoading();
  }

  void _setSearchParamsForRequestConfig() {
    if (_requestConfig.method.toUpperCase() == 'GET') {
      final Map<String, dynamic> queryParameters =
          (_requestConfig.queryParameters ?? {});
      _requestConfig.queryParameters = queryParameters
        ..addAll({
          'pageNo': _page,
          'pageSize': _pageSize,
        });
    }
    if (_requestConfig.method.toUpperCase() == 'POST') {
      final Map<String, dynamic> data = _requestConfig.data ?? {};
      _requestConfig.data = data
        ..addAll({
          'pageNo': _page,
          'pageSize': _pageSize,
        });
    }
  }

  // RequestConfig get requestConfig => setRequestConfig();

  /// 初始化 ---- viewModel 对应state 初始化的操作
  @override
  void initData() async {
    /// 初始化RequestConfig
    _requestConfig = initRequestConfig();

    /// 检查网络状态
    if (await checkNet()) {
      setNoNet(message: "没有网络");
      notifyListeners();
      return;
    }

    /// 保证list 是 空状态
    _list.clear();
    List<T> tempList = await getList();
    if (tempList.isEmpty) {
      setEmpty();
    } else {
      _list.addAll(tempList);
      setSuccess();
      onCompleted(_list);
    }
    notifyListeners();
  }

  /// 加载操作
  void onLoading() async {
    List<T> tempList = await getList();
    if (tempList.isEmpty) {
      _refreshController.loadNoData();
    } else {
      _list.addAll(tempList);
      _refreshController.loadComplete();
    }
    onCompleted(_list);
    notifyListeners();
  }

  /// 刷新操作
  void onRefresh() async {
    _list.clear();
    _page = 1;
    List<T> tempList = await getList();
    if (tempList.isEmpty) {
      setEmpty();
    } else {
      _list.addAll(tempList);
      setSuccess();
    }
    _refreshController.refreshCompleted(resetFooterState: true);
    onCompleted(_list);
    notifyListeners();
  }

  /// 获取list数据
  Future<List<T>> getList() async {
    try {
      final M model = await loadData();
      final List<T> tempList = formatResponseList(model);
      return tempList;
    } catch (e) {
      Log.e(e);
      rethrow;
    }
  }

  /// 获取数据 并且把返回的数据转换成model
  Future<M> loadData() async {
    /// 每次请求前 把 pageNo 跟 pageSize 挂上
    _setSearchParamsForRequestConfig();

    /// 获取数据
    final json = await sendRequest(_requestConfig);
    final M? model = JsonConvert.fromJsonAsT<M>(json.data);
    if (model == null) {
      setOther("数据转换错误");

      /// 转换数据 错误~ 基本可以自查
      throw TransformDataException();
    }

    /// 成功之后页面 +1
    _page++;

    return model;
  }

  /// 请求完成并且 数据已经给provider之后的操作
  void onCompleted(List<T> list) {}

  /// 初始化request config
  RequestConfig initRequestConfig();

  /// 格式化list数据 即 从model 到list的取值
  List<T> formatResponseList(M model);

  @override
  void viewModelDispose() {
    super.viewModelDispose();
    _refreshController.dispose();
  }
}

不多说了,跟 BaseSingleViewModel 一样,不过由于是列表,就稍微做了一下列表的处理。

RequestConfig 请求配置

class RequestConfig {
  final String path;
  final String method;
  Map<String, dynamic>? queryParameters;
  Map<String, dynamic>? data;
  final CancelToken? cancelToken;
  final Options? options;
  final ProgressCallback? onSendProgress;
  final ProgressCallback? onReceiveProgress;
  final bool showLog;

  factory RequestConfig.empty() {
    return RequestConfig(path: '', method: 'get');
  }

  RequestConfig({
    required this.path,
    required this.method,
    this.queryParameters,
    this.data,
    this.options,
    this.cancelToken,
    this.onSendProgress,
    this.onReceiveProgress,
    this.showLog = false,
  });

  factory RequestConfig.fromGet(
    final String path, {
    final Map<String, dynamic>? queryParameters,
    final CancelToken? cancelToken,
    final Options? options,
    final ProgressCallback? onSendProgress,
    final ProgressCallback? onReceiveProgress,
    final bool showLog = false,
  }) {
    return RequestConfig(
      path: path,
      method: 'GET',
      options: options,
      queryParameters: queryParameters,
      onSendProgress: onSendProgress,
      onReceiveProgress: onReceiveProgress,
      showLog: showLog,
    );
  }

  factory RequestConfig.fromPost(
    final String path, {
    final Map<String, dynamic>? data,
    final CancelToken? cancelToken,
    final Options? options,
    final ProgressCallback? onSendProgress,
    final ProgressCallback? onReceiveProgress,
    final bool showLog = false,
  }) {
    return RequestConfig(
      path: path,
      method: 'POST',
      data: data,
      options: options,
      cancelToken: cancelToken,
      onSendProgress: onSendProgress,
      onReceiveProgress: onReceiveProgress,
      showLog: showLog,
    );
  }

  RequestConfig copyWithQueryParameters(
      final Map<String, dynamic> queryParameters) {
    return RequestConfig(
        path: path, method: method, queryParameters: queryParameters);
  }

  RequestConfig copyWithData(final Map<String, dynamic> data) {
    return RequestConfig(path: path, method: method, data: data);
  }
}

RequestConfig的配置对应dio请求的配置,最终这些配置会丢在dio的网络请求里面,这里由于项目差不多用的都是get、post所以就没有过多的去封装,你也可以自己扩展。

路由、sp、toast等工具的使用可以看对应的mixin混入类

其他的内容不过多介绍,可以直接看代码。

传送门

总结

  • 使用的话按照上面的简单实用 定义好 page 、 viewmodel 即可,简化了应用程序的开发和维护,使用起来简单明朗。
  • 使用Provider进行状态管理,封装了一系列相关的工具跟组件
  • 开箱即用的开发模版项目,clone下来直接修改即可使用,使用者只需要关注业务以及页面的书写即可
  • 灵活可扩展(如果你觉得现有的东西不满足你的需求,可以扩展)
转载自:https://juejin.cn/post/7384444807433682959
评论
请登录