Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\example models/threadedsonglist
\title Models and Views: List Model using a worker thread for data fetching
\brief Demonstrates how to implement a list model with a responsive UI
using a worker thread to fetch data.
\examplecategory {User Interface Components}
\examplecategory {Data Processing & I/O}
\meta {tag} {quick,threads}
\image qml-threadedsonglist-example.webp {Screenshot of the application
where a songlist with album arts, song names, artist names and album names
are visible}
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.
\section1 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.
\snippet models/threadedsonglist/ThreadedSongList.qml Fixed Controls style
\section1 How it works
The business logic of providing the song list data is separated into
\c DataStorage class that provides a simple ID-based interface for the model.
\snippet models/threadedsonglist/datastorage.h Interface towards the model
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 \c 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.
\snippet models/threadedsonglist/datastorage.cpp Send signal if no data
\c 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.
\section1 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 \c 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().
\include examples-run.qdocinc
*/