记录flutter项目之——MVVM模式

603 阅读4分钟

文中所用环境及配置

flutter 2.2.3

dependencies:
  provider: 5.0.0

  dio: ^4.0.0

  logger: ^1.1.0
  flutter_smart_dialog: ^3.1.0

目录结构

image.png

app: 管理类

  • app_config: 基本的app配置参数
  • application: 初始化项目配置
  • route_paths: 页面路由统一存放
  • routes: 路由管理

base: 基类

  • base_widget:widget页面基类
  • base_state_model: viewModel基类,包含一些公共的参数,函数

common: 公共组件
model: 数据模型
net: 网络请求封装
page: 页面UI存放
service: 请求
utils: 工具
view_model: 页面对应的viewModel

mvvm之viewModel:

这里使用到了flutter的ChangeNotifier类,它可以在数据改变的时候调用notifyListeners()函数通知组件更新状态。

开始

base_widget 基类

所有页面都继承自此类,初步实现ViewModel的绑定

class BaseWidget<T extends BaseStateModel> extends StatefulWidget {
  final ValueWidgetBuilder<T> builder;
  final T model;
  final bool autoDispose;
  final Function(T model) onModelReady;
  //以下在使用默认布局时使用,仅当builder为空时有效
  final Widget appBar;
  final Widget Function(BuildContext context, T model) child;
  final bool showLoadView;//显示页面初始加载动画

  BaseWidget({
    Key key,
    @required this.model,
    this.builder,
    this.autoDispose: true,
    this.onModelReady,
    this.appBar,
    this.child,
    this.showLoadView: true,
  }) : super(key: key);

  @override
  _BaseState<T> createState() => _BaseState<T>();
}

class _BaseState<T extends BaseStateModel> extends State<BaseWidget<T>> {
  T model;

  @override
  void initState() {
    super.initState();
    model = widget.model;
    widget.onModelReady?.call(model);
    //首先初始化界面状态为加载中
    setState(() {
      model.pageState = getState(showLoadView: widget.showLoadView);
    });
    model.initData();
  }

  @override
  void dispose() {
    if (widget.autoDispose) model.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<T>.value(
      value: model,
      child: Consumer<T>(
        //builder为空时显示默认的布局
        //默认布局为标题栏 + body
        builder: widget.builder ?? (_context, _model, _child) {
          return Scaffold(
            appBar: widget.appBar ?? const SizedBox(),
            body: PageStateWidget(
              pageState: model.pageState,
              refresh: () {
                _model.refresh();
              },
              child: widget.child(_context, _model) ?? const SizedBox(),
            ),
          );
        },
      ),
    );
  }
}

base_state_model 基类

此类为所有ViewModel的基类,后续可能还会添加其他方法(如分页加载)。
因项目需求,且为了避免在网络请求过程中直接操作view,所以另外创建了一个PageStateWidget来管理页面的多状态(如系统异常、请求失败、加载中),并通过request方法来统一进行网络请求前后的处理。

abstract class BaseStateModel with ChangeNotifier {
  /// 防止页面销毁后,异步任务才完成,导致报错
  bool _disposed = false;
  PageState pageState; //当前页面的状态值

  ///初始化
  @protected
  void initData();

  ///刷新
  ///showLoadView: 显示加载动画
  void refresh({bool showLoadView: true}) {
    //刷新时使用
    pageState = getState(
      showLoadView: true,
    );
    notifyListeners();
    initData();
  }

  @override
  void notifyListeners() {
    if (!_disposed) {
      super.notifyListeners();
    }
  }

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }

  ///二次封装 用于请求接口返回的处理 例如弹窗、页面状态改变
  ///request 接口
  ///showLoadDialog 显示加载弹窗
  ///showLoadView: 显示加载动画
  ///showError: 显示异常
  ///gotoLogin 未登录是否自动前往登录页
  Future<dynamic> request({
    Future<dynamic> request,
    bool showLoadDialog: true,
    bool showLoadView: false,
    bool showError: true,
    bool gotoLogin: true,
  }) async {
    if (showLoadDialog) showLoading();
    var response = await request;
    hideLoading();
    switch (response.code) {
      case 401:
        if (gotoLogin) {
          showToast("登录失效,正在前往登录页面");
          ///取消当前cancelToken的所有请求
          httpClient.cancelToken.cancel();
          Future.delayed(Duration(seconds: 1)).then((value) {
            httpClient.cancelToken = CancelToken();
            closeAllAndJumpPage(RoutePaths.login);
          });
        }
        break;
      case 500:
        showToast("系统异常啦~");
        break;
      case 400:
      case -1:
        showToast(response.message);
        break;
      default:
        break;
    }
    pageState = getState(
      code: response?.code ?? -1,
      showLoadView: showLoadView,
      showError: showError,
    );
    notifyListeners();
    return response?.data ?? null;
  }
}

PageStateWidget 页面多状态

根据pageState的传入值控制当前页面显示哪种内容。
这里仅做简单的处理,可根据实际需求更改。

class PageStateWidget extends StatefulWidget {
  final PageState pageState;
  final Widget child;
  final bool showNoNetwork; //显示无网络状态
  final Function refresh;

  PageStateWidget({
    @required this.pageState,
    @required this.child,
    this.showNoNetwork: true,
    this.refresh,
  });

  @override
  State<StatefulWidget> createState() => _PageState();
}

