Flutter Architecture架构管理 Provider+get_it

由于Flutter中的状态管理问题,Flutter中有着ephemeral短时状态和app状态,且会因为业务需要,在两种状态之间做出转换,所以状态管理是个值得研究的问题。因此引出了架构管理问题,其实整个架构就是为了合理的管理状态而设计的。

类似于Android中的MVC 、MVVM、 MVI、等架构,Flutter也有,一个包含各类架构的sample地址:

https://github.com/brianegan/flutter_architecture_samples

上诉例子中,原文有段话如下:

Your own particular priorities will impact how you implement the concepts in these projects, so you should not consider these samples to be canonical examples.

翻译就是:您自己的特定优先级将影响您在这些项目中实现这些架构概念的方式,因此您不应将这些示例视为规范示例。
但是一个合理的架构仍然是需要去做探究的

“Flutter Architecture架构管理”系列文档将会探讨一下如何使用一个合理的架构。


由于本文使用Providerget_it库,所以如果不了解的同学,可以参考我之前的两篇文章:
Flutter中的依赖注入——get_it
Flutter状态管理1-ChangeNotifierProvider的使用

本文用于展示Flutter架构的app有三个页面,登陆、列表、详情,录屏如下:
在这里插入图片描述

项目整体结构如下:

在这里插入图片描述
各文件夹或者文件的用途:

  • models 存放数据实体类
  • services 存放服务类,这里的服务指的是一些请求网络数据或者持久化数据的类,或者是提供一些全局使用的数据服务类
  • viewmodels 连接view和数据的类,数据通常来源于网络,并用于更新View,后面会详细介绍
  • shared 存放一些通用的样式,颜色,UI相关的工具类等
  • views 存放各个页面
  • widgets 存放自定义的widget
  • router.dart 存放各页面直接的路由配置
  • locator.dart 存放Service Locator对象,用于依赖注入
  • main.dart 应用入口

BaseView和BaseModel的基类

如果想看View和ViewModel的基类产生的详细过程,可以参考Flutter Architecture - My Provider Implementation Guide,这里直接阐述最终的结果。

ViewState

这里定义一个枚举类,包含加载中、夹在结束两种状态,如果需要,可以再自己添加加载失败状态,
后续View会根据这些状态来显示相应的Widget

/// Represents the state of the view
enum ViewState { Idle, Busy }

关于路由

建议使用以下方式:

  1. 新建一个全局的路由配置文件, 添加通用的错误页面(default分支)
class Router {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => HomeView());

      case 'login':
        return MaterialPageRoute(builder: (_) => LoginView());
      case '/':
        return MaterialPageRoute(builder: (_) => PostView());
      case 'post':
        var post = settings.arguments as Post;
        return MaterialPageRoute(builder: (_) => PostView(post: post));
      default:
        return MaterialPageRoute(builder: (_) {
          return Scaffold(
              body: Center(
            child: Text('No route defined for ${settings.name}'),
          ));
        });
    }
  }
}
  1. 配置路由文件
    使用MaterialApp里的属性onGenerateRouteinitialRoute
MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(),
    initialRoute: 'login',
    onGenerateRoute: Router.generateRoute,
  );
  1. 发起页面跳转
onPressed: () async {
    var loginSuccess = await model.login(controller.text);
    if (loginSuccess) {
      Navigator.pushNamed(context, '/');
    }
  }

关于导航,可以参考我的另外一篇文章Flutter中的导航Navigation总结

BaseModel

按照之间介绍ChangeNotifierProvider使用的文章中所说,我们定义一个数据类,以登陆页为例子,

class LoginModel extends ChangeNotifier {
  final AuthenticationService _authenticationService = locator<AuthenticationService>();
  ViewState _state = ViewState.Idle;

  ViewState get state => _state;

  void setState(ViewState viewState) {
    _state = viewState;
    notifyListeners();
  }

   Future<bool> login(String userIdText) async {
    setState(ViewState.Busy);

    var userId = int.tryParse(userIdText);
    var success =  await _authenticationService.login(userId);

    setState(ViewState.Idle);
    return success;
  }
}

⚠️注意:

  • 这里的Model类继承自ChangeNotifier
  • Model类内部通过使用locator(依赖注入库get_it)获取需要的服务对象
  • 在数据没有准备好之前先设置成ViewState.Busy,获取到数据后再设置成ViewState.Idle

