Edifice Core#
Class overview#
|
The main application object. |
|
The base class for Edifice Elements. |
Reference to an |
|
Decorator which turns a render function of props into an |
|
Place a child passed through the special |
Declaring Element Trees#
An edifice application is created by rendering a Element
with an
App
.
The rendering for an Edifice application is done by declaring a tree of Elements
starting with a single root @component
, and then declaring its children.
An Element may be either a
Base Element
or a @component
Element.
A @component
Element is a function which renders an
Element
tree.
Elements have both internal state and external properties. The external properties, props, are passed as arguments to the Element:
@component
def Foo(self, a, b, c): # a, b, c are the props
The props are owned by the external caller and must not be modified by
this @component
Element.
Render a Foo
with props a=1, b=2, c=3
:
Foo(a=1, b=2, c=3)
The internal state belongs to the Element.
In a @component
Element, the internal state is managed by Hooks.
Changes in state or props will automatically trigger a re-render.
Declaring an Element tree in a @component
Element render function looks
like this.
To declare an Element to be the parent of some other
child Elements in the tree, use the parent as a
with statement context manager.
@component
def MyApp(self):
with Window():
with VBoxView():
with HBoxView():
Label(text="Username: ")
TextInput()
with HBoxView():
Label(text="Email: ")
TextInput()
In HTML/XML/JSX, this would be written as:
<Window>
<VBoxView>
<HBoxView>
<Label text="Username: " />
<TextInput />
</HBoxView>
<HBoxView>
<Label text="Email: " />
<TextInput />
</HBoxView>
</VBoxView>
</Window>
You describe your entire application as a single root Element, which has child Elements representing different parts of your application. Each Element is responsible for managing its own state.
Model-View-Update#
Edifice, like React, uses the Elm Architecture, also known as Model-View-Update. This means that there is a one-way information flow from Model to View to Update and then back to Model.
Model |
The state of the application declared with |
View |
The |
Update |
Qt Widget event handlers which change the state. |
It is the one-way information flow of Model-View-Update which differentiates it from Model-View-Controller, and which makes the Model-View-Update style of GUI programming scale up well to complicated user interfaces while remaining well-organized and bug-free.
Compare this to the Models and Views in Qt Quick and notice how in Qt Quick the information flow is bidirectional through the Delegate.
Rendering#
Conceptually, Edifice (and React) works like this: Every time there is a
change (Update) to the state (Model), the render function (View) is
called and it renders the entire Element
tree of Qt Widgets from scratch.
That sounds expensive and slow, and it would be if it weren’t for the diffing algorithm.
The diffing algorithm#
When Elements are rendered, the Element tree is then compared against the result of the previous render. The two Element trees are diffed and the Elements from the previous render are updated with new props.
The diffing algorithm will compare a parents Element’s children from the previous render with the children from the new render, to try to update the old children instead of replacing them.
If a new Element is a different class than the old Element, the old Element is replaced with the new Element.
If a new Element is the same class as the old Element, they are assumed to be the same and the old Element will be updated with new props and the same state.
If the props and state of an Element are the same as the previous render, then the Element from the previous render will be re-used and not rendered.
When a parent Element has many peer child Elements of the same class, a more complex procedure (the same as in React) will determine which Elements to maintain and which to replace.
When comparing the child Elements, the Element’s
_key
attribute will
be compared. Elements with the same _key
and same class are assumed to be
the same. You can set the key using the Element.set_key()
method.
with HBoxView():
if some_condition:
MyElement("Hello").set_key("hello")
if other_condition:
MyElement("World").set_key("world")
If the _key
is not provided, the diffing algorithm will guess which
child Elements are identical based on the order of the children.
Whenever a parent has children which are added and removed,
it is recommended to Element.set_key()
on the children
to tell the diffing algorithm which child Elements are identical so that it
doesn’t have to guess.
Substitutional __eq__
relation#
The Python __eq__ relation is important for everything in Edifice. It is used, for example:
To compare props for deciding when an
Element
must be re-rendered.To compare
use_state()
state variables for deciding when a@component
must be re-rendered.To compare dependencies for deciding when a
use_effect()
must be re-run.
For the __eq__
relation to work properly, it should mean that if two
objects are __eq__
, then one can be substituted for the other.
Keep in mind that for Python object types __eq__
defaults to
referential identity.
By default,
object
implements__eq__()
by usingis
Sometimes this referential identity __eq__
is what you want,
for example with large objects like images or tensors, because it’s too expensive
to compare the objects by value and you know that you don’t have duplicate values
in your program. But usually you want an __eq__
that compares values.
Every value used as a prop or state or dependency in Edifice should
have a substitutional __eq__
relation.
Element initialization is a render side-effect#
Each Element
is implemented as the constructor function
for a Python class. The Element
constructor function also has
the side-effect of inserting itself to the rendered Element
tree,
as a child of the with
context layout Element.
We like this style of declaring Element trees in Python code because it forces the indentation structure of the Python code to reflect the structure of the Element tree.
Because Element initialization is a render side-effect, we have to be careful about binding Elements to variables and passing them around. They will insert themselves in the tree at the time they are initialized. This code will NOT declare the intended Element tree.
@component
def MySimpleComp(self, prop1, prop2, prop3):
label3 = Label(text=prop3)
with VBoxView():
Label(text=prop1)
Label(text=prop2)
label3 # This will NOT render here as intended
To solve this, defer the construction of the Element with a lambda function. This code will declare the same intended Element tree as the code above.
@component
def MySimpleComp(self, prop1, prop2, prop3):
label3 = lambda: Label(text=prop3)
with VBoxView():
Label(text=prop1)
Label(text=prop2)
label3()
If these component Elements are render functions, then why couldn’t we just write a normal render function with no decorator instead?
# No decorator
def MySimpleComp(prop1, prop2, prop3):
with VBoxView():
Label(text=prop1)
Label(text=prop2)
Label(text=prop3)
The difference is, with the @component
decorator, an
actual Element
object is created,
which means that subsequent renders will be skipped if the props didn’t change.
Also we need an @component
to
be able to use Hooks such as use_state()
, because those are bound to
an Element
.
Rules of Edifice#
Are the same as the Rules of React.
Components and Hooks must be pure
Purity in Components and Hooks is a key rule of Edifice that makes your app predictable, easy to debug, and allows Edifice to automatically optimize your code.
Components must be idempotent – Edifice components are assumed to always return the same output with respect to their inputs – props, state, and context.
Side effects must run outside of render – Side effects should not run in render.
Props and state are immutable – A component’s props and state are immutable snapshots with respect to a single render. Never mutate them directly.
Return values and arguments to Hooks are immutable – Once values are passed to a Hook, you should not modify them. Like props in an Element, values become immutable when passed to a Hook.
Values are immutable after being passed to an Element – Don’t mutate values after they’ve been used in an Element. Move the mutation before the Element is created.