内容简介:Flutter中的响应式编程(Reactive Programming)、流(Streams)、业务逻辑组件(BloC)以及实际使用案例难度:中级在前一段时间介绍了业务逻辑组件(BloC),响应式编程(Reactive Programming)和流(Streams)这些概念之后,我想与你分享一些我经常使用的并且个人觉得十分有用的模式,应该会是很有趣的。在我的开发过程中,这些模式使我节省了大量时间,并使我的代码更易于阅读和调试。
Flutter中的响应式编程(Reactive Programming)、流(Streams)、业务逻辑组件(BloC)以及实际使用案例
难度:中级
介绍
在前一段时间介绍了业务逻辑组件(BloC),响应式编程(Reactive Programming)和流(Streams)这些概念之后,我想与你分享一些我经常使用的并且个人觉得十分有用的模式,应该会是很有趣的。在我的开发过程中,这些模式使我节省了大量时间,并使我的代码更易于阅读和调试。
我要讨论的话题有:
- BLoC Provider 以及 InheritedWidget
- 在哪初始化BLoc
- 事件-状态 允许基于事件对状态转换作出响应
-
表单验证 允许依据条目和验证控制表单的行为。(我解释的解决方案还包括密码和重新输入密码的比较)。
-
Part of
允许Widget根据其在列表中的存在来调整其行为。
完整的源代码可以在 GitHub 上下载。
我借此文章的机会介绍我的另一个版本的BlocProvider,它现在依赖于一个InheritedWidget。
使用InheritedWidget的优点是我们获得了性能上的提升。
让我解释…
1.1 之前的实现
我之前版本的BlocProvider是按常规的StatefulWidget实现的,如下所示:
abstract class BlocBase {
void dispose();
}
//泛型Bloc Provider
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}
我使用StatefulWidget从dispose()方法中受益,以确保在不再需要时释放BLoC分配的资源。
这很好用但从性能角度来看并不是最佳的。
context.ancestorWidgetOfExactType()是一个O(n)函数。为了检索对应于某种类型的请求的祖先,它从上下文开始向上导航树,并且一次递归地向上移动一个父,直到完成。如果从上下文到祖先的距离很小,则可以接受对此函数的调用,否则应该避免。这是这个函数的代码。
@override
Widget ancestorWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
while (ancestor != null && ancestor.widget.runtimeType != targetType)
ancestor = ancestor._parent;
return ancestor?.widget;
}
1.2 新的实现
新实现依赖于StatefulWidget,并结合InheritedWidget:
Type _typeOf<T>() => T;
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final Widget child;
final T bloc;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>>{
@override
void dispose(){
widget.bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return new _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
_BlocProviderInherited({
Key key,
@required Widget child,
@required this.bloc,
}) : super(key: key, child: child);
final T bloc;
@override
bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;
}
这个解决方案的优点是性能。
由于使用了InheritedWidget,它现在可以调用context.ancestorInheritedElementForWidgetOfExactType()函数,它是一个O(1),这意味祖先是立即获取的,如其源代码所示:
@override
InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null
? null
: _inheritedWidgets[targetType];
return ancestor;
}
这是因为事实上,所有InheritedWidgets都由Framework记住了。
为什么使用ancestorInheritedElementForWidgetOfExactType?
您可能已经注意到我使用ancestorInheritedElementForWidgetOfExactType方法而不是通常的inheritFromWidgetOfExactType。
原因是我不希望调用BlocProvider的上下文被注册为InheritedWidget的依赖项,因为我不需要它。
1.3 如何使用新的BlocProvider?
1.3.1 Bloc的注入
Widget build(BuildContext context){
return BlocProvider<MyBloc>{
bloc: myBloc,
child: ...
}
}
1.3.2 Bloc的获取
Widget build(BuildContext context){
MyBloc myBloc = BlocProvider.of<MyBloc>(context);
...
}
从哪初始化BLoc
要回答这个问题,你需要确认它的使用范围。
2.1 应用中随处可用
假设你必须处理与用户身份验证/配置文件,用户设置,购物车相关的一些机制……任何需要从应用程序的任何可能部分(例如,从不同页面)获得BLoC的任何机制,存在两种方式使这个BLoC可访问。
全局单例的使用
此解决方案依赖于使用Global对象,对所有对象实例化,而不是任何Widget树的一部分。
import 'package:rxdart/rxdart.dart';
class GlobalBloc {
///
/// Streams related to this BLoC
///
BehaviorSubject<String> _controller = BehaviorSubject<String>();
Function(String) get push => _controller.sink.add;
Stream<String> get stream => _controller;
///
/// Singleton factory
///
static final GlobalBloc _bloc = new GlobalBloc._internal();
factory GlobalBloc(){
return _bloc;
}
GlobalBloc._internal();
///
/// Resource disposal
///
void dispose(){
_controller?.close();
}
GlobalBloc globalBloc = GlobalBloc();
要使用此BLoC,您只需导入该类并直接调用其方法,如下所示:
import 'global_bloc.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
globalBloc.push('building MyWidget');
return Container();
}
}
如果您需要一个独特的BLoC并且需要从应用程序内部的任何位置访问,这是一个可接受的解决方案。
- 这是非常容易使用;
- 它不依赖于任何BuildContext;
- 无需通过任何BlocProvider查找BLoC,
- 为了释放它的资源,只需确保将应用程序实现为StatefulWidget,并在应用程序Widget的重写dispose()方法中调用globalBloc.dispose()。
许多纯粹主义者反对这种解决方案。我不知道为什么,但是…所以让我们看看另一个……
2.1.2 把它放在所有东西的顶部
在Flutter中,所有页面的祖先本身必须是MaterialApp的父级。这是因为页面(或路径)被包装在OverlayEntry中,这是所有页面的公共堆栈的子项。
换句话说,每个页面都有一个Buildcontext,它独立于任何其他页面。这就解释了为什么在不使用任何技巧的情况下,2页(路线)不可能有任何共同点。
因此,如果您需要在应用程序中的任何位置使用BLoC,则必须将其作为MaterialApp的父级,如下所示:
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: InitializationPage(),
),
);
}
}
2.2 在子树上能访问
大多数情况下,您可能需要在应用程序的某些特定部分使用BLoC。
作为一个例子,我们可以想到BLoC将用于的讨论线程
- 与服务器交互以检索,添加,更新帖子
- 列出要在特定页面中显示的线程
- … 对于此示例,您不需要此BLoC可用于整个应用程序,而是需要某些Widgets(树的一部分)。
第一种解决方案可能是将BLoC注入Widgets树的根目录,如下所示:
class MyTree extends StatelessWidget {
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: MyBloc(),
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
MyBloc = BlocProvider.of<MyBloc>(context);
return Container();
}
}
这样,所有小部件都将通过调用BlocProvider.of方法访问BLoC。
边注
如上所示的解决方案并不是最佳的,因为它将在每次重建时实例化BLoC。
后果:
-
您将丢失任何现有的BLoC内容,它会耗费CPU时间,因为它需要在每次构建时实例化它。
-
在这种情况下,更好的方法是使用StatefulWidget从其持久状态中受益,如下所示:
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
MyBloc bloc;
@override
void initState(){
super.initState();
bloc = MyBloc();
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: bloc,
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
使用这种方法,如果“MyTree”小部件需要重建,则不必重新实例化BLoC并直接重用现有实例。
2.3 只对一个widget可访问
这涉及BLoC仅由一个Widget使用的情况。
在这种情况下,可以在Widget中实例化BLoC。
3.事件状态
有时,处理一系列可能是顺序或并行,长或短,同步或异步以及可能导致各种结果的活动可能变得非常难以编程。您可能还需要更新显示以及进度或根据状态。
第一个使用实例旨在使这种情况更容易处理。
该解决方案基于以下原则:
- 发出一个事件;
- 此事件触发一些导致一个或多个状态的动作;
- 这些状态中的每一个都可以反过来发出其他事件或导致另一个状态;
- 然后,这些事件将根据活动状态触发其他操作;
- 等等…
为了说明这个概念,我们来看两个常见的例子:
- 应用初始化
假设您需要运行一系列操作来初始化应用程序。操作可能与服务器的交互相关联(例如,加载一些数据)。在此初始化过程中,您可能需要显示进度条和一系列图像以使用户等待。
- 认证
在启动时,应用程序可能需要用户进行身份验证或注册。用户通过身份验证后,将重定向到应用程序的主页面。然后,如果用户注销,则将其重定向到认证页面。
为了能够处理所有可能的情况以及事件序列,但是如果我们考虑可以在应用程序中的任何地方触发事件,这可能变得非常难以管理。
这正是BlocEventState与BlocEventStateBuilder相结合所能提供帮助的地方。
3.1 BlocEventState
BlocEventState背后的思想是定义一个BLoC:
- 接受事件作为输入;
- 在新事件被发射的时候调用事件处理器;
- 事件处理器负责根据事件采取适当的行动并发出状态作为回应。
下图展示了这个思想:
这是这个类的源代码。解释如下:
import 'package:blocs/bloc_helpers/bloc_provider.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
abstract class BlocEvent extends Object {}
abstract class BlocState extends Object {}
abstract class BlocEventStateBase<BlocEvent, BlocState> implements BlocBase {
PublishSubject<BlocEvent> _eventController = PublishSubject<BlocEvent>();
BehaviorSubject<BlocState> _stateController = BehaviorSubject<BlocState>();
///
/// To be invoked to emit an event
///
Function(BlocEvent) get emitEvent => _eventController.sink.add;
///
/// Current/New state
///
Stream<BlocState> get state => _stateController.stream;
///
/// External processing of the event
///
Stream<BlocState> eventHandler(BlocEvent event, BlocState currentState);
///
/// initialState
///
final BlocState initialState;
//
// Constructor
//
BlocEventStateBase({
@required this.initialState,
}){
//
// For each received event, we invoke the [eventHandler] and
// emit any resulting newState
//
_eventController.listen((BlocEvent event){
BlocState currentState = _stateController.value ?? initialState;
eventHandler(event, currentState).forEach((BlocState newState){
_stateController.sink.add(newState);
});
});
}
@override
void dispose() {
_eventController.close();
_stateController.close();
}
}
如你所见,这是一个需要扩展的抽象类,在扩展类中定义eventHandler方法的行为。
它暴露:
- 一个Sink(emitEvent)来推送一个事件;
- 一个流(状态)来监听发射状态。
在初始化时(请参阅构造函数):
- 需要提供initialState;
- 它创建一个StreamSubscription来监听传入的事件
- 将它们发送到eventHandler
- 发出结果状态。
3.2 具体的BlocEventState
用于实现此类BlocEventState的模板在下面给出。之后,我们将实现一个真正的。
class TemplateEventStateBloc extends BlocEventStateBase<BlocEvent, BlocState> {
TemplateEventStateBloc()
: super(
initialState: BlocState.notInitialized(),
);
@override
Stream<BlocState> eventHandler( BlocEvent event, BlocState currentState) async* {
yield BlocState.notInitialized();
}
}
如果这个模板没有编译通过,请不要担心……这是正常的,因为我们还没有定义BlocState.notInitialized()……这将在几分钟内出现。
此模板仅在初始化时提供initialState并覆盖eventHandler。
这里有一些非常有趣的事情需要注意。我们使用异步生成器:async *和yield声明。
使用async * 修饰符标记函数,将函数标识为异步生成器:
每次调用yield语句时,它都会将yield后面的表达式结果添加到输出Stream。
如果我们需要通过一系列动作发出一系列状态(我们稍后会在实践中看到),这将特别有用
有关异步生成器的其他详细信息,请单击此 链接 。
3.3 BlocEvent和BlocState
正如您所注意到的,我们已经定义了一个BlocEvent和BlocState抽象类。
这些类需要使用您想要发出的具体事件和状态进行扩展。
3.4 BlocEventStateBuilder小部件
这个模式的最后一部分是BlocEventStateBuilder Widget,它允许对BlocEventState发出的State作出响应。
这里是源代码
typedef Widget AsyncBlocEventStateBuilder<BlocState>(BuildContext context, BlocState state);
class BlocEventStateBuilder<BlocEvent,BlocState> extends StatelessWidget {
const BlocEventStateBuilder({
Key key,
@required this.builder,
@required this.bloc,
}): assert(builder != null),
assert(bloc != null),
super(key: key);
final BlocEventStateBase<BlocEvent,BlocState> bloc;
final AsyncBlocEventStateBuilder<BlocState> builder;
@override
Widget build(BuildContext context){
return StreamBuilder<BlocState>(
stream: bloc.state,
initialData: bloc.initialState,
builder: (BuildContext context, AsyncSnapshot<BlocState> snapshot){
return builder(context, snapshot.data);
},
);
}
}
这个Widget不是别的,就是一个具体的StreamBuilder,它会在每次发出新的BlocState时调用构建器输入参数。 ___
好的。现在我们已经拥有了所有的部分,现在是时候展示我们可以用它们做些什么了……
3.5 应用程序初始化
第一个示例说明了您需要应用程序在启动时执行某些任务的情况。
常见的用途是游戏最初显示启动画面(动画与否),同时从服务器获取一些文件,检查新的更新是否可用,尝试连接到任何游戏中心……在显示实际主屏幕之前。为了不给应用程序什么都不做的感觉,它可能会显示一个进度条并定期显示一些图片,同时它会完成所有初始化过程。
我要向你展示的实现非常简单。它只会在屏幕上显示一些完成百分比,但这可以很容易地扩展从而以满足你的需求。
首先要做的是定义事件和状态……
3.5.1 应用初始化事件
在这个例子中,我只会考虑2个事件:
- start:此事件将触发初始化过程;
- stop:该事件可用于强制初始化进程停止
这里是定义:
class ApplicationInitializationEvent extends BlocEvent {
final ApplicationInitializationEventType type;
ApplicationInitializationEvent({
this.type: ApplicationInitializationEventType.start,
}) : assert(type != null);
}
enum ApplicationInitializationEventType {
start,
stop,
}
3.5.2 应用初始化件状态
该类将提供与初始化过程相关的信息。
对于这个例子,我会考虑:
- 2个标志:
- isInitialized指示初始化是否完成
- isInitializing以了解我们是否处于初始化过程的中间
- 进度完成率
这里是源代码:
class ApplicationInitializationState extends BlocState {
ApplicationInitializationState({
@required this.isInitialized,
this.isInitializing: false,
this.progress: 0,
});
final bool isInitialized;
final bool isInitializing;
final int progress;
factory ApplicationInitializationState.notInitialized() {
return ApplicationInitializationState(
isInitialized: false,
);
}
factory ApplicationInitializationState.progressing(int progress) {
return ApplicationInitializationState(
isInitialized: progress == 100,
isInitializing: true,
progress: progress,
);
}
factory ApplicationInitializationState.initialized() {
return ApplicationInitializationState(
isInitialized: true,
progress: 100,
);
}
}
3.5.3 应用初始化Bloc
该BLoC负责基于事件处理初始化过程。
这里是代码:
class ApplicationInitializationBloc
extends BlocEventStateBase<ApplicationInitializationEvent, ApplicationInitializationState> {
ApplicationInitializationBloc()
: super(
initialState: ApplicationInitializationState.notInitialized(),
);
@override
Stream<ApplicationInitializationState> eventHandler(
ApplicationInitializationEvent event, ApplicationInitializationState currentState) async* {
if (!currentState.isInitialized){
yield ApplicationInitializationState.notInitialized();
}
if (event.type == ApplicationInitializationEventType.start) {
for (int progress = 0; progress < 101; progress += 10){
await Future.delayed(const Duration(milliseconds: 300));
yield ApplicationInitializationState.progressing(progress);
}
}
if (event.type == ApplicationInitializationEventType.stop){
yield ApplicationInitializationState.initialized();
}
}
}
一些解释:
- 当收到事件“ApplicationInitializationEventType.start”时,它从0到100开始计数(步骤10),并且对于每个值(0,10,20,……),它发出(通过yield)一个告诉的新状态初始化正在运行(isInitializing = true)及其进度值。
- 当收到事件“ApplicationInitializationEventType.stop”时,它认为初始化已完成。
- 正如你所看到的,我在计数器循环中放了一些延迟。这将向您展示如何使用任何Future(例如,您需要联系服务器的情况
3.5.4 将它们全部包装在一起
现在,剩下的部分是显示显示计数器的伪Splash屏幕……
class InitializationPage extends StatefulWidget {
@override
_InitializationPageState createState() => _InitializationPageState();
}
class _InitializationPageState extends State<InitializationPage> {
ApplicationInitializationBloc bloc;
@override
void initState(){
super.initState();
bloc = ApplicationInitializationBloc();
bloc.emitEvent(ApplicationInitializationEvent());
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext pageContext) {
return SafeArea(
child: Scaffold(
body: Container(
child: Center(
child: BlocEventStateBuilder<ApplicationInitializationEvent, ApplicationInitializationState>(
bloc: bloc,
builder: (BuildContext context, ApplicationInitializationState state){
if (state.isInitialized){
//
// Once the initialization is complete, let's move to another page
//
WidgetsBinding.instance.addPostFrameCallback((_){
Navigator.of(context).pushReplacementNamed('/home');
});
}
return Text('Initialization in progress... ${state.progress}%');
},
),
),
),
),
);
}
}
说明:
- 由于ApplicationInitializationBloc不需要在应用程序的任何地方使用,我们可以在StatefulWidget中初始化它;
- 我们直接发出ApplicationInitializationEventType.start事件来触发eventHandler
- 每次发出ApplicationInitializationState时,我们都会更新文本
- 初始化完成后,我们将用户重定向到主页。
技巧 由于我们无法直接重定向到主页,在构建器内部,我们使用WidgetsBinding.instance.addPostFrameCallback()方法请求Flutter在渲染完成后立即执行方法
3.6 例2:应用程序身份验证和注销
对于此示例,我将考虑以下用例:
- 在启动时,如果用户未经过身份验证,则会自动显示“身份验证/注册”页面; 在用户认证期间,显示CircularProgressIndicator;
- 经过身份验证后,用户将被重定向到主页;
- 在应用程序的任何地方,用户都可以注销;
- 当用户注销时,用户将自动重定向到“身份验证”页面。
当然,很有可能以编程方式处理所有这些,但将所有这些委托给BLoC要容易得多。
下图解释了我要解释的解决方案
名为“DecisionPage”的中间页面将负责将用户自动重定向到“身份验证”页面或主页,具体取决于用户身份验证的状态。当然,此DecisionPage从不显示,也不应被视为页面。
首先要做的是定义事件和状态……
3.6.1. AuthenticationEvent
在这个例子中,我只考虑2个事件:
- login:当用户正确验证时发出此事件;
- logout:用户注销时发出的事件。
这里是定义:
bstract class AuthenticationEvent extends BlocEvent {
final String name;
AuthenticationEvent({
this.name: '',
});
}
class AuthenticationEventLogin extends AuthenticationEvent {
AuthenticationEventLogin({
String name,
}) : super(
name: name,
);
}
class AuthenticationEventLogout extends AuthenticationEvent {}
3.6.2 AuthenticationState
该类将提供与身份验证过程相关的信息。
对于这个例子,我会考虑:
- 3个标记:
- isAuthenticated指示身份验证是否完整
- isAuthenticating以了解我们是否处于身份验证过程的中间
- hasFailed表示身份验证失败
- 经过身份验证的用户名
这里是源代码:
class AuthenticationState extends BlocState {
AuthenticationState({
@required this.isAuthenticated,
this.isAuthenticating: false,
this.hasFailed: false,
this.name: '',
});
final bool isAuthenticated;
final bool isAuthenticating;
final bool hasFailed;
final String name;
factory AuthenticationState.notAuthenticated() {
return AuthenticationState(
isAuthenticated: false,
);
}
factory AuthenticationState.authenticated(String name) {
return AuthenticationState(
isAuthenticated: true,
name: name,
);
}
factory AuthenticationState.authenticating() {
return AuthenticationState(
isAuthenticated: false,
isAuthenticating: true,
);
}
factory AuthenticationState.failure() {
return AuthenticationState(
isAuthenticated: false,
hasFailed: true,
);
}
}
3.5.3 AuthenticationBloc
此BLoC负责根据事件处理身份验证过程。
这是代码:
class AuthenticationBloc
extends BlocEventStateBase<AuthenticationEvent, AuthenticationState> {
AuthenticationBloc()
: super(
initialState: AuthenticationState.notAuthenticated(),
);
@override
Stream<AuthenticationState> eventHandler(
AuthenticationEvent event, AuthenticationState currentState) async* {
if (event is AuthenticationEventLogin) {
// Inform that we are proceeding with the authentication
yield AuthenticationState.authenticating();
// Simulate a call to the authentication server
await Future.delayed(const Duration(seconds: 2));
// Inform that we have successfuly authenticated, or not
if (event.name == "failure"){
yield AuthenticationState.failure();
} else {
yield AuthenticationState.authenticated(event.name);
}
}
if (event is AuthenticationEventLogout){
yield AuthenticationState.notAuthenticated();
}
}
}
一些解释:
- 当收到事件“AuthenticationEventLogin”时,它会(通过yield)发出一个新状态,告知身份验证正在运行(isAuthenticating = true)。
- 然后它运行身份验证,一旦完成,就会发出另一个状态,告知身份验证已完成。
- 当收到事件“AuthenticationEventLogout”时,它将发出一个新状态,告诉用户不再进行身份验证。
3.5.4. AuthenticationPage
正如您将要看到的那样,为了便于解释,此页面非常基本且不会做太多。
这是代码。解释如下:
class AuthenticationPage extends StatelessWidget {
///
/// Prevents the use of the "back" button
///
Future<bool> _onWillPopScope() async {
return false;
}
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return WillPopScope(
onWillPop: _onWillPopScope,
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Authentication Page'),
leading: Container(),
),
body: Container(
child:
BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
bloc: bloc,
builder: (BuildContext context, AuthenticationState state) {
if (state.isAuthenticating) {
return PendingAction();
}
if (state.isAuthenticated){
return Container();
}
List<Widget> children = <Widget>[];
// Button to fake the authentication (success)
children.add(
ListTile(
title: RaisedButton(
child: Text('Log in (success)'),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogin(name: 'Didier'));
},
),
),
);
// Button to fake the authentication (failure)
children.add(
ListTile(
title: RaisedButton(
child: Text('Log in (failure)'),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogin(name: 'failure'));
},
),
),
);
// Display a text if the authentication failed
if (state.hasFailed){
children.add(
Text('Authentication failure!'),
);
}
return Column(
children: children,
);
},
),
),
),
),
);
}
}
说明:
- 第11行:页面检索对AuthenticationBloc的引用
- 第24-70行:它监听发出的AuthenticationState:
- 如果身份验证正在进行中,它会显示一个CircularProgressIndicator,告诉用户正在进行某些操作并阻止用户访问该页面(第25-27行)
- 如果验证成功,我们不需要显示任何内容(第29-31行)。
- 如果用户未经过身份验证,则会显示2个按钮以模拟成功的身份验证和失败。
- 当我们点击其中一个按钮时,我们发出一个AuthenticationEventLogin事件,以及一些参数(通常由认证过程使用)
- 如果验证失败,我们会显示错误消息(第60-64行)
仅此而已!没有别的事情需要做……是不是很容易?
注意
您可能已经注意到,我将页面包装在WillPopScope中。
理由是我不希望用户能够使用Android’后退’按钮,如此示例中所示,身份验证是一个必须的步骤,它阻止用户访问任何其他部分,除非经过正确的身份验证。
3.5.5 DecisionPage
如前所述,我希望应用程序根据身份验证状态自动重定向到AuthenticationPage或HomePage。
以下是此DecisionPage的代码,说明如下:
class DecisionPage extends StatefulWidget {
@override
DecisionPageState createState() {
return new DecisionPageState();
}
}
class DecisionPageState extends State<DecisionPage> {
AuthenticationState oldAuthenticationState;
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
bloc: bloc,
builder: (BuildContext context, AuthenticationState state) {
if (state != oldAuthenticationState){
oldAuthenticationState = state;
if (state.isAuthenticated){
_redirectToPage(context, HomePage());
} else if (state.isAuthenticating || state.hasFailed){
//do nothing
} else {
_redirectToPage(context, AuthenticationPage());
}
}
// This page does not need to display anything since it will
// always remind behind any active page (and thus 'hidden').
return Container();
}
);
}
void _redirectToPage(BuildContext context, Widget page){
WidgetsBinding.instance.addPostFrameCallback((_){
MaterialPageRoute newRoute = MaterialPageRoute(
builder: (BuildContext context) => page
);
Navigator.of(context).pushAndRemoveUntil(newRoute, ModalRoute.withName('/decision'));
});
}
}
提醒
为了详细解释这一点,我们需要回到Flutter处理Pages(= Route)的方式。要处理路由,我们使用导航器,它创建一个叠加层。
这个Overlay是一个OverlayEntry堆栈,每个都包含一个Page。
当我们通过Navigator.of(上下文)推送,弹出,替换页面时,后者更新其重建的覆盖(因此堆栈)。
重建堆栈时,还会重建每个OverlayEntry(因此其内容)。
因此,当我们通过Navigator.of(上下文)进行操作时,所有剩余的页面都会重建!
那么,为什么我将它实现为StatefulWidget?
为了能够响应AuthenticationState的任何更改,此“页面”需要在应用程序的整个生命周期中保持存在。
这意味着,根据上面的提醒,每次Navigator.of(上下文)完成操作时,都会重建此页面。
因此,它的BlocEventStateBuilder也将重建,调用自己的构建器方法。
因为此构建器负责将用户重定向到与AuthenticationState对应的页面,所以如果我们每次重建页面时重定向用户,因为不断重建,它将继续重定向。
为了防止这种情况发生,我们只需要记住我们采取行动的最后一个AuthenticationState,并且只在收到另一个AuthenticationState时采取另一个动作。
这是如何运作的?
如上所述,每次发出AuthenticationState时,BlocEventStateBuilder都会调用其构建器。
基于状态标志(isAuthenticated),我们知道我们需要向哪个页面重定向用户。
小技巧
由于我们无法直接从构建器重定向到另一个页面,因此我们使用WidgetsBinding.instance.addPostFrameCallback()方法在呈现完成后请求Flutter执行方法
此外,由于我们需要在重定向用户之前删除任何现有页面,除了需要保留在所有情况下的此DecisionPage之外,我们使用Navigator.of(context).pushAndRemoveUntil(…)来实现此目的。
3.5.8 注销
要让用户注销,您现在可以创建一个“LogOutButton”并将其放在应用程序的任何位置。
此按钮只需要发出AuthenticationEventLogout()事件,这将导致以下自动操作链:
- 它将由AuthenticationBloc处理
- 反过来会发出一个AuthentiationState(isAuthenticated = false)
- 这将由DecisionPage通过BlocEventStateBuilder处理
- 这会将用户重定向到AuthenticationPage
这是这样的按钮的代码:
class LogOutButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogout());
},
);
}
}
3.5.7 AuthenticationBloc
由于AuthenticationBloc需要可用于此应用程序的任何页面,我们还会将其作为MaterialApp的父级注入,如下所示:
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DecisionPage(),
),
);
}
}
4. 表单验证
BLoC的另一个有趣用途是当您需要验证表单时:
- 根据某些业务规则验证与TextField相关的条目;
- 根据规则显示验证错误消息;
- 根据业务规则自动化窗口小部件的可访问性。
我现在要做的一个例子是RegistrationForm,它由3个TextFields(电子邮件,密码,确认密码)和1个RaisedButton组成,以启动注册过程。
我想要实现的业务规则是:
- 电子邮件需要是有效的电子邮件地址。如果不是,则需要显示消息。
- 密码必须有效(必须包含至少8个字符,1个大写,1个小写,1个数字和1个特殊字符)。如果无效,则需要显示消息。
- 重新输入密码需要符合相同的验证规则并且与密码相同。如果不相同,则需要显示消息。
- 注册按钮只有在所有规则有效时才有效。
4.1。 RegistrationFormBloc
该BLoC负责处理验证业务规则,如前所述。
class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase {
final BehaviorSubject<String> _emailController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordConfirmController = BehaviorSubject<String>();
//
// Inputs
//
Function(String) get onEmailChanged => _emailController.sink.add;
Function(String) get onPasswordChanged => _passwordController.sink.add;
Function(String) get onRetypePasswordChanged => _passwordConfirmController.sink.add;
//
// Validators
//
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password => _passwordController.stream.transform(validatePassword);
Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
.doOnData((String c){
// If the password is accepted (after validation of the rules)
// we need to ensure both password and retyped password match
if (0 != _passwordController.value.compareTo(c)){
// If they do not match, add an error
_passwordConfirmController.addError("No Match");
}
});
//
// Registration button
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => true
);
@override
void dispose() {
_emailController?.close();
_passwordController?.close();
_passwordConfirmController?.close();
}
}
我详细解释一下……
- 我们首先初始化3个BehaviorSubject来处理表单的每个TextField的Streams。
- 我们公开了3个Function(String),它将用于接受来自TextFields的输入。
- 我们公开了3个Stream
,TextField将使用它来显示由它们各自的验证产生的潜在错误消息 -
我们公开了1个Stream
,它将被RaisedButton使用,以根据整个验证结果启用/禁用它。 好的,现在是时候深入了解更多细节...... - 您可能已经注意到,此类的签名有点特殊。我们来回顾一下吧。
class RegistrationFormBloc extends Object
with EmailValidator, PasswordValidator
implements BlocBase {
...
}
with关键字表示此类正在使用MIXINS(=“在另一个类中重用某些类代码的方法”),并且为了能够使用with关键字,该类需要扩展Object类。这些mixin包含分别验证电子邮件和密码的代码。
有关Mixins的更多详细信息,我建议您阅读 Romain Rastel的这篇精彩文章 。
4.1.1 Validator Mixins
我只会解释EmailValidator,因为PasswordValidator非常相似
首先,是代码
const String _kEmailRule = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";
class EmailValidator {
final StreamTransformer<String,String> validateEmail =
StreamTransformer<String,String>.fromHandlers(handleData: (email, sink){
final RegExp emailExp = new RegExp(_kEmailRule);
if (!emailExp.hasMatch(email) || email.isEmpty){
sink.addError('Entre a valid email');
} else {
sink.add(email);
}
});
}
该类公开了一个最终函数(“validateEmail”),它是一个StreamTransformer。
提醒
StreamTransformer的调用方式如下:stream.transform(StreamTransformer)。
StreamTransformer通过transform方法从Stream引用它的输入。然后它处理此输入,并将转换后的输入重新注入初始Stream。
在此代码中,输入的处理包括根据正则表达式进行检查。如果输入与正则表达式匹配,我们只需将输入重新注入流中,否则,我们会向流中注入错误消息。
4.1.2 为什么使用stream.tansform()?
如前所述,如果验证成功,StreamTransformer会将输入重新注入Stream。为什么有用?
以下是与Observable.combineLatest3()相关的解释…此方法在它引用的所有Streams之前不会发出任何值,至少发出一个值。
让我们看看下面的图片来说明我们想要实现的目标。
- 如果用户输入电子邮件并且后者经过验证,它将由电子邮件流发出,该电子邮件流将是Observable.combineLatest3()的一个输入;
- 如果电子邮件无效,则会向流中添加错误(并且流中没有值); 这同样适用于密码和重新输入密码;
- 当所有这三个验证都成功时(意味着所有这三个流都会发出一个值),Observable.combineLatest3()将依次发出一个真正的感谢“(e,p,c)=> true”(见第35行)。
4.1.2。验证2个密码
我在互联网上看到了很多与这种比较有关的问题。存在几种解决方案,让我解释其中的两种。
4.1.2.1。基本解决方案 - 没有错误消息
第一个解决方案可能是以下一个:
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => (0 == p.compareTo(c))
);
这个解决方案只需验证两个密码,如果它们匹配,就会发出一个值(= true)。
我们很快就会看到,Register按钮的可访问性将取决于registerValid流。
如果两个密码不匹配,则该流不会发出任何值,并且“注册”按钮保持不活动状态,但用户不会收到任何错误消息以帮助他理解原因。
4.1.2.2。带错误消息的解决方案
另一种解决方案包括扩展confirmPassword流的处理,如下所示:
Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
.doOnData((String c){
// If the password is accepted (after validation of the rules)
// we need to ensure both password and retyped password match
if (0 != _passwordController.value.compareTo(c)){
// If they do not match, add an error
_passwordConfirmController.addError("No Match");
}
});
一旦验证了重新输入密码,它就会被Stream发出,并且使用doOnData,我们可以直接获取此发出的值并将其与密码流的值进行比较。如果两者不匹配,我们现在可以发送错误消息。
4.2 注册表单
现在让我们先解释一下RegistrationForm:
class RegistrationForm extends StatefulWidget {
@override
_RegistrationFormState createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
RegistrationFormBloc _registrationFormBloc;
@override
void initState() {
super.initState();
_registrationFormBloc = RegistrationFormBloc();
}
@override
void dispose() {
_registrationFormBloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
StreamBuilder<String>(
stream: _registrationFormBloc.email,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'email',
errorText: snapshot.error,
),
onChanged: _registrationFormBloc.onEmailChanged,
keyboardType: TextInputType.emailAddress,
);
}),
StreamBuilder<String>(
stream: _registrationFormBloc.password,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'password',
errorText: snapshot.error,
),
obscureText: false,
onChanged: _registrationFormBloc.onPasswordChanged,
);
}),
StreamBuilder<String>(
stream: _registrationFormBloc.confirmPassword,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'retype password',
errorText: snapshot.error,
),
obscureText: false,
onChanged: _registrationFormBloc.onRetypePasswordChanged,
);
}),
StreamBuilder<bool>(
stream: _registrationFormBloc.registerValid,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return RaisedButton(
child: Text('Register'),
onPressed: (snapshot.hasData && snapshot.data == true)
? () {
// launch the registration process
}
: null,
);
}),
],
),
);
}
}
说明:
- 由于RegisterFormBloc仅供此表单使用,因此适合在此处初始化它。
- 每个TextField都包装在StreamBuilder
中,以便能够响应验证过程的任何结果(请参阅errorText:snapshot.error) - 每次对TextField的内容进行修改时,我们都会通过onChanged发送输入到BLoC进行验证:_registrationFormBloc.onEmailChanged(电子邮件输入的情况)
- 对于RegisterButton,后者也包含在StreamBuilder
中。 - 如果_registrationFormBloc.registerValid发出一个值,onPressed方法将执行某些操作
- 如果未发出任何值,则onPressed方法将被指定为null,这将取消激活该按钮。
仅此而已!表单中没有任何业务规则,这意味着可以更改规则而无需对表单进行任何修改,这非常好!
5.Part of
有时,Widget知道它是否是驱动其行为的集合的一部分是有趣的。
对于本文的最后一个实际例子,我将考虑以下场景:
- 应用程序处理项目;
- 用户可以选择放入购物篮的物品;
- 一件商品只能放入购物篮一次;
- 存放在购物篮中的物品可以从购物篮中取出;
- 一旦被移除,就可以将其取回。
对于此示例,每个项目将显示一个按钮,该按钮将取决于购物篮中物品的存在。如果不是购物篮的一部分,该按钮将允许用户将其添加到购物篮中。如果是购物篮的一部分,该按钮将允许用户将其从篮子中取出。
为了更好地说明“Part of”模式,我将考虑以下架构:
- 购物页面将显示所有可能的项目列表;
- 购物页面中的每个商品都会显示一个按钮,用于将商品添加到购物篮或将其移除,具体取决于其在购物篮中的位置;
- 如果购物页面中的商品被添加到购物篮中,其按钮将自动更新以允许用户将其从购物篮中移除(反之亦然),而无需重建购物页面
- 另一页,购物篮,将列出篮子里的所有物品;
- 可以从此页面中删除购物篮中的任何商品。
边注 Part Of这个名字是我给的个人名字。这不是官方名称。
5.1 ShoppingBloc
正如您现在可以想象的那样,我们需要考虑一个专门用于处理所有可能项目列表的BLoC,以及购物篮的一部分。
这个BLoC可能如下所示:
class ShoppingBloc implements BlocBase {
// List of all items, part of the shopping basket
Set<ShoppingItem> _shoppingBasket = Set<ShoppingItem>();
// Stream to list of all possible items
BehaviorSubject<List<ShoppingItem>> _itemsController = BehaviorSubject<List<ShoppingItem>>();
Stream<List<ShoppingItem>> get items => _itemsController;
// Stream to list the items part of the shopping basket
BehaviorSubject<List<ShoppingItem>> _shoppingBasketController = BehaviorSubject<List<ShoppingItem>>(seedValue: <ShoppingItem>[]);
Stream<List<ShoppingItem>> get shoppingBasket => _shoppingBasketController;
@override
void dispose() {
_itemsController?.close();
_shoppingBasketController?.close();
}
// Constructor
ShoppingBloc() {
_loadShoppingItems();
}
void addToShoppingBasket(ShoppingItem item){
_shoppingBasket.add(item);
_postActionOnBasket();
}
void removeFromShoppingBasket(ShoppingItem item){
_shoppingBasket.remove(item);
_postActionOnBasket();
}
void _postActionOnBasket(){
// Feed the shopping basket stream with the new content
_shoppingBasketController.sink.add(_shoppingBasket.toList());
// any additional processing such as
// computation of the total price of the basket
// number of items, part of the basket...
}
//
// Generates a series of Shopping Items
// Normally this should come from a call to the server
// but for this sample, we simply simulate
//
void _loadShoppingItems() {
_itemsController.sink.add(List<ShoppingItem>.generate(50, (int index) {
return ShoppingItem(
id: index,
title: "Item $index",
price: ((Random().nextDouble() * 40.0 + 10.0) * 100.0).roundToDouble() /
100.0,
color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0)
.withOpacity(1.0),
);
}));
}
}
唯一可能需要解释的方法是_postActionOnBasket()方法。每次在篮子中添加或删除项目时,我们都需要“刷新”_shoppingBasketController Stream的内容,以便通知所有正在监听此Stream更改的Widgets并能够刷新/重建。
5.2 ShoppingPage
此页面非常简单,只显示所有项目。
class ShoppingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
ShoppingBloc bloc = BlocProvider.of<ShoppingBloc>(context);
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Shopping Page'),
actions: <Widget>[
ShoppingBasket(),
],
),
body: Container(
child: StreamBuilder<List<ShoppingItem>>(
stream: bloc.items,
builder: (BuildContext context,
AsyncSnapshot<List<ShoppingItem>> snapshot) {
if (!snapshot.hasData) {
return Container();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ShoppingItemWidget(
shoppingItem: snapshot.data[index],
);
},
);
},
),
),
));
}
}
说明:
- AppBar显示一个按钮:
- 显示出现在购物篮中的商品数量
- 单击时将用户重定向到ShoppingBasket页面
- 项目列表使用GridView构建,包含在StreamBuilder <List
>中 - 每个项目对应一个ShoppingItemWidget
5.3 ShoppingBasketPage
此页面与ShoppingPage非常相似,只是StreamBuilder现在正在侦听由ShoppingBloc公开的_shoppingBasket流的变体。
5.4 ShoppingItemWidget和ShoppingItemBloc
Part模式的依赖于这两个元素的组合:
- ShoppingItemWidget负责:
- 显示项目和
- 用于在购物篮中添加项目或从中取出项目的按钮
- ShoppingItemBloc负责告诉ShoppingItemWidget后者是否属于购物篮的一部分。 让我们看看他们如何一起工作……
5.4.1 ShoppingItemBloc
ShoppingItemBloc由每个ShoppingItemWidget实例化,赋予它“身份”。
此BLoC侦听ShoppingBasket流的所有变体,并检查特定项目标识是否是篮子的一部分。
如果是,它会发出一个布尔值(= true),它将被ShoppingItemWidget捕获,以确定它是否是篮子的一部分。
这里是BLoc的代码
class ShoppingItemBloc implements BlocBase {
// Stream to notify if the ShoppingItemWidget is part of the shopping basket
BehaviorSubject<bool> _isInShoppingBasketController = BehaviorSubject<bool>();
Stream<bool> get isInShoppingBasket => _isInShoppingBasketController;
// Stream that receives the list of all items, part of the shopping basket
PublishSubject<List<ShoppingItem>> _shoppingBasketController = PublishSubject<List<ShoppingItem>>();
Function(List<ShoppingItem>) get shoppingBasket => _shoppingBasketController.sink.add;
// Constructor with the 'identity' of the shoppingItem
ShoppingItemBloc(ShoppingItem shoppingItem){
// Each time a variation of the content of the shopping basket
_shoppingBasketController.stream
// we check if this shoppingItem is part of the shopping basket
.map((list) => list.any((ShoppingItem item) => item.id == shoppingItem.id))
// if it is part
.listen((isInShoppingBasket)
// we notify the ShoppingItemWidget
=> _isInShoppingBasketController.add(isInShoppingBasket));
}
@override
void dispose() {
_isInShoppingBasketController?.close();
_shoppingBasketController?.close();
}
}
5.4.2 ShoppingItemWidget
此Widget负责:
- 创建ShoppingItemBloc的实例并将其自己的标识传递给BLoC
- 聆听ShoppingBasket内容的任何变化并将其转移到BLoC
- 听ShoppingItemBloc知道它是否是篮子的一部分
- 显示相应的按钮(添加/删除),具体取决于它在篮子中的存在
- 响应按钮的用户操作
- 当用户点击添加按钮时,将自己添加到购物篮中
- 当用户点击删除按钮时,将自己从篮子中移除。
让我们看看它是如何工作的(解释在代码中给出)。
class ShoppingItemWidget extends StatefulWidget {
ShoppingItemWidget({
Key key,
@required this.shoppingItem,
}) : super(key: key);
final ShoppingItem shoppingItem;
@override
_ShoppingItemWidgetState createState() => _ShoppingItemWidgetState();
}
class _ShoppingItemWidgetState extends State<ShoppingItemWidget> {
StreamSubscription _subscription;
ShoppingItemBloc _bloc;
ShoppingBloc _shoppingBloc;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// As the context should not be used in the "initState()" method,
// prefer using the "didChangeDependencies()" when you need
// to refer to the context at initialization time
_initBloc();
}
@override
void didUpdateWidget(ShoppingItemWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// as Flutter might decide to reorganize the Widgets tree
// it is preferable to recreate the links
_disposeBloc();
_initBloc();
}
@override
void dispose() {
_disposeBloc();
super.dispose();
}
// This routine is reponsible for creating the links
void _initBloc() {
// Create an instance of the ShoppingItemBloc
_bloc = ShoppingItemBloc(widget.shoppingItem);
// Retrieve the BLoC that handles the Shopping Basket content
_shoppingBloc = BlocProvider.of<ShoppingBloc>(context);
// Simple pipe that transfers the content of the shopping
// basket to the ShoppingItemBloc
_subscription = _shoppingBloc.shoppingBasket.listen(_bloc.shoppingBasket);
}
void _disposeBloc() {
_subscription?.cancel();
_bloc?.dispose();
}
Widget _buildButton() {
return StreamBuilder<bool>(
stream: _bloc.isInShoppingBasket,
initialData: false,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return snapshot.data
? _buildRemoveFromShoppingBasket()
: _buildAddToShoppingBasket();
},
);
}
Widget _buildAddToShoppingBasket(){
return RaisedButton(
child: Text('Add...'),
onPressed: (){
_shoppingBloc.addToShoppingBasket(widget.shoppingItem);
},
);
}
Widget _buildRemoveFromShoppingBasket(){
return RaisedButton(
child: Text('Remove...'),
onPressed: (){
_shoppingBloc.removeFromShoppingBasket(widget.shoppingItem);
},
);
}
@override
Widget build(BuildContext context) {
return Card(
child: GridTile(
header: Center(
child: Text(widget.shoppingItem.title),
),
footer: Center(
child: Text('${widget.shoppingItem.price} €'),
),
child: Container(
color: widget.shoppingItem.color,
child: Center(
child: _buildButton(),
),
),
),
);
}
}
5.5 它是如何工作的?
下图显示了所有部分是如何协同工作。
结论
又是一篇长章,我希望我将其变得简略一点,但我一些解释是必要的。
正如我在介绍中所说,我个人在我的开发中经常使用这些“模式”。这让我节省了大量的时间和精力;我的代码更易读,更容易调试。
此外,它有助于将业务与视图分离。
大概率肯定有其他方法可以做到这一点,甚至做得更好。但它对我有用的,这就是我想与你分享的一切。
请继续关注新文章,同时祝您编程愉快。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- [ Laravel 5.7 文档 ] 基础组件 —— HTTP 响应
- [ Laravel 5.8 文档 ] 基础组件 —— HTTP 响应
- Binder+Handler,看组件生命周期如何被响应
- 响应式文件浏览管理器angular-filemanager【组件】
- DevExtreme 18.2 发布,响应式 Web 开发 JS 组件
- Android官方架构组件Paging-Ex:列表状态的响应式管理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming in Haskell
Graham Hutton / Cambridge University Press / 2007-1-18 / GBP 34.99
Haskell is one of the leading languages for teaching functional programming, enabling students to write simpler and cleaner code, and to learn how to structure and reason about programs. This introduc......一起来看看 《Programming in Haskell》 这本书的介绍吧!