我们可以抽取Model类的通用逻辑到BaseModel中,如下:

class BaseModel extends ChangeNotifier {
  ViewState _state = ViewState.Idle;

  ViewState get state => _state;

  void setState(ViewState viewState) {
    _state = viewState;
    notifyListeners();
  }
}

然后我们可以改写上面登陆页面的Model,如下:

class LoginModel extends BaseModel {
  final AuthenticationService _authenticationService =
      locator<AuthenticationService>();

  String errorMessage;

  Future<bool> login(String userIdText) async {
    setState(ViewState.Busy);

    var userId = int.tryParse(userIdText);

    if (userId == null) {
      errorMessage = 'Value entered is not a number';

      setState(ViewState.Idle);
      return false;
    }

    var success = await _authenticationService.login(userId);

    setState(ViewState.Idle);
    return success;
  }
}

BaseView

根据View的通用逻辑,即需要用到自己的Model,一个继续自Provider的root Widget ,一个拥有builder参数的Consumer子类对象,我们可以抽取一个BaseView

class BaseView<T extends BaseModel> extends StatelessWidget {
  final Widget Function(BuildContext context, T value, Widget child) builder;

  BaseView({this.builder});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<T>(
      builder: (context) => locator<T>(),
      child: Consumer<T>(builder: builder),
    );
  }
}

但是为了使得BaseView变得更加通用,或者说,通常我们不想将一个页面中的很多Widget都改成StatefulWidget,但是我们又想拥有由于数据状态的变化引起View的更新的功能。
因此,我们可以将BaseView继承自StatefulWidget,并且在构造函数中传入两个函数,

  • onModelReady是负责回到model到View层,然后View层根据这个model数据去显示不同的Widget或者填充内容
  • builder负责根据传入的model来绘制UI Widget
  • 同时在initState()里,检查传入的onModelReady回调是否为空,不为空则调用View的onModelReady,并传入model数据
class BaseView<T extends ChangeNotifier> extends StatefulWidget {
  final Widget Function(BuildContext context, T value, Widget child) builder;
  final Function(T) onModelReady;

  BaseView({@required this.builder, this.onModelReady});

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

class _BaseViewState<T extends ChangeNotifier> extends State<BaseView<T>> {
  T model = locator<T>();

  @override
  void initState() {
    if (widget.onModelReady != null) {
      widget.onModelReady(model);
    }
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<T>(
      builder: (context) => model,
      child: Consumer<T>(builder: widget.builder),
    );
  }
}

关于全局数据的使用

在这个app中, 用户信息数据是每个页面都要使用到的,这里可以使用一个全局的单例类,但是建议使用provider包里的StreamProvider,

比如,我们可以在登陆成功后,保存用户信息到StreamController<User>对象中,然后在MaterialApp中这样使用:

class AuthenticationService {
  Api _api = locator<Api>();

  StreamController<User> userController = StreamController<User>();

  Future<bool> login(int userId) async {
    // Not real login, we'll just request the user profile
    var fetchedUser = await _api.getUserProfile(userId);
    var hasUser = fetchedUser != null;
    if (hasUser) {
      userController.add(fetchedUser);
    }

    return hasUser;
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamProvider<User>(
        initialData: User.initial(),
        builder: (context) => locator<AuthenticationService>().userController,
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(),
          initialRoute: 'login',
          onGenerateRoute: Router.generateRoute,
        ));
  }
}

然后在整个app中,都可以使用Provider.of<User>(context)来获取User用户数据,而不用在页面间传递数据,比如在列表页根据用户ID去请求数据

class HomeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseView<HomeModel>(
        onModelReady: (model) {
          model.getPosts(Provider.of<User>(context).id);
        },
        builder: (context, model, child) => Scaffold(
          backgroundColor: backgroundColor,
          body: Column()));
  }
}

至此,架构完成,其他列表页和详情页都可以参照此方式来处理。当然这只是一个起步,你可以根据需要自己添加其他所需,或者创建更具体的文件夹目录…等等

该架构整体源码:https://github.com/chenyucheng97/Flutter-Mobile-Codelabs/tree/master/provider-architecture

参考:
使用官方的Provider来管理状态的一个架构:
Flutter Provider Architecture for State Management | Flutter Provider
视频对应的文章解说:
Flutter Architecture - My Provider Implementation Guide
该作者还有很多其他优秀的项目可以参考:
https://www.filledstacks.com/tutorials

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值