Models and Views: List Model using a worker thread for data fetching | Qt Quick (original) (raw)
Demonstrates how to implement a list model with a responsive UI using a worker thread to fetch data.
This example introduces a custom item model, inheriting QAbstractListModel. The model gets its data from a worker object that is in separate QThread, fetching data from a slow data source.
Overview of the Threaded Song List example
The data source simulates a slow data source by adding a delay of 100 milliseconds per song fetched from it. This means that loading of the entire list of 3600 songs would take 6 minutes, making the opening of the application impractical. This delay is mitigated by fetching the data only for the visible area of the view, using a QObject placed into a worker thread.
The worker object has a limit for the number of fetch requests it holds in queue. This ensures that only the elements of the currently visible part of the song list are fetched, removing the need to wait for the non-visible part of the list to load, when user has already scrolled past some part of the list.
Focus of this example is in the source model of the view. The view itself is an unmodified QML ListView with a simple delegate. The use of thread is hidden behind the implementation of model data handling and the ListView does not need to have any customization to be able to adapt to the thread-based model.
Also since the focus is in the model, the Qt Quick Controls is set to use Universal style on all platforms to ensure identical UI behavior.
import QtQuick import QtQuick.Controls.Universal
How it works
The business logic of providing the song list data is separated into DataStorage
class that provides a simple ID-based interface for the model.
QList idList(); MediaElement item(int id) const; std::optional currentlyFetchedId() const;
When model requests data from the DataStorage, the storage will first check if it has the data already available. If it does, data is returned instantly, as would be the case in a non-threaded model. In case the data is not found, DataStorage will emit a dataFetchNeeded()
signal to the worker object and add an empty item to the list of already existing data. Adding the empty item ensures that no further signals are sent to the worker for the same list item.
if (!m_items.contains(id)) { m_items.insert(id, MediaElement{}); emit dataFetchNeeded(m_idList.indexOf(id)); } return m_items.value(id);
QueueWorker
- the worker thread object - processes the dataFetchNeeded() signals it has received by sending a signal to itself, which makes it possible to receive all signals already in QEventQueue before starting the slow data read operation.
Applying the approach to dynamic models
If one wishes to expand the solution towards a case where items may be added, moved or removed from the data source (in this case RemoteMedia), DataStorage needs to be updated with signals to match QAbstractItemModel::rowsMoved(), QAbstractItemModel::rowsInserted() and two signals to trigger the QAbstractItemModel::beginRemoveRows() and QAbstractItemModel::endRemoveRows() inside ThreadedListModel.
For the insertion and move the ThreadedListModel can just call QAbstractItemModel::beginInsertRows(), then add new IDs to its ID list and call QAbstractItemModel::endInsertRows(). As ThreadedListModel holds a copy of the ID list and accesses storage by ID, there is no need to signal the begin and end from storage. Equally ThreadedListModel can call QAbstractItemModel::beginMoveRows(), move IDs in its ID list and then call QAbstractItemModel::endMoveRows().
Removal is a slightly more complex case. The view needs to have a possibility to request the data that is going to be removed before it is actually removed. So DataStorage needs to signal a warning of the removal, causing the Model to call QAbstractItemModel::beginRemoveRows(). At this stage ThreadedListModel may get one or more data()
calls. Once the call to direct connected signal returns at DataStorage, it is OK for DataStorage to remove the item and then signal the model again with another signal that triggers the model to call QAbstractItemModel::endRemoveRows().
Running the Example
To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, see Qt Creator: Tutorial: Build and run.