
(原始文章以英文在Medium上发表)
Flutter提供了一个现代的响应框架,大量的小部件和工具。 但是,不幸的是,文档与Android应用程序的推荐体系结构指南完全不同 。
没有理想的通用体系结构可以满足技术任务的任何可能的要求,但让我们承认,我们正在研究的大多数移动应用程序都具有以下功能:
- 请求和下载数据。
- 为用户转换和准备数据。
- 从数据库或文件系统写入和读取数据。
考虑到所有这些,我创建了一个演示应用程序,该应用程序使用不同的体系结构方法解决了相同的问题。
最初,显示给用户的屏幕是位于中心的“加载用户数据”按钮。 当用户单击按钮时,将进行异步数据加载,并且该按钮将被加载指示器替换。 数据下载完成后,下载指示符将替换为数据。
因此,让我们开始吧。

资料
为了简化任务,我创建了Repository
类,其中包含getUser()
方法。 此方法模拟从网络异步加载数据并返回Future<User>
。
如果您不熟悉Dart中的Futures和异步编程,我们可以在这里阅读更多信息,并阅读Future类的文档 。
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()方法会通知框架此对象的内部状态已更改,并且可能会影响子树中的用户界面。 这就是框架在此状态对象上调用build方法的原因。
这意味着在调用setState()
方法之后,框架将再次调用build(BuildContext context)
方法,这将重新创建整个小部件树 。 由于_isLoading
字段的值_isLoading
更改为true
,而不是_buildBody()
方法,因此将调用_buildBody()
方法,并且进度指示器将显示在屏幕上。
当我们从getUser()
获得回调并调用方法时,会发生完全相同的事情。
setState()
将新值分配给_isLoading
和_user
字段。
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
优点
- 低进入门槛。
- 无需第三方库。
缺点
- 当小部件的状态更改时,每次都会完全重新创建小部件树。
- 违反唯一责任原则。 小部件不仅负责创建UI,还负责加载数据,业务逻辑和状态管理。
- 有关如何显示当前状态的决定直接在UI代码中做出。 如果状态变得更加复杂,则代码的可读性将大大降低。
范围模型
范围模型是第三方库 。 这是开发人员的描述方式:
一组实用程序,可让您将祖先窗口小部件的数据模型传输到其所有后代。 除此之外,当模型数据更改时,将重新创建所有使用该模型的后代。 该库最初取自Fuchsia项目代码。
让我们创建与上一个示例相同的屏幕,但是使用范围模型。 首先,我们需要将范围模型库添加到项目中。 在“ dependencies
部分中,将scoped_model
依赖项添加到scoped_model
文件中。
scoped_model: ^1.0.1
让我们看一下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代码中的逻辑。 在“范围模型”示例中,我们仍然检查_isLoading
字段的值_isLoading
true
还是false
以确定要创建哪个窗口小部件。 对于BLoC,我们将新状态传递给流,而UserBlocScreen
小部件的唯一任务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 )的最后一个值(Snapshot)创建自己。
优点
- 无需第三方库。
- 业务逻辑,状态管理和数据加载与UI代码分开。
- 反应性 无需像在Scoped Model
notifyListeners()
的示例中那样调用其他方法。
缺点
- 进入门槛稍高。 需要使用流或rxdart的经验。
友情链接
您可以通过从我的github存储库下载完整代码来阅读完整代码。
原始文章发表在Medium