qtqml

How to pre-load heavy qml pages and later push them to StackView


I have a QtQuick StackView application that involves some quite heavy pages. I want to pre-load some of the heaviest pages during app startup to reduce lag and wait time later on.

For reference, the heaviest page involves several backend calls, many visual elements, a few loaders, and buttons who's text and functionality depends on a parameter passed to the page.

I have tried to set up a minimal application that achieves the same result: This app has a loading screen for while everything is set up (and ideally pre-loading heavy pages), and a landing screen from where the user can navigate to the heavy screen.

Main.qml - Set up StackView

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    width: 640
    height: 480
    visible: true
    title: qsTr("Pre-loading stack view application")

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: LoadingScreen {}
    }
}

LoadingScreen.qml - Display this while everything is loaded, then move to landing screen

import QtQuick 2.15
import QtQuick.Controls 2.15
import "LoadingFunction.js" as LoadingFunction

Rectangle {
    id: loadingScreen
    width: stackView.width
    height: stackView.height

    Rectangle {
        anchors.fill: parent
        color: "lightgray"
        Text {
            anchors.centerIn: parent
            text: "Loading..."
        }
    }

    // Do some loading from database..
    property double dbLoadTime: LoadingFunction.simulateLoadTime(loadingScreen)

    // Pre-load heavy page
    Loader {
        id: preloadHeavy
        asynchronous: true
        source: "HeavyScreen.qml"
        visible: false
        onLoaded: {
            console.log("HeavyScreen preloaded.");
        }
    }

    Component.onCompleted: {
        stackView.replace("LandingPage.qml");
    }
}

LandingPage.qml - The application landing page

import QtQuick 2.15
import QtQuick.Controls 2.15

Rectangle {
    id: landingPage
    width: stackView.width
    height: stackView.height

    Column {
        anchors.centerIn: parent
        spacing: 10

        Text {
            text: "Landing Page"
            font.pointSize: 20
        }

        Button {
            text: "Go to Heavy Screen"
            onClicked: {
                stackView.push("HeavyScreen.qml", {displayString: "Hello from Landing Page"})
            }
        }
    }
}

HeavyScreen.qml - I want this to be pre-loaded (and also not destroyed on pop so that it can be re-opened immediately under different configurations based on displayString)

import QtQuick 2.15
import QtQuick.Controls 2.15
import "LoadingFunction.js" as LoadingFunction

Rectangle {
    id: heavyScreen
    width: stackView.width
    height: stackView.height

    property string displayString
    property double dbLoadTime: LoadingFunction.simulateLoadTime(heavyScreen)

    Column {
        anchors.centerIn: parent

        Text {
            text: "Your string: " + displayString
        }

        Button{
            text: "Back"
            onClicked: stackView.pop()
        }

    }
}

LoadingFunction.js - to simulate heavy components

function simulateLoadTime(parent) {
    let randomTime = 3000 + Math.floor(Math.random() * 2000); // 3000-4999 ms
    console.log("Simulated load time:", randomTime, "ms");

    Qt.createQmlObject(`
        import QtQuick 2.15
        Timer {
            interval: ${randomTime};
            repeat: false;
            running: true;
            onTriggered: {
                console.log("Load completed after ${randomTime} ms");
                destroy();
            }
        }
    `, parent, "SimulatedLoadTimer");

    return randomTime
}

Upon starting the application the following outputs are immediately printed to the console and the app goes to the landing screen.

qml: Simulated load time: 4960 ms
qml: Simulated load time: 3236 ms
qml: HeavyScreen preloaded.

After sitting on the landing page for a while the following is printed to console.

qml: Load completed after 3236 ms
qml: Load completed after 4960 ms

This seems to mean that the loading is not completed before moving to the landing page even though the Loader's onLoaded message is printed. Why is this happening?

Additionally, after clicking 'Go to heavy screen' from the landing page the following is printed to console.

qml: Simulated load time: 3550 ms
qml: Load completed after 3550 ms

This indicates that the pre-loaded page is not being used since the loading function in the heavy page is getting called again. How can I make sure to push the pre-loaded page to the StackView?


Solution

  • How can I make sure to push the pre-loaded page to the StackView?

    You can bind a property to the dynamically loaded item to push the preloaded item :

    LoadingScreen.qml :

    Component.onCompleted: { 
        stackView.replace("LandingPage.qml",
        {"rec": Qt.binding(function() { return preloadHeavy.item;})})
    }
    

    LandingPage.qml :

    Rectangle {
        id: landingPage
        property Rectangle rec // add a Rectangle property
        width: stackView.width
        height: stackView.height
    
        Column {
            anchors.centerIn: parent
            spacing: 10
    
            Text {
                text: "Landing Page"
                font.pointSize: 20
            }
    
            Button {
                text: "Go to Heavy Screen"
                onClicked: {
                    // push the property bound to the dynamically loaded item                
                    stackView.push(rec, {displayString: "Hello from Landing Page"})
                }
            }
        }
    }