class _PageState extends State<PageStateWidget> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    if (widget.pageState == PageState.normal) {
      return widget.child ?? SizedBox();
    } else if (widget.pageState == PageState.loading) {
      return _loading();
    } else if (widget.pageState == PageState.noNetwork &&
        widget.showNoNetwork) {
      return _NoNetWork(widget.refresh);
    } else if (widget.pageState == PageState.systemAbnormal) {
      return _systemError();
    } else
      return widget.child ?? SizedBox();
  }

  Widget _loading() {
    return Container(
      child: Text("加载中"),
    );
  }

  Widget _systemError() {
    return Container(
      child: Text("系统异常"),
    );
  }
}

///无网络状态
class _NoNetWork extends StatelessWidget {
  final Function refresh;

  _NoNetWork(this.refresh);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Column(
        children: [
          Text(
            "网络离家出走了",
          ),
          MaterialButton(
            child: Text(
              "戳这重试",
            ),
            onPressed: () {
              if (refresh != null) refresh();
            },
          ),
        ],
      ),
    );
  }
}

/// 返回当前页面状态
/// code: 请求返回的状态码
/// result: 当前网络状态
/// showLoadView: 显示加载view
/// showError: 显示异常界面
PageState getState({int code, ConnectivityResult result,
  bool showLoadView: true, bool showError: true}) {
  if (result == null) {
    result = CommonData.connectivityResult;
  }
  switch (result) {
    case ConnectivityResult.none:
      return PageState.noNetwork;
    case ConnectivityResult.mobile:
    case ConnectivityResult.wifi:
      switch (code) {
        case 200:
          return PageState.normal;
        case 500:
          return showError ? PageState.systemAbnormal : PageState.normal;
        default:
          return showLoadView ? PageState.loading : PageState.normal;
      }
      break;
    default:
      return showLoadView ? PageState.loading : PageState.normal;
  }
}

enum PageState {
  ///默认,显示child
  normal,

  ///加载中
  loading,

  ///无网络
  noNetwork,

  ///系统异常
  systemAbnormal,
}

RoutePaths

class RoutePaths {
  static const String home = "home";
  static const String login = "login";
}

Route 路由管理

class MyRoute {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case RoutePaths.home:
        return MaterialPageRoute(builder: (_) => Home());
      case RoutePaths.login:
        return MaterialPageRoute(builder: (_) => Login());
      default:
        return MaterialPageRoute(builder: (_) => Home());
    }
  }
}

main

final logger = Logger(
  printer: PrettyPrinter(
      methodCount: 2, // number of method calls to be displayed
      errorMethodCount: 8, // number of method calls if stacktrace is provided
      lineLength: 300, // width of the output
      colors: true, // Colorful log messages
      printEmojis: true, // Print an emoji for each log message
      printTime: false, // Should each log print contain a timestamp
  ),
);
//final httpClient = HttpClient.getInstance();
void main() {
  runApp(MyApp());
}

my_app

此处应注意initialRoute和onGenerateRoute的实现。
navigatorObservers: [FlutterSmartDialog.observer] 与 FlutterSmartDialog(child: child)为插件flutter_smart_dialog的初始化,如果不使用此插件可不添加。

class MyApp extends StatefulWidget {

  @override
  State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {

  @override
  void initState() {
    super.initState();
    Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
      // WIFI网络
      if(result == ConnectivityResult.wifi){
        print("当前为WIFI网络");
        // 移动网络
      }else if(result == ConnectivityResult.mobile){
        print("当前为手机网络");
        // 没有网络
      }else{
        print("当前没有网络");
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: true,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        checkboxTheme: Theme.of(context).checkboxTheme.copyWith(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(100),
          ),
        ),
      ),
      initialRoute: RoutePaths.home,
      onGenerateRoute: MyRoute.generateRoute,
      navigatorObservers: [FlutterSmartDialog.observer],
      navigatorKey: AppConfig.globalKey,
      builder: (context, child) => MediaQuery(
        //设置文字大小不随系统设置改变
        data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
        child: Scaffold(
          body: GestureDetector(
            onTap: () {
              FocusScopeNode currentFocus = FocusScope.of(context);
              if (!currentFocus.hasPrimaryFocus &&
                  currentFocus.focusedChild != null) {
                FocusManager.instance.primaryFocus.unfocus();
              }
            },
            child: FlutterSmartDialog(child: child),
          ),
        ),
      ),
    );
  }
}

使用

创建Home页面

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseWidget<HomeViewModel>(
      model: HomeViewModel(),
      onModelReady: (model) {

      },
      appBar: AppBar(
        title: Text("标题"),
      ),
      child: (context, model) => Container(),
    );
  }
}

HomeViewModel

class HomeViewModel extends BaseStateModel {

  ///此方法进入页面后自动加载
  @override
  void initData()async {
    var response = await request(
      request: HomeService().getHomePage(),
      showLoadDialog: false,
      gotoLogin: false,
    );
  }
}

HomeService

class HomeService {

  Future<BaseResponse<dynamic>> getHomePage() async {
    List<Records> result;
    var response = await HttpClient.getInstance().get(url: NetWorkAddress.homePage, params: {
      "page": 2,
      "size": 10,
    });
    if (response.code == 200) {
      result = response.data['list'].map<Records>((item) => Records.fromJson(item)).toList();
      response.data = result;
    }
    return response;
  }
}

结束

当前为基础封装,后续可能会根据项目继续改进~