Asynchronous QtQuick UIs and their implementation: The Toolbox

This blog post goes over the set of tools you have to work with when doing QtQuick UIs that need to perform something asynchronously.

Data Reactivity

Probably the oldest tool in the toolbox, as it's foundational to QML's construction, and one you should already know how to use.

BooksModel {
    id: booksModel
}
Button {
    text: `The model has ${booksModel.count} items`
}
Button {
   onClicked: booksModel.refresh()
}

QML is reactive, meaning that when you change a property, dependent bindings update automatically.

When applied to asynchrony, this means that asynchronous events or requests can simply mutate an object's properties on completion, and the UI will change when it does.

This is the most common form of asynchrony in a QtQuick UI, and it should be your first goto when you need to perform something asynchronously and display results in the UI, as it plays nicely with a declarative UI.

This is generally the right pattern to use when you can describe the asynchronous process as “the UI displays the current state of a model/object, which changes in response to events.”

Thennables

The newest tool in the toolbox (at least in QML), Thennables play nicely with other cool and shiny things, such as C++20 Coroutines.

Button {
    text: "Do it"
    onClicked: getNewButtonTextFromTheInternet().then((it) => this.text = it)
}

Implementations of the Thennable pattern can be found in Qt Automotive, as the QIviPendingReply family of classes, and in Croutons, as the Future family of classes. These allow you to make an object in C++ that you can return as a Thennable to QML after scheduling a task that'll fufill or reject the thennable from the C++ side. On the backend, it usually looks like this:

QIviPendingReply<bool> foo;

// runs asynchronously
client->getStatus([foo](bool isOnline) mutable {
    foo.setSuccess(response);
});

return foo;

Or, if you're using Croutons + C++20 Coroutines...

auto response = co_await client->getStatus();

co_return response.isOnline();

This is generally the right pattern to use when you can describe the flow of asynchronous actions as an “and then” chain. For example, “make a request and then display a notification to the user if it succeeds or fails”. If the asynchronous process can take advantage of data reactivity, this is probably not the correct pattern to use.

Task Objects

This is rarely the right pattern, and people that have to use your API will probably hate you if you ship this in a library. Despite that, it's a (thankfully uncommon) thing in the wild, so I'll document it here.

FileDownload {
    id: fileDownloader

    url: "http://placehold.it/350x150"

    onStarted: status.text = "Starting download..."
    onError: status.text = "Download failed"
    onProgressChanged: status.text = `Download ${progress}% done`
    onFinished: status.text = "Download finished"
}
Button {
	text: "Download File"
	onClicked: fileDownloader.start()
}

This essentially consists of using a full-fledged object description in QML to describe a task, and to connect to signals on it. The object needs to be given an id, and that id needs to be used to invoke a .start() method or setting a running property to true.

Task objects have many disadvantages: – trying to describe an imperative process directly with a declarative language, which goes as well as you'd expect – require creating and binding properties for an extra object, which can add up fast, even if the object's task isn't actually triggered by a user action. e.g., 50 download buttons with corresponding FileDownload objects, and only a few of those buttons will ever get pressed. – non-UI matters in an otherwise UI-only language

You can often recognise task objects by distinct “input” properties and “output” signals (less commonly, “output” properties) for a given task.

Instead of using a task object, considering using a Thennable, or better utilising reactive data.

Workers

I've honestly never seen the WorkerScript type get used in an actual program, so I'm not sure what their practical application is. Despite that, these are another option for currency, so I'll describe their behaviour.

A WorkerScript is a wrapper around the JS notion of a WebWorker, which runs in its own thread and receives and sends messages from the main thread. This is the only way to get data in and out of them, so you have to structure it as messages.

Tags: #libre