flutter bloc
目标 (Goal)
The app I’m working on in my spare time has a need to consume large lists of data, with a key requirement of making it easy to filter the list based on properties of items in the list. Additionally, the list should be searchable to further narrow the results.
我在业余时间使用的应用程序需要消耗大量数据 ,其中一项关键要求是使其可以轻松地根据列表中项目的属性来过滤列表。 此外,该列表应可搜索以进一步缩小结果范围。
There are several places I was going to need this functionality, so it needed to be as generic as possible. I’m making heavy use of the flutter_bloc
package for state management, so it should ideally be able to slot right into the existing app architecture. Needing to be generic anyway, it seemed like a perfect opportunity to create a Flutter package that could be helpful to everyone!
我将在几个地方需要此功能,因此它需要尽可能通用。 我正在大量使用flutter_bloc
软件包进行状态管理,因此理想情况下,它应该能够直接插入现有的应用程序体系结构中。 无论如何,都需要通用,这似乎是创建Flutter软件包的绝好机会,它可能对每个人都有帮助!
To recap, our end goal is a package that meets the following criteria
回顾一下,我们的最终目标是满足以下条件的软件包
Follows the
flutter_bloc
pattern遵循
flutter_bloc
模式- Accepts a data source 接受数据源
- Exposes state derived from specified properties of the data source items that can be used to render UI that manages the available filter options 公开从数据源项的指定属性派生的状态,这些状态可用于呈现管理可用过滤器选项的UI
- Is able to filter that data source by user-activated options 能够通过用户激活的选项过滤该数据源
- Is able to narrow that data source by searching provided properties 能够通过搜索提供的属性来缩小数据源
- Exposes state that can be used to render a list 公开可用于呈现列表的状态
- Is as generic as possible 尽可能通用
初步计划 (Initial plan)
It shouldn’t be difficult to get started with this package. To that end, I knew I only wanted one main entry-point that needed to be integrated. Any remaining functionality should be exposed underneath that widget but automatically connected and ready to use.
开始使用此软件包并不难。 为此,我知道我只想要一个需要集成的主要入口点。 所有剩余的功能都应显示在该小部件下方,但会自动连接并可以使用。
As someone interfacing with the package, you should be able to render your UI however (and wherever) you’d like. We’ll be integrating with flutter_bloc
which already has an excellent means to accomplish that via the BlocBuilder
widget… which means our primary means of communicating state out should be a bloc.
作为与该程序包交互的人,您应该能够(但无论如何)呈现您的UI。 我们将与flutter_bloc
集成,后者已经具有通过BlocBuilder
小部件完成此操作的极好方法……这意味着我们传达状态的主要方法应该是bloc。
It stands to reason, then, that everything could be hooked together as shown in the following diagram:
因此,有理由将所有内容都钩在一起,如下图所示:
条目小部件 (Entry widget)
It’s the only widget directly rendered by the application, and is supplied with:
它是应用程序直接渲染的唯一窗口小部件,并提供:
- The widget you want to render that will have access to the package-injected blocs in its build context 要渲染的小部件,将在其构建上下文中访问已注入包的块
- Properties (corresponding to the items that will be provided by the source bloc) to use while filtering/searching 筛选/搜索时要使用的属性(与源块将提供的项目相对应)
- A source bloc providing the base data 提供基本数据的源块
筛选/搜索状态 (Filter/search state)
A bloc that takes the source bloc and filter/search properties and:
具有源块和过滤器/搜索属性以及以下内容的块:
- Uses them to generate groups of values that match all of the incoming data 使用它们来生成与所有传入数据匹配的值组
- Exposes state (as a stream) to the list state bloc 将状态(作为流)公开到列表状态组
- Is available to the child build context to render an appropriate filtering UI and to allow that UI to toggle filtering conditions as active or inactive 可用于子构建上下文以呈现适当的过滤UI并允许该UI将过滤条件切换为活动或不活动
清单状态 (List state)
A bloc that takes the source bloc and state from the filter bloc to:
一个将源块和状态从过滤器块转移到以下位置的块:
- Filter the incoming data based on any filter conditions that have been selected as active by the app 根据应用已选择为活动的任何过滤条件来过滤传入数据
- Is available to the child build context to render an appropriate list UI 可用于子构建上下文以渲染适当的列表UI
入门 (Getting started)
One would imagine the logical place to start would be the entry widget… after all, that’s going to be the integration point with the package. Yup, that would have been a great idea! At the time, though, I still wasn’t sure exactly how I wanted to tie everything together. Yeah, I know the diagram up there looks really flashy, but it comes from attempting to piece this timeline together once everything was completed. (The next guide I take on I plan to actually write it as I’m going through the process, which should be a great change of pace).
可以想象,逻辑上的起点应该是入口小部件……毕竟,这将是与软件包的集成点。 是的,那真是个好主意! 但是,当时,我仍然不确定如何将所有内容捆绑在一起。 是的,我知道上面的图看起来确实很浮华,但这是由于在完成所有操作后将这段时间轴拼凑在一起而来。 (上接导游,我承担我打算实际编写它, 因为我经历的过程,这应该是节奏有很大的变化)。
Perhaps the next best place to start would be the list state? Sounds really good, especially since the whole point here is to render a list of filtered data to our UI. However, my brain was really just ready to start tackling the filtering portion of the problem, so that’s where I ended up starting.
也许下一个最好的起点是列表状态? 听起来确实不错,尤其是因为这里的全部要点是将经过过滤的数据列表呈现到我们的UI。 但是,我的大脑真的已经准备好开始解决问题的过滤部分,因此我终于开始了。
Having never created a standalone package before, I decided a prudent first step would be to start development in my current project until such time that I was able to prove out most of the functionality. As I was working on the filtering bloc (in-depth dive below), I didn’t like that I planned to be dealing with both the filtering state and the search state in the same place. Dealing with both the list of potential filter conditions and active filter conditions was already shaping up to be more than enough for one bloc. Additionally, it was acting as a straight passthrough of the target search properties from the entry widget, which was also not ideal.
之前从未创建过独立的程序包,所以我决定谨慎的第一步是在当前项目中开始开发,直到能够证明大部分功能为止。 当我在使用过滤器组时(下面将进行深入研究),我不喜欢我打算在同一位置处理过滤器状态和搜索状态。 处理潜在过滤器条件列表和活动过滤器条件列表已经成形为足以容纳一个集团。 此外,它还充当条目小部件中目标搜索属性的直接传递,这也不理想。
Before revisiting the plan, I decided it was time to migrate all of the code (basically the working filter conditions bloc and data classes) into a separate repository. However, there was a huge hiccup when I started that process! You can read all about that adventure here.
在重新考虑该计划之前,我决定是时候将所有代码(基本上是工作过滤条件块和数据类)迁移到一个单独的存储库中。 但是,当我开始该过程时,却遇到了巨大的麻烦! 您可以在此处阅读有关该冒险活动的所有信息。
新计划 (The new plan)
In hindsight, the new plan wasn’t drastically different from the old plan. It simply spread out the concerns a little more.
事后看来,新计划与旧计划并没有太大的不同。 它只是稍微分散了关注点。
清单管理员 (List manager)
The main entry point to using the package. The required child
has access to all of the below blocs to use while constructing UI. At a minimum, you must also supply a list of keys to be passed along to the filter conditions bloc.
使用包的主要入口点 。 所需的child
有权访问以下所有集团,以在构造UI时使用。 至少,您还必须提供要传递给过滤条件组的键列表。
There’s really nothing much to this widget, it just sets up the other blocs and provides the child
to be rendered.
这个小部件实际上没有什么太多,它只是设置其他块并提供要渲染的child
。
过滤条件组 (Filter conditions bloc)
Now that the filter conditions bloc is no longer responsible for any of the search state, it’s a bit easier to reason through.
现在,过滤条件块不再对任何搜索状态负责,因此推理起来要容易一些。
源初始化 (Source initialization)
On initialization, we need to subscribe to the sourceBloc
in order to regenerate the availableConditions
any time our source information changes. We also want to ensure no activeConditions
are left dangling.
在初始化时,我们需要订阅sourceBloc
,以便在源信息发生任何更改时重新生成availableConditions
。 我们还想确保没有activeConditions
悬空。
If the sourceBloc
isn’t in its loaded state (as determined by the supplied type from the parent widget), we want to skip parsing the data for now.
如果sourceBloc
尚未处于加载状态(由父窗口小部件提供的类型确定),我们现在想跳过对数据的解析。
For every item in the source state, we need to keep track of the corresponding value for every filterProperty
. In an effort to reduce the number of iterations through the source items, we also need to go ahead and store all potential incoming condition keys so we can narrow down any activeConditions
that have been removed from the source state.
对于源状态中的每个项目,我们需要跟踪每个filterProperty
的对应值。 为了减少通过源项目进行的迭代次数,我们还需要继续进行操作并存储所有潜在的传入条件键,以便我们缩小从源状态中删除的activeConditions
。
Speaking of generateConditionKey
, why didn’t we use the same storage format as we did for the availableConditions
? That was initially the plan! However, the use-case for availableConditions
— rendering the available values for every filter property key — lends itself nicely to nested iteration. In the case of activeConditions
, we always want direct and easy access to that list. A concatenation of the filter property key and its value serves as a unique enough identifier. At the moment we’re filtering out everything but String
values, but adding number/boolean support would be rather easy.
说到generateConditionKey
,为什么我们不使用与availableConditions
相同的存储格式? 那最初是计划! 但是, availableConditions
的用例(为每个过滤器属性键呈现可用值)非常适合嵌套迭代。 对于activeConditions
,我们始终希望直接轻松地访问该列表。 过滤器属性键及其值的串联用作唯一的足够的标识符。 目前,我们正在过滤掉除String
值之外的所有内容,但是添加数字 / 布尔值支持将非常容易。
Last but not least we want to ensure a stable (and unique) ordering of availableConditions
such that the filtering UI doesn’t shift around as the source state is updated.
最后但并非最不重要的一点是,我们要确保availableConditions
顺序稳定(且唯一),以使过滤UI在源状态更新时不会四处移动。
添加和删除条件 (Adding and removing conditions)
These two functions only differ by two lines, so we’ll discuss them together.
这两个函数仅相差两行,因此我们将一起讨论它们。
If we haven’t entered into an initialized state yet, we’ll have no means to accurately set a condition as active and don’t want to modify the state.
如果我们还没有进入初始化状态,我们将无法准确地将条件设置为活动状态,也不想修改该状态。
If the set of activeConditions
already has a matching entry (or in the case of RemoveCondition
if there isn’t already a matching entry), there’s no need to modify the state.
如果activeConditions
集合已经具有匹配的条目(或者在RemoveCondition
的情况下,如果还没有匹配的条目),则无需修改状态。
When using theBloc
pattern state mutation is a no-no (see ‘The mutation problem’ below), so we need to create a new Set
when manipulating activeConditions
.
使用Bloc
模式时,状态突变是禁止的(请参见下面的“突变问题”),因此在处理activeConditions
时我们需要创建一个新的Set
。
搜索查询块 (Search query bloc)
There’s really nothing at all to this bloc.
这个集团实际上根本没有任何东西。
Clearing the searchQuery
sets it back to an empty string. Setting a searchQuery
will store the provided value lowercase in order to make filtering source items more reliable.
清除searchQuery
设置回一个空字符串。 设置searchQuery
将以小写形式存储提供的值,以使过滤源项目更加可靠。
物品清单集团 (Item list bloc)
The ItemListBloc
is the bread and butter of the package. It takes the source items, activeConditions
from the FilterConditionsBloc
, the searchQuery
from the SearchQueryBloc
, searchProperties
passed down from the base widget and distills them into a complete list of items that can be rendered however you’d like.
ItemListBloc
是包装的面包和黄油。 它从基础窗口小部件传递源项,即FilterConditionsBloc
activeConditions
, searchQuery
的SearchQueryBloc
,从基本窗口小部件传递下来的SearchQueryBloc
, searchProperties
它们提炼成可根据需要呈现的项的完整列表。
初始化 (Initialization)
As we rely on data from three other blocs, we need to set up our listeners. We could do work inside these callbacks (as we did in the FilterConditionsBloc
), but it made more sense to keep everything simple and in the event system.
当我们依靠来自其他三个集团的数据时,我们需要设置我们的监听器。 我们可以在这些回调中进行工作(就像我们在FilterConditionsBloc
所做的那样),但是在事件系统中保持一切简单更有意义。
Don’t forget to cancel the listeners when the bloc is closed! I initially tried to pipe all three of these into a Future.wait
, but that doesn’t play nicely with the null aware syntax as a promise wasn’t provided in the case where the subscription didn’t exist. The only other option is a lot of conditional logic or providing empty promises as default values. I made the decision to not optimize that now, as closing the ItemListBloc
should be a rare occurrence.
团体关闭时,别忘了取消监听器! 最初,我尝试将所有这三个方法都输送到Future.wait
,但这与以空值Future.wait
语法不能很好地配合,因为在不存在订阅的情况下没有提供承诺。 唯一的其他选择是大量条件逻辑或提供空承诺作为默认值。 我决定现在不对其进行优化,因为关闭ItemListBloc
应该很少见。
映射事件 (Mapping events)
I generally try to keep logic out of mapEventToState
, however, this bloc needs to respond in the same way whether new source items come in, new activeConditions
come in, or a new searchQuery
comes in. The entire list needs to be regenerated every time.
我通常尝试将逻辑排除在mapEventToState
,但是,无论是否有新的源项,有新的activeConditions
或有新的searchQuery
,此组都需要以相同的方式进行响应。每次都需要重新生成整个列表。
If conditions aren’t already initialized, or if the source bloc isn’t in its loaded state (it shouldn’t really be possible for the filter conditions to be initialized and at the same time have the source bloc not loaded… but it doesn’t hurt to protect against that case) we have a special state we want to emit for that in order to differentiate from a standard empty state.
如果条件尚未初始化,或者源块未处于其加载状态(实际上不应该对过滤器条件进行初始化,同时未加载源块……但是它不会可以保护自己免受伤害),我们有一个特殊的状态要为此发出,以区别于标准的空状态。
We then send the source items through filtering and searching (see below) and dispatch either an event to let the caller know there are no results (an empty state), or an event with the filtered and searched results.
然后,我们通过过滤和搜索(请参阅下文)发送源项目,并调度一个事件以使调用者知道没有结果(空状态),或者调度一个具有过滤和搜索结果的事件。
过滤源项目 (Filtering source items)
As with the rest of the implementations, no one part is terribly complex… that’s by design. Filtering the source items is very straight forward.
与其余的实现一样,没有哪一部分是非常复杂的……那是设计使然。 过滤源项目非常简单。
If there are no activeConditions
we can short-circuit some of the logic and immediately return the source items.
如果没有activeConditions
我们可以短路一些逻辑并立即返回源项目。
We then proceed to check the source items against all of the activeConditions
. We can also short-circuit some of this logic using any
… we don’t care if the item matches every single active condition, it just needs to match one.
然后,我们将对照所有activeConditions
检查源项目。 我们还可以使用any
一种来短路某些逻辑……我们不在乎该项目是否与每个有效条件都匹配,它只需要匹配一个即可。
搜索源项目 (Searching source items)
Similar to filtering the source items, we can short-circuit some of the logic and immediately return the source items if there is no searchQuery
.
与过滤源项目类似,我们可以缩短一些逻辑,如果没有searchQuery
,则立即返回源项目。
We then proceed to check the source items against the searchQuery
for every provided searchPropery
. We can also short-circuit some of this logic using any
… only one search property needs a positive match.
然后,我们针对每个提供的searchPropery
对照searchQuery
检查源项。 我们还可以使用any
一种来短路某些逻辑……只有一个搜索属性需要一个正匹配。
This is, for sure, a very basic search algorithm (if you can even call it that). It gets the job done, but that’s about it. More information below, but a proposed update is to provide a pluggable search callback such that you could implement whatever search works for you (I’m mainly thinking about fuzzy searching).
当然,这是一个非常基本的搜索算法(如果您甚至可以称呼它)。 它完成了工作,仅此而已。 以下是更多信息,但建议的更新是提供可插入的搜索回调,以便您可以实现对自己有用的任何搜索(我主要是在考虑模糊搜索)。
测试中 (Testing)
I’m not going to cover every single line of test code here, I don’t think it would be valuable. Instead, I’m going to focus on a few test cases that were noteworthy.
我不会在这里介绍测试代码的每一行 ,我认为这没有用。 相反,我将重点关注一些值得注意的测试用例。
Wherever possible I’ve moved to the bloc_test
package for testing. It saves hassle, provides pretty much all of the helpers you would need, and encourages isolated test state (even if that does make things a little more verbose in the end). Very highly recommended.
我已尽可能地移至bloc_test
软件包进行测试。 它省去了麻烦,提供了几乎所有您需要的帮助程序,并鼓励了孤立的测试状态(即使这样做确实会使事情变得更加冗长)。 极力推荐。
变异问题 (The mutation problem)
As most good tests should, this particular case helped validate assumptions I had surrounding underlying state management with the Bloc
pattern. The test errors quickly highlighted where I was going wrong and where the fix should be implemented.
就像大多数好的测试应该的那样, 这个特殊的案例有助于验证我对Bloc
模式所涉及的基础状态管理的假设。 测试错误Swift突出了我要去哪里哪里以及应该在哪里实施修复。
More specifically, I had hoped to be able to get away with a little state mutation when updating the active filter conditions. In the current single Set
iteration that’s very easily accomplished. In the previous implementation with nested data (that led to the above-linked test case throwing errors), that required far more boilerplate to accomplish.
更具体地说,我希望能够在更新活动过滤器条件时摆脱一些状态突变。 在当前的单个Set
迭代中,这很容易实现。 在具有嵌套数据的先前实现中(导致上述链接的测试用例抛出错误),需要完成更多的样板工作。
As the data was heavily nested we would first have to check if an array of values existed for the provided property, and if that array didn’t already have an entry for the provided value.
由于数据大量嵌套,因此我们首先必须检查所提供的属性是否存在值数组,以及该数组是否尚未具有所提供值的条目。
If no value existed, we need to create a new map reference to hold the existing entries. Then we need to update the array of the provided property (again, by creating a new array reference and modifying it appropriately).
如果不存在任何值,则需要创建一个新的地图引用来保存现有条目。 然后,我们需要更新提供的属性的数组(同样,通过创建一个新的数组引用并对其进行适当的修改)。
流问题 (The stream problem)
As mentioned above, the bloc_test
package really helps when testing a bloc
. In almost all situations the whenListen
helper is plenty powerful, however, it doesn’t give any flexibility as to when the provided items are piped into the stream. If you want to test interactions between an external bloc (or stream) and the internal behavior of the bloc under test, you’ll need to look for another solution.
如上所述,在测试bloc
时, bloc_test
软件包确实bloc_test
。 在几乎所有情况下, whenListen
帮助程序都非常强大,但是,对于何时将提供的项目通过管道传输到流中,它没有任何灵活性。 如果要测试外部块(或流)与被测试块的内部行为之间的交互,则需要寻找其他解决方案。
The simple setup below allows you to add items into the source stream at whatever point in time you actually need to test, instead of all at once on the first listen.
下面的简单设置允许您在实际需要测试的任何时间点将项目添加到源流中,而不是在第一次监听时一次添加所有项目。
出版 (Publishing)
Publishing the package to pub.dev was amazingly easy, kudos to the team! Rather than rehash the already excellent documentation, I’ll just provide the resulting package… check it out!
将程序包发布到pub.dev非常容易,对团队来说是个荣誉! 与其重新整理已经非常出色的文档 ,不如仅提供所产生的软件包 …检查一下!
从这往哪儿走 (Where to go from here)
There are absolutely things I don’t like about the finished implementation and things I would have done differently given what I know now.
对于完成的实现,绝对会有我不喜欢的事情,并且根据我现在所知道的,我会做的事情有所不同。
源块 (Source bloc)
I feel the implementation would be better served by not having to supply a source bloc, supplying a repository to the widget instead. Since the package is supplying all state necessary to render UI for the list, having a source bloc manage that as well is rather redundant. It also allows for the internal state checking in the package to be much simpler.
我觉得自己的执行将通过不提供源集团,提供一个存储库的小部件,而不是得到更好的服务。 由于程序包提供了呈现列表的UI所需的所有状态,因此由源块来管理它也是多余的。 它还允许包装中的内部状态检查更加简单。
可插拔搜索 (Pluggable search)
I love the flexibility that fuzzy searching brings to the table. However, I don’t feel it should be the default for everyone, nor should it increase package size needlessly if it will never be used. To that end, there should be some form of pluggable search provider or callback the caller can use to replace the current (naive) search implementation.
我喜欢模糊搜索带来的灵活性。 但是,我不认为这应该是每个人的默认设置,也不要不必要地增加包装尺寸,如果它永远不会使用的话。 为此,应该有某种形式的可插拔搜索提供程序或回调,调用者可以使用它来替换当前的(原始)搜索实现。
非字符串过滤条件 (Non-string filter conditions)
Not all data is made of strings. Filter conditions should also be able to support numbers and booleans.
并非所有数据都是由字符串组成的。 过滤条件还应该能够支持数字和布尔值 。
Booleans will need special handling as their value isn’t display-ready. Numbers will need special handling as we may want to treat their values as part of a range instead of distinct values.
布尔值将需要进行特殊处理,因为它们的值不适合显示。 数字将需要特殊处理,因为我们可能希望将其值视为范围的一部分而不是不同的值。
增强的过滤器选项 (Enhanced filter options)
The only option for filtering at the moment is additive (meaning that an item that matches any one of the active conditions will make the cut). More advanced filter options would be a benefit.
目前唯一的过滤选项是累加(意味着与任何一个有效条件匹配的项目都会进行剪切)。 更高级的过滤器选项将是一个好处。
I would love your feedback on the already proposed improvements, as well as thoughts on other ways to improve the package!
我希望您对已经提出的改进意见以及对改进包装的其他方式有任何想法!
flutter bloc