
(最初在Medium上发布)
Flutter提供了一个现代的反应式框架,丰富的小部件集合和工具,但是没有什么类似于Android的应用程序架构指南 。
确实,没有最终的架构可以满足所有可能的要求,但是让我们面对这样一个事实,我们正在开发的大多数移动应用程序至少具有以下某些功能:
- 向/向网络请求/上传数据。
- 映射,转换,准备数据并将其呈现给用户。
- 将数据放入数据库或从数据库获取数据。
考虑到这一点,我创建了一个示例应用程序,它使用三种不同的体系结构方法来解决完全相同的问题。
屏幕中央将为用户显示“加载用户数据”按钮。 当用户单击按钮时,将触发异步数据加载,并将该按钮替换为加载指示器。 加载数据后,将加载指示器替换为数据。
让我们开始吧。

资料
为简单起见,我创建了Repository
类,其中包含模拟异步网络调用并返回带有硬编码值的Future<User>
对象的getUser()
方法。
如果您不熟悉Dart中的Futures和异步编程,则可以通过阅读本教程并阅读文档来进一步了解它。
class Repository { Future<User> getUser() async { await Future.delayed(Duration(seconds: 2)); return User(name: 'John', surname: 'Smith'); } }
class User { User({ @required this.name, @required this.surname, }); final String name; final String surname; }
香草味
让我们以大多数开发人员在阅读Flutter官方文档后会做的方式构建应用程序。
使用Navigator
导航到VanillaScreen
屏幕
Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), );
由于小部件的状态在小部件的生命周期中可能会改变几次,因此我们应该扩展StatefulWidget
。 实现有状态的窗口小部件还需要具有State
类。 _VanillaScreenState
类中的bool _isLoading
和User _user
字段表示小部件的状态。 在调用build(BuildContext context)
方法之前,将初始化这两个字段。
class VanillaScreen extends StatefulWidget { VanillaScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _VanillaScreenState(); } class _VanillaScreenState extends State<VanillaScreen> { bool _isLoading = false; User _user; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Vanilla'), ), body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ), ); } Widget _buildBody() { if (_user != null) { return _buildContent(); } else { return _buildInit(); } } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { setState(() { _isLoading = true; }); widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); }, ), ); } Widget _buildContent() { return Center( child: Text('Hello ${_user.name} ${_user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } }
当创建窗口小部件状态对象时,将调用build(BuildContext context)
方法来构建UI。 有关应构建为代表当前状态的小部件的所有决定均在UI声明代码中做出。
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
为了在用户单击“加载用户详细信息”按钮时显示进度指示器,我们执行以下操作。
setState(() { _isLoading = true; });
调用setState()会通知框架此对象的内部状态已更改,该方式可能会影响此子树中的用户界面,这将导致框架为该State对象安排构建。
这意味着在调用setState()
方法后,框架将再次调用build(BuildContext context)
方法,并重新构建整个小部件树 。 由于_isLoading
现在设置为true
_buildLoading()
将调用_buildBody()
而不是_buildBody()
并且将在屏幕上显示加载指示器。 当我们处理getUser()
回调并调用setState()
重新分配_isLoading
和_user
字段时,情况_user
。
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
优点
- 易于学习和理解。
- 不需要第三方库。
缺点
- 每次小部件状态更改时,整个小部件树都会重新构建。
- 这打破了单一责任原则。 Widget不仅负责构建UI,还负责数据加载,业务逻辑和状态管理。
- 在UI声明代码中做出有关应如何表示当前状态的决定。 如果我们有一些更复杂的状态代码,则可读性将降低。
范围模型
范围模型是Flutter框架中未包含的第三方程序包 。 这是范围模型的开发者如何描述它:
一组实用程序,可让您轻松地将数据模型从父Widget传递到其后代。 此外,当更新模型时,它还会重建使用该模型的所有子代。 该库最初是从Fuchsia代码库中提取的。
让我们使用范围模型构建相同的屏幕。 首先,我们需要在pubspec.yaml
部分的pubspec.yaml
添加scoped_model
依赖项来安装Scoped Model软件包。
scoped_model: ^1.0.1
让我们看一下UserModelScreen
小部件,并将其与之前未使用范围模型构建的示例进行比较。 让我们看一下UserModelScreen小部件,并将其与之前未使用范围模型构建的示例进行比较。 当我们想让模型对所有小部件的后代可用时,我们应该用通用的ScopedModel包装它,并提供一个小部件和一个模型。
class UserModelScreen extends StatefulWidget { UserModelScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserModelScreenState(); } class _UserModelScreenState extends State<UserModelScreen> { UserModel _userModel; @override void initState() { _userModel = UserModel(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return ScopedModel( model: _userModel, child: Scaffold( appBar: AppBar( title: const Text('Scoped model'), ), body: SafeArea( child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) { return _buildLoading(); } else { if (model.user != null) { return _buildContent(model); } else { return _buildInit(model); } } }, ), ), ), ); } Widget _buildInit(UserModel userModel) { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { userModel.loadUserData(); }, ), ); } Widget _buildContent(UserModel userModel) { return Center( child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } }
在前面的示例中,当窗口小部件的状态更改时,整个窗口小部件树都已重建。 但是我们实际上是否需要重建整个屏幕? 例如AppBar根本不应该更改,因此重建它毫无意义。 理想情况下,我们应该只重建那些已更新的小部件。 范围模型可以帮助我们解决该问题。
ScopedModelDescendant<UserModel>
小部件用于在小部件树中查找UserModel
。 每当UserModel
通知已发生更改时,它将自动重建。
另一个改进是UserModelScreen
不再负责状态管理和业务逻辑。
让我们看一下UserModel
代码。
class UserModel extends Model { UserModel(this._repository); final Repository _repository; bool _isLoading = false; User _user; User get user => _user; bool get isLoading => _isLoading; void loadUserData() { _isLoading = true; notifyListeners(); _repository.getUser().then((user) { _user = user; _isLoading = false; notifyListeners(); }); } static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); }
现在, UserModel
保留并管理状态。 为了通知侦听器(和重建后代)更改已发生,应调用notifyListeners()
方法。
优点
- 业务逻辑,状态管理和UI代码分离。
- 简单易学。
缺点
- 需要第三方库。
- 随着模型变得越来越复杂,很难跟踪何时应调用
notifyListeners()
。
信用证
BLoC(商务逻辑组件)是Google开发人员推荐的模式。 它利用流功能来管理和传播状态更改。
对于Android开发人员:您可以将Bloc
对象视为ViewModel
而将StreamController
视为LiveData
。 由于您已经熟悉这些概念,因此这将使以下代码非常简单。
class UserBloc { UserBloc(this._repository); final Repository _repository; final _userStreamController = StreamController<UserState>(); Stream<UserState> get user => _userStreamController.stream; void loadUserData() { _userStreamController.sink.add(UserState._userLoading()); _repository.getUser().then((user) { _userStreamController.sink.add(UserState._userData(user)); }); } void dispose() { _userStreamController.close(); } } class UserState { UserState(); factory UserState._userData(User user) = UserDataState; factory UserState._userLoading() = UserLoadingState; } class UserInitState extends UserState {} class UserLoadingState extends UserState {} class UserDataState extends UserState { UserDataState(this.user); final User user; }
状态更改时,不需要其他方法调用即可通知订户。
我创建了3个类来表示屏幕的可能状态:
用户打开状态为中间带有按钮的屏幕时,为状态的UserInitState
。
状态的UserLoadingState
,在加载数据时显示加载指示符时。
数据加载并显示在屏幕上时的状态的UserDataState
。
以这种方式传播状态更改,使我们可以摆脱UI声明代码中的所有逻辑。 在使用范围模型的示例中,我们仍在检查UI声明代码中_isLoading
是否为true
,以确定应该呈现哪个小部件。 如果使用BLoC,我们将传播屏幕状态,而UserBlocScreen
小部件的唯一责任是呈现此状态的UI。
class UserBlocScreen extends StatefulWidget { UserBlocScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserBlocScreenState(); } class _UserBlocScreenState extends State<UserBlocScreen> { UserBloc _userBloc; @override void initState() { _userBloc = UserBloc(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc'), ), body: SafeArea( child: StreamBuilder<UserState>( stream: _userBloc.user, initialData: UserInitState(), builder: (context, snapshot) { if (snapshot.data is UserInitState) { return _buildInit(); } if (snapshot.data is UserDataState) { UserDataState state = snapshot.data; return _buildContent(state.user); } if (snapshot.data is UserLoadingState) { return _buildLoading(); } }, ), ), ); } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { _userBloc.loadUserData(); }, ), ); } Widget _buildContent(User user) { return Center( child: Text('Hello ${user.name} ${user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } @override void dispose() { _userBloc.dispose(); super.dispose(); } }
与前面的示例相比, UserBlocScreen
代码变得更加简单。 要听状态更改的变化,我们使用StreamBuilder 。 StreamBuilder
是一个StatefulWidget
,它基于与Stream交互的最新快照构建自身。
优点
无需第三方库。
业务逻辑,状态管理和UI逻辑分离。
它是反应性的。 不需要像Scoped Model的notifyListeners()
那样的其他调用。
缺点
需要具有使用流或rxdart的经验。
友情链接
您可以从此github存储库中检出以上示例的源代码。
最初文章发表在Medium