Architecture of the app: QML vs C++
Problem statement
For my liking, there is "too much" going on in JavaScript in the QML code.
Right now the QML code basically is the app, and C++ bits are simply "along for the ride". I.e. the QML code is in control of how various transitions happen: everything from navigation to whether or not to accept forms is ultimately decided on in the QML view layer. C++ code is merely used as a grab bag library of handy bits which would otherwise be very awkward to implement in pure QML.
It is simple, it sort of works but even in its current state the code is already clearly too poorly structure in order to be able to write unit tests for the app logic. Similarly even for such a relatively simple app there is quite a bit of state that needs to be maintained in the view already.
Proposal
I propose we should rework the app to keep logic in C++, and keep the QML as 'dumb' as possible. I would propose that at the top level (pages) each QML page is implemented as a set of 3 or so types:
- The QML page view itself (per page)
- A C++ Actions wrapper type (per page) which provide the necessary logical actions that can be invoked on a page. This does not comprise trivial show/hide view level logic, but rather things like "create this new account with these settings", or "delete this account" or "request a new token for this account" etc. It is worth emphasising that this does also include navigation actions such as "show the add a new account page" or "show the account details page for this account".
- A C++ Model wrapper type (per page) which aggregates data values that the page must render (display)
Types 2 and 3 would be exposed as non-creatable types to the QML layer, and provisioned as properties on the page. Instances would be created (and their lifecycle managed) only from the C++ side, including setting up necessary signal/slot connections to other C++ components to handle invoked "actions" or to propagate data/value changes in the model.
Example (pseudo code)
For a given MyPage.qml
this would look like the following pseudo-code:
my-page/actions.{h,cpp}:
Q_DECLARE_METATYPE(mypage::Actions)
class Actions: public QObject
{
Q_OBJECT:
public:
Q_INVOKEABLE void doSomethingWithAccount(Account *account)
{
Q_EMIT doSomethingWith(account);
};
Q_SIGNALS:
void doSomethingWith(Account *account);
};
my-page/model.{h,cpp}:
class Model: public QObject
{
Q_OBJECT
Q_PRROPERTY(Acount *account ...)
}
contents/ui/MyPage.qml:
import ...
MyPage {
id: root
property Model model
property Actions actions
property Account myAccount: model.account
Button {
onClicked: {
actions.doSomethingWithAccount(root.myAccount)
}
}
}
There core idea is the the QML code becomes a fairly pure and declarative layer which simply computes the "right" view for a given model and wires up events/handlers to logical "actions" which are invoked.
All the real logic is pushed all the way through to C++ where we can make more informed decisions, which will reach the QML layer eventually through property bindings to the "model" objects.
Wiring up the main application
This just leaves "wiring up" the main application.
At this point we need to work with Kirigami's PageRow
which appears to be entirely written in QML/JavaScript. This will be a bit less elegant but it can be solved e.g. by having our KirigamiApplication (main.qml
) implementation listen for signals from a dedicated C++ component and translate these to manipulations on the pageStack
property (push()
, pop()
) as needed.
The main difficulty here is determining which page to push if applicable, and that difficulty stems from using Component
loaders (meaning page objects come and go). We could even solve that by introducing some kind of static map of static keys to loaders (Component {}
) and signalling the page type/id as part of the push()
/pop()
signals emitted from the previously mentioned C++ component.
Additional stuff
We also may want some kind of Store
/Container
type to aggregate the various models and actions, and facilitate lookup on a per page/page type basis. This would be convenient whenever a new page needs to be pushed with the correct properties.
More generally we may well wind up with a bunch of additional C++ utility classes/types which implement behaviours to support more complex interaction patterns in QML. The main goal is to implement logic in C++, not just business logic (primarily) because it is easier to bring things under test in pure C++ code and also easier to maintain a separation of concerns in C++. However for complex interaction patterns such "behaviours" could just as well first be prototyped in JavaScript before ultimately being migrated to C++ once we are satisfied the basic concept can work and we want it to be tested more thoroughly or implemented more cleanly.
Hang on this looks familiar
Yes. It's highly similar to how certain popular web frontend (Single Page Application) frameworks and libraries "do things".
- The
Model
bits will be highly familiar to anyone who has seen the manyvm
variables in Angular code bases. - The
Actions
bits will be highly familiar to anyone who has worked with Redux (action creators). In fact the combination of these actions and property bindings to the model mean the data flow ends up highly reminiscent of the Flux/Redux/Elm family of architectures and, at the QML layer, or Functional Reactive Programming more generally. However, note that I do not propose to rebuild Redux or enforce functional purity in the C++ domain. - The QML is very much like an Angular template. However, note that this analog breaks down when considering Angular filters/pipes (formatters/pretty printers/i18n).
- The QML also bears some resemblance to React Components: being purely declarative and using a "funny" custom DSL syntax (QML vs JSX) by convention. However, note that this likeness is rather more superficial. In particular I don't intend for a true analog to things like Higher Order Components, property spreading or similar React patterns.