Singletons

Design Pattern: Singletons

The Singleton design pattern is a useful software design pattern for Qt/QML applications that need access to certain services or logic-heavy backend components.

My favorite explanation of the problem solved by the singleton design pattern:

The application needs one, and only one, instance of an object. Additionally, lazy initialization and global access are necessary.
By SourceMaking.

Method 1: Exposing a Qt C++ Class as a QML Singleton

Qt's preferred way to expose a Qt C++ Class as a QML Singleton is by using qmlRegisterSingletonType which is fairly well documented.

This method can be used to register a singleton provider (basically an instance() method with a different signature) of a certain class to a uri which can then be imported in QML through a QQuickExtensionPlugin.

The singleton provider, or instance method, is called by the QQmlEngine when the singleton is first used in QML – and not when the import is first used.

Because of this behavior, it is very difficult to precisely control the instantiation of the singleton object.

In this example, we are using a singleton called Theme to build up a custom Text element:

import QtQuick 2.7
import QmlGuide 1.0

Text {
    font.pixelSize: Theme.adjustedFontSize(24)
    font.family: Theme.fontFamily
}

The singleton called Theme is under the QmlGuide 1.0 import. This import is where we want to register our Theme class:

qmlRegisterSingletonType("QmlGuide", 1, 0, "Theme", Theme::singletonProvider);

The last argument in the qmlRegisterSingletonType macro is a callback, defined as QObject *(*callback)(QQmlEngine *, QJSEngine *).

This callback is invoked the first time the Theme singleton is used, i.e. in the example above, the Theme::singletonProvider callback is executed when font.pixelSize is set to Theme.adjustedFontSize(24).

In this example, the Theme::singletonProvider callback would never be executed, and therefore, the Theme object would never be created, even though import QmlGuide 1.0 is specified:

import QtQuick 2.7
import QmlGuide 1.0

Text {

}

This can potentially lead to dangerous code follies, as the simplest case of a developer logging the Theme object can create the singleton:

import QtQuick 2.7
import QmlGuide 1.0

Text {
    Component.onCompleted: {
        console.log("Theme: ", Theme);
    }
}

Generally, once a developer sees the Theme singleton being logged in the example above – they will automatically assume the Theme object is being initialized correctly. However, once the console.log statement is removed, the Theme object is no longer initialized.

For most users, this behavior of extreme lazy instantiation is perfectly okay. For those users who are focused on performance optimization or application start up times, may find this a PITA.

Method 2: Using the QmlEngine's root context to set context properties

This method counters method #1 in stating clearly that:

I do not want the QmlEngine to manage my singletons – I will manage them myself.

QmlContext allows us to manipulate the context hierarchy of a tree of elements within a QmlEngine.

This feature can be used to add global properties to all QML files within a single QmlEngine. This allows us to replicate the behavior of a singleton by exposing a QmlContext property through the QmlEngine::rootContext

In the Theme class, we can define a registerSingleton(..) method:


...

void Theme::registerSingleton(QQmlEngine *qmlEngine)
{
    if (!s_instance) {
        s_instance = new Theme(qmlEngine);
    }
    QQmlContext *rootContext = qmlEngine->rootContext();
    rootContext->setContextProperty("Theme", s_instance);
}

...

Now, in main.cpp, we can invoke this method by passing in a reference to the QQmlEngine object:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    Theme::registerSingleton(&engine);

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

Now, we can use the Theme object in QML:


...

Text {
    anchors.centerIn: parent
    font.pixelSize: 42
    font.family: Theme.fontFamily

    text: "Hello World"
}
    
...

Note that the import QmlGuide 1.0 statement is no longer necessary as the Theme object was registered to the rootContext of the QQmlEngine which means it is available in all QML files that are built by this QQmlEngine.

As can be seen, the statement:

Theme::registerSingleton(&engine);

can be invoked in any order the developer sees fit. This helps when tuning an application for startup and even for runtime performance. If a QML developer tries to use the Theme object before it is instantiated, a warning will be output. And as always, warnings guide developers between good practices and evil ones.

Sample code is available in the qml.guide GitHub repository.