css 颤动
While it can be an overlooked thing in a small project (which I don’t think it should either), when it comes to a medium/large project it’s necessary to take in account the fact that we need a clean, modular, testable and robust architecture for our app. If it wasn’t so, a lot of headaches will come on the long run, when trying to figure out some bug, implementing new features, changing existing ones and performing a lot of more “mutations” our app might go into.
尽管在小型项目中这可能是一件被忽略的事情(我也不认为应该这样做),但在大中型项目中,有必要考虑以下事实:我们需要一个干净,模块化,可测试且我们应用程序的强大架构。 如果不是这样,从长远来看,当试图找出一些错误,实现新功能,更改现有功能以及执行我们的应用程序可能会进行的更多“变异”时,就会令人头疼。
The goal of a design pattern is to provide a clean standard for how our project will have its files organised, how the components will interact with each other, separating layers so that a change into one is transparent to the others, and, mostly, promoting the reuse of blocks of code.
设计模式的目标是为项目的组织方式,组件之间的交互方式,分层之间的分隔提供清晰的标准,从而使更改成一个透明的形式对其他组件透明,并且主要是促进代码块的重用。
There are a lot of design patterns that people use for Flutter. Some of the names you might’ve stepped on are Redux, Provider, InheritedWidget, MobX, MVC, MVP. They are all different (sometimes not so different) ways to manage the state of an app, deciding how it mutates according to the interaction with the user and/or with other environment’s agents.
人们为Flutter使用了许多设计模式 。 您可能要踩到的一些名称是Redux,Provider,InheritedWidget,MobX,MVC,MVP。 它们都是用于管理应用程序状态的不同方法(有时相差不大),根据与用户和/或与其他环境的代理的交互来决定应用程序的状态。
快速绕道:提升状态的重要性 (Quick detour: the importance of lifting the state up)
Suppose you have a simple application, suppose that’s the counter app that you get when creating a Flutter project (assuming you’ve not fiddled with the default template).
假设您有一个简单的应用程序,假设这是您在创建Flutter项目时获得的计数器应用程序(假设您还没有摆弄默认模板 )。
Keeping that idea in mind and complicating it a bit, that app can become something like this:
牢记这个主意并将其复杂化,该应用可以变成这样:
Then, suppose you want to show the counter in a different way, for example you want the text coloured in red (and bold, italic, underlined, upside down, you name it). What do you do?
然后,假设您想以不同的方式显示计数器,例如,您想要用红色上色的文本(您可以将其命名为粗体,斜体,下划线,上下颠倒)。 你是做什么?
You might think to create a FancyCounterTextWidget, pass the value and voilà.
您可能会想创建一个FancyCounterTextWidget ,并传递值和样式。
You’re going to have something like this:
您将具有以下内容:
Yay. Well, what happens if your CounterText and FancyCounterText widgets need to be moved somewhere else, let’s say in another widget? You’re going to need to inject the counter passing it to the constructor, and so on. And what if you need to move/copy that widget to another part of the app? This will result in an injection nightmare!
好极了。 好吧,如果您的CounterText和FancyCounterText小部件需要移动到其他位置(例如在另一个小部件中),会发生什么? 您将需要注入将counter传递给构造函数的counter ,依此类推。 如果您需要将该小部件移动/复制到应用程序的另一部分,该怎么办? 这将导致注射噩梦!
This is why Flutter provides us with InheritedWidget which allows us to lift these dependencies above the widget subtree and provides access to those dependencies to every widget below the subtree. This is what the Provider package does in a fancy way.
这就是Flutter为我们提供InheritedWidget ,它使我们能够将这些依赖关系提升到小部件子树上方,并提供对子树下每个小部件的依赖关系的访问。 这是Provider包的一种奇特方式。
This concept is what we need to explain what a BlocProvider is later in this article. We’re not going to dig about how these are implemented since it’s out of the scope of this article.
这是个什么概念,我们需要解释什么BlocProvider是后来这篇文章。 由于本文不讨论这些实现方式,因此我们将不进行探讨。
BLoC模式 (The BLoC pattern)
So what is this? BLoC stands for Business Logic Component, and a BLoC is essentially a class which keeps the state of our app/feature/screen/widget, mutates it upon receiving input events and notifying its change.
那是什么 BLOC代表乙 usiness 罗 GICÇomponent和BLOC基本上是其保持在接收到输入事件,并通知它的变化我们的应用程序/特征/屏幕/窗口小部件,它变异的状态的类。
Using a BLoC to manage the state of the previous screen, our widget tree will look like this:
使用BLoC管理前一个屏幕的状态,我们的小部件树将如下所示:
The dashed lines indicate that while those widgets are not direct children of the BlocProvider they are its descendants and have access to the CounterBloc it provides.
虚线表示虽然这些小部件不是BlocProvider直接子级,但它们是其后代,并且可以访问它提供的CounterBloc 。
The CounterBloc will hold and notify a state of type int and will manage an IncrementCounterEvent .
CounterBloc将保存并通知一个int类型的状态,并将管理一个IncrementCounterEvent 。
So now when the Button is tapped, an IncrementCounterEvent will be dispatched to the CounterBloc who will increment its counter and notify the state change to whoever is listening:
因此,现在在点击Button时,一个IncrementCounterEvent将被分派给CounterBloc , CounterBloc将增加其计数器并将状态更改通知给正在侦听的人:
This approach has many benefits:
这种方法有很多好处:
Once the Button has dispatched the event it won’t be necessary for the UI to know what happens next. It’s BLoC business. Moreover, CounterText and FancyCounterText do not care if the increment came from the press of a button or an accelerometer spike or a solar eclipse, and this means we can easily mock states in order to test those components.
Button调度事件后,UI不必知道接下来会发生什么。 这是BLoC业务。 而且, CounterText和FancyCounterText不在乎增量是否来自按下按钮,加速计尖峰或Eclipse,这意味着我们可以轻松模拟状态以测试这些组件。
That’s it. This is the BLoC pattern core logic in a nutshell. Now that we’ve introduced it, let’s see an example that is surely more practical and realistic than a dummy counter app.
而已。 简而言之,这就是BLoC模式的核心逻辑。 现在我们已经介绍了它,让我们看一个比虚拟计数器应用程序肯定更实际和现实的示例。
一个实际的例子:意大利的电晕 (A practical example: Corona Italy)
I’ve been a BLoC user for quite some time now, but before writing this article I wanted to try implementing it with the flutter_bloc package. Using it avoids some boilerplate and helps you implementing the BLoC pattern without necessarily knowing all of its ins and outs right away.
我已经很长时间以来一直是BLoC用户,但是在写这篇文章之前,我想尝试使用flutter_bloc包实现它。 使用它可以避免一些重复的步骤,并可以帮助您实现BLoC模式,而不必立即了解其所有来龙去脉。
The project is open and you can find it on GitHub. It’s a simple front end (with a very poor graphic for now) to display the daily Covid-19 infections in Italy using the government open data.
该项目已打开,您可以在GitHub上找到它。 这是一个简单的前端(目前图形非常差),可以使用政府的公开数据显示意大利每天发生的Covid-19感染情况。
It consists of:
它包括:
- a home screen which has a map and a bottom sheet containing the national highlights and the list of regions 主屏幕,其中包含地图和底页,其中包含国家重点摘要和地区列表
- a screen which shows the full report for the national data 屏幕显示国家数据的完整报告
- a screen which shows the full report for a region and a list of its provinces along the increment of cases for each one of them 屏幕显示一个区域的完整报告及其省份列表,以及每个省份的案件增量
So, we have 3 types of information:
因此,我们有3种类型的信息:
- National report 国家报告
- Regional report 区域报告
- Provincial report 省级报告
Which means, three BLoCs.
这意味着三个BLoC 。
Since this is a simple app, the core logic of these BLoCs is the same. They differ only in the type of event, the api request and response mapping. We’re going to take a closer look to the BLoC that provides the regional reports.
由于这是一个简单的应用程序,因此这些BLoC的核心逻辑是相同的。 它们仅在事件类型,api请求和响应映射上有所不同。 我们将仔细研究提供区域报告的BLoC。
事件和状态 (Events and states)
First thing first, we’re going to define the events and the states which the BLoC is going to work and communicate with. Mind that an event is the “input action” and a state is one of the possible outputs.
首先,我们将定义BLoC将要工作并与之通信的事件和状态。 注意事件是“输入动作”,状态是可能的输出之一。
Don’t mind the base classes InfectionReportBlocEvent and InfectionReportBlocState , they are empty and exist only to group conceptually the various events and types under the same semantic space.
不必介意InfectionReportBlocEvent和InfectionReportBlocState基类,它们是空的,并且仅在概念上将相同事件空间下的各种事件和类型组合在一起而存在。
So we have the event:
因此,我们有一个事件:
In this case we define only the fetch event. We could have defined other events in case we wanted to do different things, but since this is a read-only api only fetch makes sense here.
在这种情况下,我们仅定义提取事件。 如果我们想做不同的事情,我们可以定义其他事件,但是由于这是一个只读的api,因此这里的fetch才有意义。
And then we define our possible states:
然后我们定义可能的状态:
Some states do not need any extras (like RegionalReportIdle and RegionalReportLoading ), but some others will convey data, in particular:
一些州不需要任何额外的费用(例如RegionalReportIdle和RegionalReportLoading ),但是其他一些州将传达数据,尤其是:
-
RegionalReportLoadedwill deliver the loaded reportsRegionalReportLoaded将交付已加载的报告 -
RegionalReportLoadingErrorwill deliver a reason for the errorRegionalReportLoadingError将提供错误原因
Now we can go for the proper BLoC implementation.
现在,我们可以进行适当的BLoC实施了。
集团 (The Bloc)
Since in my app I needed to know if a bloc was disposed (for dependency injection purposes), I’ve defined a ClosableBloc class which extends the real Bloc class provided by flutter_bloc :
因为在我的应用程序中,我需要知道是否处置了一个bloc(出于依赖关系注入的目的),所以我定义了ClosableBloc类,该类扩展了flutter_bloc提供的实际Bloc类:
This simply overrides the close method of the base class Bloc (which is another way to say, conceptually, dispose), updates a closed variable and calls super.
这简单地覆盖了基类Bloc的close方法(从概念上讲,这是另一种表示方式),更新了一个closed变量并调用了super。
That said, defining a ClosableBloc implementation is exactly the same thing than using che original Bloc class.
也就是说,定义ClosableBloc实现与使用原始Bloc类完全相同。
Let’s break this thing down:
让我们分解一下这个东西:
-
line 8–9: we’re saying that
RegionalReportBlocis aClosableBloc(which is aBlocthat maps instances ofRegionalReportBlocEventto instances ofRegionalReportState行8-9:我们说
RegionalReportBloc是ClosableBloc(这是一个Bloc映射的实例RegionalReportBlocEvent到的实例RegionalReportState -
line 10 to 13: we just define the dependency from
InfectionReportServicewhich is the layer that will query the API. This is out of the scope of the article but still provides a good example for an asynchronous operation and a more realistic scenario than an increment button.第10到13行:我们只定义了
InfectionReportService的依赖关系,该依赖关系是查询API的层。 这超出了本文的范围,但是仍然提供了一个异步操作的好示例,并且比增加按钮提供了更实际的方案。 -
line 14 to 31: the mapping function. This is the heart of the implementation. First thing you’ll notice is the
async*which is not the usualasync: this is because the return type is aStream. Then we just switch among the possible types of event and act accordingly,yielding the corresponding state.第14到31行:映射功能。 这是实施的核心。 您会注意到的第一件事是
async*,它不是通常的async:这是因为返回类型是Stream。 然后,我们仅在可能的事件类型之间切换并采取相应的行动,yield相应的状态。 - line 33 to 37: the function that fetches and maps the data, nothing fancy. It’s out of the scope of this article, but feel free to dig into the repository’s code if you want to know more :) 第33至37行:提取和映射数据的函数,没什么花哨的。 它超出了本文的范围,但是如果您想了解更多信息,可以随时深入研究存储库的代码:)
So far we have defined the first bloc. As for the others, the structure of events, states, and mapping is exactly the same.
到目前为止,我们已经定义了第一个集团。 对于其他事件,事件,状态和映射的结构完全相同。
让我们连接UI (Let’s hook up the UI)
First things first: we need to provide the bloc to our screen. Remember the whole InheritedWidget thing discussed before? We’re going to do exactly that: make the bloc available to each widget of the screen (the subtree).
首先,我们需要将块提供给屏幕。 还记得之前讨论过的整个InheritedWidget吗? 我们将精确地做到这一点:使块可用于屏幕的每个小部件(子树)。
This is the Home route handler. For the purpose of this article you just need to know this:
这是本地路由处理程序。 就本文而言,您只需要了解以下内容:
-
This
RouteHandlergets invoked by theonGenerateRouteto create the route (I’m using named routes in the project and this package)RouteHandler调用此onGenerateRoute来创建路由(我在项目和此包中使用命名的路由) -
I’m using
MultiBlocProviderbecauseHomeScreenneeds bothNationalReportBlocandRegionalReportBloc. For one bloc only, just useBlocProviderand specify achildfor it.我使用
MultiBlocProvider是因为HomeScreen需要NationalReportBloc和RegionalReportBloc。 仅对于一个集团,只需使用BlocProvider并BlocProvider指定一个child。 -
I’m using
DependencyProviderto get the instances of the blocs for dependency injection reasons, instantiating the blocs directly by their constructor is exactly the same thing.由于依赖关系注入的原因,我正在使用
DependencyProvider来获取bloc的实例,直接通过其构造函数实例化bloc是完全一样的。
At this point we have the home route which returns a HomeScreen wrapped by a provider for the blocs it needs. Let’s see how the widgets hook up to those blocs. We’re almost done, hang in there!
至此,我们有了归属路由,该路由返回一个HomeScreen ,该HomeScreen由提供者包装,用于所需的块。 让我们看看这些小部件如何与这些集团挂钩。 我们快完成了,挂在那里!
Here’s the code home screen:
这是代码主屏幕:
Points of interest of the code:
代码的重点:
-
line 21 to 24: after the first frame is rendered, send the fetch event for the blocs. This will trigger their implementation of
mapEventToStatemaking them yield the loading state, call the service, the API, and then yield the output state to whoever is listening.第21至24行:渲染第一帧后,发送块的获取事件。 这将触发其对
mapEventToState的实现,从而使其产生加载状态,调用服务和API,然后将输出状态传递给正在侦听的任何人。 -
Once again: remember the
InheritedWidgetstuff above? Note how since bothHomePanelandInfectionsMapuse data from those blocs no injection is needed at all!再说一遍:还记得上面的
InheritedWidget吗? 请注意,由于HomePanel和InfectionsMap使用这些块中的数据,因此根本不需要注入!
Just to clarify: the tr in the Text is something used by easy_localization for multi language and SlidingUpPanel is a widget provided by this package.
需要说明的是: Text的tr是easy_localization用于多语言的内容,而SlidingUpPanel是此包提供的小部件。
Let’s start with taking a look at the HomePanel widget, which will lead us to see the simplest case of hooking up a bloc:
让我们先来看一下HomePanel小部件,它将使我们看到最简单的挂钩集团的情况:
The widget is made of two widgets. The first one, NationalReportWidget is the one that hooks up to the NationalReportBloc , which we won’t see here, and the second one is RegionsReportList that hooks up to the bloc we’ve seen above:
该小部件由两个小部件组成。 第一个是NationalReportBloc ,它是连接到NationalReportWidget的一个,我们在这里看不到;第二个是RegionsReportList ,它是RegionsReportList到我们上面看到的一个块的:
As you might have guessed, the key to everything here is BlocBuilder .
您可能已经猜到了,这里一切的关键是BlocBuilder 。
Line 15 uses a BlocBuilder which will listen to the state of a RegionalReportBloc and manage instances of RegionalReportState .
第15行使用BlocBuilder ,它将监听RegionalReportBloc的状态并管理RegionalReportState实例。
Then the builder allows us to specify what we want to build upon receiving a state update. For the sake of brevity I’ve omitted the _Body widget code since it’s just normal UI stuff.
然后,构建器允许我们在收到状态更新时指定要构建的内容。 为了简洁起见,我省略了_Body小部件代码,因为它只是普通的UI内容。
I’d say this is it. Of course BlocBuilder allows specifying other stuff like directly providing an instance of the bloc, or specifying condition for rebuilding, and so on. But since this is more an article on bloc than on flutter_bloc capabilities I won’t dig in there too much and leave you the docs at the end of the article.
我会说就是这样。 当然, BlocBuilder允许指定其他内容,例如直接提供bloc的实例,或指定要重建的条件,等等。 但是,由于这是有关bloc的文章,而不是有关flutter_bloc功能的文章,因此我不会在其中进行过多探讨,而将文档留在文章末尾。
That said, even this might be a good place to stop, I think it’s useful to take a quick look at the InfectionsMap implementation, which features something I really like from the package and the bloc pattern itself.
就是说,即使这可能是一个停止的好地方,我认为快速查看InfectionsMap实现也很有用,该实现具有我非常喜欢的软件包和bloc模式本身的功能。
We’ve seen that we can build an error widget when receiving an error state from the bloc, but what if we need to act differently? This is the case for InfectionsMap : the map is shown, then the data is loaded and the map is updated with the markers. But what if the loading doesn’t fall through? Drawing a “there was an error” error widget on the map doesn’t sound like a good idea, so what? Informing the user via a SnackBar seems a pretty good way to me (a dev without much of design skills).
我们已经看到,当从集团接收到错误状态时,我们可以构建一个错误小部件,但是如果我们需要采取不同的行动怎么办? InfectionsMap就是这种情况:显示地图,然后加载数据并使用标记更新地图。 但是,如果负载没有下降怎么办? 在地图上绘制“有错误”错误小部件听起来不是个好主意,那又如何呢? 对我来说,通过SnackBar通知用户似乎是一种不错的方法(一个没有太多设计技能的开发人员)。
In order to do this we should conceptually have a BlocBuilder which reacts to the changes of state, and another listener which brings the SnackBar up. Luckily for us, the flutter_bloc package gives us a BlocConsumer widget, which allows to do both of the things in one place!
为了做到这一点,我们在概念上应该有一个BlocBuilder ,它对状态的变化做出React,以及另一个侦听器,它可以打开SnackBar 。 对我们来说幸运的是,flutter_bloc包为我们提供了一个BlocConsumer小部件,该小部件可以在一个地方完成这两项工作!
-
at line 23 we use
BlocConsumerin the exact same fashion we did before withBlocBuilder在第23行,我们以与之前使用
BlocBuilder完全相同的方式使用BlocConsumer -
line 24 tells to trigger the listener function only if the current state is
RegionalReportLoadingError第24行告知仅在当前状态为
RegionalReportLoadingError时才触发侦听器功能 -
line 25 to 35 is the actual function that brings up the
SnackBar第25到35行是调出
SnackBar的实际功能
That’s it. As you can see we have two widgets listening to the same RegionalReportBloc (the map and the report list) which do very different things with the very same source of truth, and those things are not necessarily building widgets but calling other things too.
而已。 如您所见,我们有两个小部件在侦听同一个RegionalReportBloc (地图和报告列表),它们在具有相同真相来源的情况下执行非常不同的事情,而这些事情不一定在构建小部件,而是调用其他事物。
This is one of the best things about BLoC, if you ask me!
如果您问我,这是BLoC最好的事情之一!
It’s been a long road and we’ve seen quite a lot. My suggestion is not to be afraid of all of this but just let this sink in a bit(pun intended) and get the hang of it. I’ve used a more rudimental implementation of the BLoC pattern in a banking app I’ve developed as a consultant in a team of two devs and I can assure you that even if it was less modular than this approach the pattern really paid off on the long run in terms of maintainability, mockability, debugging and features extension and change (it was pretty much frequent for the client’s specs to change while still developing and testing).
这是一条漫长的路,我们已经看到了很多东西。 我的建议是不要害怕所有这些,而只是让它陷入一点(双关语意)并掌握其中。 我在一个由两个开发人员组成的团队的顾问中开发的银行应用程序中使用了BLoC模式的更基本的实现,并且我可以向您保证,即使该模块的模块化程度低于这种方法,该模式也确实能带来回报从可维护性,可模拟性,调试以及功能扩展和更改的角度来看,这是长期的工作(在仍然进行开发和测试的同时,更改客户的规范非常频繁)。
Before I say bye, I’ll leave here some links:
在我说再见之前,我将在这里留下一些链接:
-
GitHub repository for the complete project: https://github.com/magicleon94/corona_italy/tree/medium-article
完整项目的GitHub存储库: https : //github.com/magicleon94/corona_italy/tree/medium-article
-
Bloc documentation: https://bloclibrary.dev/
Bloc文档: https : //bloclibrary.dev/
-
flutter_bloc readme and docs: https://pub.dev/packages/flutter_bloc
flutter_bloc自述文件和文档: https ://pub.dev/packages/flutter_bloc
That’s all folks! Hope you’ve enjoyed this and found it more useful than boring!
那是所有人! 希望您喜欢它,发现它比无聊更有用!
Cheers!
干杯!
Antonello
安东内洛
css 颤动


所有评论(0)