Relational State & Qt

While developing Challah (Qt chat app for Harmony protocol), I ran into situations where I needed to reference the same data in a reactive manner from multiple locations. In usual QtQuick fashion, you usually only have one one model per view, and views only ever get data from their assigned model or the surrounding context. This meant that in order to expose the same data in multiple locations, you had to wire signals between multiple models in order to synchronise their signal changes. This takes a lot of boilerplate to do and is extremely prone to error. I simply accepted this as something that you needed to do when using QtQuick.

My friend, Blusk, who is working on our web client caught this almost immediately when we were discussing how we implemented our clients with our respective UI frameworks (me QtQuick, him Vue.)

He was like “wtf, you don't have relational data/state in Qt? HOW” when I explained my struggles with the above problem.

I asked what relational state, and he gave a very long-winded explanation. In short, relational state is this:

using DataID = quint64;

struct Data {
    QString foo;
    QString bar;
}
struct Model {
    QList<DataID> data;
}
struct Store {
    QMap<DataID,Data> data;
}

instead of this:

struct Model {
    QList<Data> data;
}

This essentially involves splitting the Model into two parts: the Model and the Store. The Model is simply a list of IDs, while the Store maps IDs to data. Consumers can reactively listen to updates to the value of any key from the Store.

This technique is used to great effect in many JS frameworks, where you can simply bind to a map's key and any UI components will update as the map is updated.

This allows putting data into a single store and referring to it from multiple places.

Examples include:

We lack an equivalent to that in Qt land, as we really only have reactive lists and reactive trees with the QAbstractItemModel hierarchy of classes.

Despite all the advantages of relational state, I didn't investigate using it for Challah because the code had already been written with the usual Qt list models & Harmony protocol is mostly fine w/out relational state.

That would have been the end of relational data and Qt for me.

And then I started Tok, a Kirigami Telegram client.

I started out writing Tok w/ the usual approach that I take for models and Qt, but then I quickly realised something: Telegram offers data in an aggressively relational manner.

Instead returning message data when you ask for chat history, Telegram simply returns a list of message IDs. Where do you get the message content? You receive a bunch of events asking you to place given messages in a local store. The events of Telegram's API can essentially be broken down into two types:

This is basically Telegram telling you that you should use QList<ID> and QMap<ID,Data>. Problem is, while QList has a reactive sibling, the QAbstractListModel, the other half of the equation, the QMap, does not.

Enter QAbstractRelationalModel

I was like “hmm, this hole in capability needs to be fixed in Qt's models collection.”

However, due to Qt5 being dead to new features and Qt6 being a ways off to being usable for a KF/Kirigami app, I had to write something for Tok to use for now. So I did.

First things first, I added a README explaining why the classes were named as if they were official Qt types.

these are named as if they were Qt classes due to an intent to submit them to Qt for Qt6.

Simple enough, though I should probably clarify that I'm using Tok as a sort of testing ground for them.

Now for the code itself.

QAbstractRelationalModel is an extremely simple interface, designed to adapt the feel of the QAbstractItemModel to a key/value offering. Since QAbstractItemModel only works for lists and trees, I made QAbstractRelationalModel a subclass of QObject and not QAbstractItemModel.

Then, I defined methods for the API:

virtual QVariant data(const QVariant& key, int role = Qt::DisplayRole) const = 0;

This is QAbstractRelationalModel's equivalent to the QAbstractItemModel's equivalently data function. Instead of taking a parent/row/column tuple (QModelIndex), QAbstractRelationalModel takes a QVariant in order to represent many types of keys with the same interface.

virtual bool checkKey(const QVariant& key) const = 0;

This is the equivalent of checkIndex from QAbstractItemModel. Give it a key, and it tells you if that key present in the model.

virtual bool canFetchKey(const QVariant& key);
virtual bool fetchKey(const QVariant& key);

These are the equivalent of canFetchMore and fetchMore from the QAbstractItemModel. However, instead of simply being for appending data to the model, these methods let you query whether or not any given key can be fetched.

virtual QHash<int, QByteArray> roleNames();

This shouldn't need any explanation. This works exactly how it does in QAbstractItemModel.

That leaves us with the data reactivity part. In Qt, data reactivity is done through signals. A set of three signals is enough to suffice for our needs:

void keyAdded(const QVariant& key);
void keyRemoved(const QVariant& key);
void keyDataChanged(const QVariant& key, const QVector<int>& roles);

These are called to notify you about Create/Update/Delete changes to data in the model.

Usage: QQmlRelationalListener

Now we need something to use it. Since Tok is a QtQuick application, I wrote a QML component that allows listening to a key provided by a QAbstractRelationalModel.

Usage is fairly simple and looks like this:

RelationalListener {
    id: messageData
    key: delegate.messageID
    shape: QtObject {
        required property string messageContent
        required property string messageAuthorID
    }
}
QQC2.Label {
    text: messageData.data.messageContent
}

Easy peasy.

The implementation of this listener isn't really as interesting as the model itself, so I won't go too into depth here. You can look at its source on invent.kde.org.

The most interesting thing here is probably the shape property. It's essentially the component that the listener instantiates and utilises to expose data to the user.

This takes advantage of the “new” required property syntax in Qt 5.15, which allows Qt to loudly abort the application when a programming error is made instead of silently yielding pesky undefineds.

You may also be confused as to how I said that was a component, as to the user, it looks like instantiating a QtObject in QML. Simply enough, the QML engine allows you to use T {} instead of Component { T { } } for properties of the QQmlComponent* type.

Usage in Tok

I spent most of today and yesterday porting Tok from plain QAbstractItemModels to QAbstractItemModels + QAbstractRelationalModels. This basically meant porting the messages and the user data to a model/store architecture. I quickly noticed that my code felt much more elegant: changes to data were entirely separated from changes to different views on that data, e.g. changing user data used by both the messages view and a user list view, or changing message data used both by a message replying to it and the message itself. The models themselves also were reduced in complexity: messages model only has to worry about ordering and adding/removing IDs from the list as Telegram dictates, and the messages store only has to worry about adding and notifiying about updates from Telegram. No more “when message changed, try to locate the child model for the chat that that message belongs to and post the event there if it's present, otherwise don't.” It's simply “when message received, change the message store.”

Conclusion

After using relational data, I strongly agree with my friend's bewildered reaction to me saying there was nothing like relational state in Qt. Many things that used to be “how do” or mistake-prone boilerplate hells for me now have readily apparent solutions for me now.

Contact Me

If you have any thoughts on this post, feel free to share them w/ me in #chat:kde.org or https://t.me/kdechat, or by DMing me at @pontaoski on Telegram or @pontaoski:kde.org on Matrix.