Entitas for Flutter
Alternative way of building reactive, testable and easy to evolve apps with Flutter.
This is going to be a very long article, but it will be totally worth it. 🤞
I assume you already have working understanding of Flutter and you might even have seen this presentation by the Flutter team:
This article will be based on examples and concepts from this presentation. It can be help-full to watch this presentation first, but it is not necessary, as I will give a gradual introduction anyways.
First of all, WTF is Entitas?
Entitas is an implementation of Entity Component System pattern (ECS for short), which focus on developer experience. The most popular implementation of Entitas (Entitas-CSharp) earned over 3K stars on Github and is adopted by many game development studios, big and small.
The main principle of a pure ECS application is:
Separation of state from behaviour
Flutter kind of follows this principle, by separating between Stateless and Stateful widgets, but it is a bit clumsy IMHO. For example, here is the naive implementation of Flutter Counter App as discussed in the “Reactive mobile apps” talk:
Let’s have a walk through the defined classes.
MyApp
is a simple stateless widget, which declares the root of the app. All good and dandy! This class just represents the widget hierarchy, completely declarative, clean as a whistle.
MyHomePage
is a stateful widget and it is just boilerplate, it defines a “link” to _MyHomePageState
class and enables the caller to provide title
to the state class.
Now _MyHomePageState
is where we mix state an behaviour, even though from the naming, you might assume it only stores state.
That class does store the count in the _counter
variable, but it also defines the incrementation logic in _increment
method and it has to override build
method which provides the logic how to build the child of the stateful widget.
What is wrong with mixing state and behaviour?
First of all, mixing state with behaviour introduces coupling and information hiding, which makes it hard to unit test. It is also easier to build up a rigid application, which will be very hard to evolve (introduce new features). It is however mostly easier to write mixed code and it is also possible to mix state and behaviour in a way, which makes unit testing and refactoring still possible.
In this article we will first implement examples, where the state and behaviour is slightly mixed and than refactor it to be decoupled (as much as possible).
Lets build a counter app with Entitas for Flutter (or EnitasFF for short)
When we build thing in Entitas, or in ECS in general, first thing we need to do is to think about components. Components in ECS are the building blocks of an Entity. They are classes which store the minimal amount of data we need at a time. In case of counter app our first and most important component is:
CountComponent
just stores the count value
, which is an int
. It implements the UniqueComponent
, which is just a “marker” class. It tells EntitasFF that this class is a unique component and that this class must be immutable.
Now what is a unique component?
A unique component is a component with a restriction. There can be only one entity instance carrying a component of such type. It’s kind of like a singleton marker in a dependency injection framework. In our case we should have only one count value throughout the app, so it totally makes sense to make this component unique.
If you are still a bit confused about the terms component, entity and unique? Don’t worry the next section will shed more light on those concepts.
The buildApp
function returns the root widget of the app. But before it does so, it instantiates EntityManager
and sets the count to be 0
. The root widget is a EntityManagerProvider
this is a widget defined in EntitasFF which is an InheritedWidget
and it is primarily responsible for providing the instance of EntityManager
to its child widgets.
What is an EntityManager?
In EntitasFF, entities are created and managed through an EntityManager. If you want to compare Entitas with a relational database, than every defined component class represents a column. An entity instance is like a row and entity manager is the table itself. An entity can carry one instance of any defined component class and we can query the entity manager to give us a reference to every entity, which carries a certain component.
Now as we see on the line 3
, we can set a unique component directly on the entity manager. The entity manager does internally following:
It looks up, if there is already an entity, which carries given unique component type and if not, it will create a new entity. Than it adds (or replaces) the component on the entity and returns it.
In our particular case em.setUnique(CountComponent(0));
will create a new entity which holds CountComponent
with value
0
.
Here comes the rest of the app:
The class MyApp
is a stateless widget. On line 20-26
we wrap the Text widget which displays the count in an EntityObservingWidget
. This widget is provided by EntitasFF and has two required parameters:
provider
is a function, which given an instance of entity manager, returns an entity, which we will be observing. In our case it is quite simple, we return the unique entity which carries theCountComponent
builder
is a function which given the entity and FluttersBuildContext
, returns a widget. In our case it is the text widget where the data is value ofCountComponent
transformed to a string.
EntityObservingWidget
will make sure that, if the entity is ever to change and this means that it gets its components updated, or it was destroyed, than the builder
function is called triggering a re-rendering of it’s children.
This is also why, the only thing that we need to do in onPressed
callback of FloatingActionButton
is to get unique component CountComponent
, take the value and set the CountComponent
with the value increased by one. See lines 31–36
. This action is updating the entity which holds the CountComponent
component and successively leads to execution of builder
function on EntityObservingWidget
.
This is it! EntityManager
is the single point of truth. We don’t need to extend StatefulWidget
anymore and our application is fully reactive.
But we do mix behaviour / changing the state and widget representation logic on lines 31–36
. In order to solve this small issue and make it easily unit testable, lets introduce the concept of Systems.
As the name Entity Component System implies, system is a core concept in every ECS implementation. If you have watched the “Reactive mobile apps” presentation mentioned above, or are generally familiar with Redux, you might think of a system as of a simplified reducer.
The soul responsibility of a system is to change state. The state in EntitasFF is reflected in EntityManager
. So a system can query an entity manager for specific entities and change those entities. It also could create or destroy entities.
In Entitas we normally distinguish between four different types of Systems:
- InitSystem, a system which is executed only once on initialisation and which is responsible for creating new entities.
- ExecuteSystem, a system which is executed on every tick. With tick we mean rendering loop. Those systems enable us to implement real time behaviour of the app.
- CleanupSystem, a system which also runs on every tick but those systems are executed after all ExecuteSystem run and are responsible for cleaning up the state.
- ReactiveSystem, reactive system is a decorated execute system. Reactive systems are also executed on every tick, but they have a mechanism of early exit in case there was no relevant state change. The details will be described a few paragraphs later.
Let’s go back to our counter app and how we can refactor it in order to be more testable. As always we start with an introduction of new component:
This is a unique component which represents the increase event. In Redux we separate state from events, in Entitas it is not necessary. State and event is both data, so why separate them?
Now we can refactor the definition of the FloatingActionButton
to this:
onPressed
callback is now super simple. It sets a new instance of IncreaseCountComponent
on entity manager. This action either produces a new entity with IncreaseCountComponent
component or updates it. We don’t do any real computations in our view hierarchy declaring code. The computation will happen in a reactive system. But before we look at the systems lets first see how the buildApp
function has evolved:
If you scroll up and compare it with previous implementation of buildApp
function, you will see that EntityManagerProvider
now has a new property — systems
.
Remember how I sad than the main responsibility of the EntityManagerProvider
is to provide entity manager to it’s children?
The secondary responsibility of the EntityManagerProvider
is to execute systems, but only if the systems property is set. Internally it uses Flutters SingleTickerProviderStateMixin
, similar how one would implement animations in Flutter.
The systems
property expects an instance of a RootSystem. A root system is a system which aggregates other systems, so we can build systems hierarchies similar to how we build widget hierarchies in Flutter. In our particular case the hierarchy is super flat, but in more complex scenarios it is better to introduce some hierarchy.
Now lets have a look at the implementation of systems.
The system InitCounterSystem
is an EntityManagerSystem
and implements InitSystem
. A system should extend EntityManagerSystem
if it want to have access to entity manager. The root system will automatically inject one to it’s child systems if they extend this class. The implementation of InitSystem
forces us to override init
method. Which will be called by EntityManagerProvider
on initialisation.
A careful reader might noticed that, we moved entityManager.setUnique(CountComponent(0));
statement from the buildApp
function into the InitCounterSystem
. This way the responsibility of buildApp
function become a bit cleaner.
IncreaseCounterSystem
is the magic sauce, which implements the transformation of the count component. Or it rather performance the same thing in executeWith
method, what the FloatingActionButton
did in onPressed
callback before the refactoring.
Before we can fully understand how ReactiveSystem
work, I need to unpack the concept of Group
and EntityMatcher
.
Lets start with entity matcher. EntityMatcher
lets us describe which component should an entity contain in order to be “selected”. It is basically a simple query. We can instantiate an EntityMatcher with three optional properties:
List<Type> all
List<Type> any
List<Type> none
An entity is selected if it has all the component types listed in all
. At least one component type listed in any
and none of the component types listed in none
. So EntityMatcher(all:[A, B], any:[C, D], none:[E])
is a query where we say we are interested in entities which have components A
and B
set, which also has to have C
or D
component present and should not have component E
.
The entity manager has a method:
Group groupMatching(EntityMatcher matcher)
which we can call in order to get all the entities which comply with the provided matcher. The interesting detail about the group is that it is a class which is wrapped around a set of entities, which match the group and it is synchronously updated if we change the state in a way that makes entities:
- leave the group (not match the matcher any more)
- be added to the group, for example if we have a matcher
all:[A, B]
an entity which already had theA
component, now if we add componentB
to it the entity will enter the group ofall:[A, B]
- or just be updated, meaning that the entity is already in the group but underwent a change which was relevant in regards to the group. If we keep same example as before. The entity has components
A
andB
and one of those components was replaced. The entity is still in the group, but it was updated.
Because of this synchronous updates, we can keep a reference to the group for a long time and be sure that if we will iterate on those entities, they will have the components that we expect. It also lets us observe the group and be notified if an entity was added, removed or underwent a relevant update.
And this is how the Reactive systems work.
When we define a reactive system, we need to provide two getters:
EntityMatcher get matcher => ...;
GroupChangeEvent get event => ...;
With matcher
property we define which Group of entities we should observe for changes. And event
property defines which event should be relevant for us as trigger. GroupChangeEvent
is defined as following:
Back to our IncreaseCounterSystem
. The getters we provided say — we want the system to be triggered when IncreaseCountComponent
is added, or exchanged on an entity. In our case this happens each time we press FloatingActionButton
.
One more small detail how reactive system is different from an execute system. In reactive system we need to override the executeWith
method. This method gets all the entities passed which have changed and lead to reactive system being triggered. In our current example this List is not important, but we will have other examples where this list will be used.
OK, we are almost done with this example, one last system to go:
This is an execute system. It will count the number of ticks the application is running and will increase the count on every 120th tick, which is every two seconds. Now our small app has real time capabilities 🤯. And as a bonus, they are easily unit testable:
Before we continue with the next example, lets have a short recap.
In this example we learned concepts like Entity
, Component
, UniqueComponent
, EntityManager
, EntityManagerProvider
, EntityObservingWidget
, EntityMatcher
, Group
and different types of systems. We see that with EntitasFF we can avoid extending StateFullWidget (with all it’s boilerplate) and can decouple the data transformation logic from the widget composition, which makes the transformation logic easily testable even if the transformation should happens over time. In the next sections we will have a look at a more advanced example and how we can implement it with EntitasFF following the “There is no spoon” principle 😜.
Next example in the “Reactive mobile apps” presentation is about a shopping list and shopping cart applications. The app has two screens — shopping list and shopping cart. Where the user can add an item from sopping list to shopping cart, by tapping on the item, there is a shopping cart button on the top right of the shopping list screen, which displays the total amount of items user added to the shopping cart. And if user is taping on this button, the shopping cart screen appears.
The corresponding Github repositories contains multiple implementations of this app where all implementations use a shared set of classes which you can find in the common
folder:
We will build our example on vanilla version of the app. But first lets walk through the shared classes and see how many of them we will, need and what will we change.
First of all in the common
folder, we have the model classes. There is a Cart
class, CartItem
, Catalog
and Product
. All those classes represent concepts on a higher level of abstraction. And this is where ECS and Entitas are completely different from the typical object oriented programming. I call it:
“There is no spoon“ principle.
In the ECS we define the data bottom up. We look at actual data we need and do not invent metaphors for it. In the case of shopping list/cart the data can be represented with following set of components:
Product
and CartItem
are just entities. Catalog is a Group
of entities which match all:[ProductIdComponent, ProductNameComponent, ColorComponent]
.
Cart
can be represented by a Group
which matches: all:[ProductIdComponent, ProductNameComponent, ColorComponent, CountComponent]
This means that an entity is part of the cart if it has a CountComponent, which represents how many items of this product user put in the shopping cart.
ProductSquare
is the widget which represents a product in the product list on the shopping list screen. Lines 16–20
represent the logic which is needed to add this particular product to the shopping cart. As you can see, we again add data transformation logic in the widget definition, but don’t you worry, we will refactor it, when we will introduce a more advanced use case.
The widget which represents the list of product squares is implemented as following:
On line 4
we see the first appearance of GroupObservingWidget
. It is very similar to EntityObservingWidget
except that it observes a group and not a single entity. In order to define a group we need to provide an EntityMatcher
as matcher
property. The builder
function receives an instance of the group and Flutters BuilderContext
. On Line 8
we create product squares and provide them with product
entity. If we happened to change a name, or a color of the product, or maybe even add or remove products, the builder
function will be triggered leading to re-rendering of the GridView
.
The initialisation of the app looks as following:
We took over the idea of the products being created after a slight delay from the presentation, showcasing the possibility of populating the ProductGrid
reactively. Otherwise I believe the initialisation is pretty straight forward. And can be moved to an InitSystem
, when we decide to introduce systems.
The App itself is broken down into two classes:
MyApp
class is quite boring, it’s just configuration of Flutters MaterialApp
widget. MyHomePage
is a wrapper about Flutters Scaffold
. The slightly interesting part for us is on lines 23–28
, here we define the CartButton
to be an app bar action. The CartButton
is wrapped in GroupObservingWidget
where we are interested in all entities which have ProductNameComponent
and CountComponent
, so we can go through those entities and add up the values of the CountComponent
. And don’t forget, it is an observing widget so it will redraw reactively.
I think at this point we mentioned all the interesting parts of the shopping cart app and it is time to introduce systems and some more complexity to the use case.
The vanilla shopping cart example is missing multiple important features. First feature is for user to be able to remove items form the shopping cart and second to have an actual price on an item. To make matters worse, or rather more real life appropriate, let’s introduce two currencies USD
and EURO
and a possibility for the user to to switch between those.
As always we start with introducing new components:
PriceComponent
is something we set on the entity while initialising it. A price component has two values, currency
and amount
. This means that on initialisation we have products, which are priced in a certain currency and we will need to convert the amount in regards to conversion ratio.
Speaking of selected currency, conversion ratio and converted amount:
Conversion rate component and selected currency are unique components, because those are values which are unique through all the app, they can change but there should be only one instance of them. This is also the big difference between typical singleton pattern and unique entity / component. Singletons are mostly implemented as unique, but also not changeable instances. Unique in Entitas means that there will be zero or one entity holding this component, whithout any other guarantees.
AmountInSelectedCurrencyComponent
is set on the product entity and is reactively computed, when we have price and current conversion rate and selected currency set. Here is the reactive system which does the job:
The system is triggered if price or conversion rate, or selected currency is changing. It safely extracts selectedCurrency
and conversionRates
values from entity manager and than iterates over all entities which have a price component and sets the AmountInSelectedCurrencyComponent
with the proper value. It is actually not necessary to set AmountInSelectedCurrencyComponent
, we could also compute this value on the fly in places where we need it, but if we want to follow a single responsibility principle in our code, introduction of such “reactively computed” components is a good thing.
Following the single responsibility principle, we introduce another component:
This component holds the string which represents the price of an item based on currently selected currency:
priceLabel
function is not part of ComputePriceLabelSystem
because it will be reused in ComputeTotalSumLabelSystem
. This system will compute the string which represent the total cost for all items in the shopping cart.
Also here we have two distinguished components:
- One for the total amount, which hold the sum as double value. It is computed by iterating over all entities which have
CountComponent
andAmountInSeletectedCurrencyComponent
- Another for textual representation of total amount. It takes the
TotalAmountComponent
andSelectedCurrencyComponent
to pass it topriceLabel
function
Last but not least, we have a set of components and systems which represent / transform data according to user interaction:
RemoveFromShoppingCartComponent
and AddToShoppingCartComponent
are set directly on the product entities. This way the “event processing” systems can directly iterate on the entities
provided to executeWith
method.
With all this reactive systems in place, the stateless widgets can stay completely free of state transformation logic.
The product square widget now looks as following:
MyHomePage
widget is also very clean:
And buildApp
function is only responsible for instantiation of EntityManagerProvider
:
Have you noticed something new in the buildApp
function? No?! 🤨
The systems
property is set to be a ReactiveRootSystem
. This is a small optimisation that we can make if all the systems we wrote are not dependent on periodic execution, but rather on state change. ReactiveRootSystem
is a RootSystem
which performs an early exit, in case the state of your application did not change since the last execution.
And that’s it! We are finally done! I hope you enjoyed this super long excursion into building reactive apps with EntitasFF. If you did, please consider to give this article a clap or two.
I will publish EntitasFF and the examples in a few days on my Github. Just want to figure out a few things first. If you are interested in contributing to EntitasFF or build a few examples, please write a comment or reach out to me on twitter.
Thank you for reading!!!
[UPDATE 20.04.2019]
I wrote a second article about EntitasFF