Introduction
QML provides a useful concept to split up code called ‘Components’. The easiest way to create a re-usable component is to add a new file to the working directory of the main QML file.
Example.qml:
import QtQuick 1.0 Rectangle { }
main.qml:
import QtQuick 1.0 Example { }
Components can also be packaged as modules (Qt Components being one such module) and be extended via plugins. This article focuses on how components can be used to write clean and maintainable QML code.
Creating new components
The first example has shown that creating additional components is very easy, so don’t be afraid of making use of that feature.
Don’t only aim for reusability. Aim for encapsulation of implementation details and decoupling of components. Keep components small. Reusability then comes naturally.
Let’s take a look at a simple analog clock example:
download example / view online
main.qml:
import QtQuick 1.0 // Shows current time in analog clock. Rectangle { id: root width: 320 height: 320 property variant now: new Date() Timer { id: clockUpdater interval: 1000 // update clock every second running: true repeat: true onTriggered: { root.now = new Date() } } Clock { id: clock anchors.centerIn: parent hours: root.now.getHours() minutes: root.now.getMinutes() seconds: root.now.getSeconds() } }
Clock.qml:
import QtQuick 1.0 // An analog clock that shows hours, minutes and seconds. Rectangle { id: root width: 262 // acts as minimum width height: 262 // acts as minimum height // public: property int hours: 0 property int minutes: 0 property int seconds: 0 // private: Item { id: impl Image { id: face source: "images/face.png" Image { id: shorthand source: "images/shorthand.png" smooth: true rotation: root.hours * 30 } Image { id: longhand source: "images/longhand.png" smooth: true rotation: root.minutes * 6 } Image { id: thinhand source: "images/thinhand.png" smooth: true rotation: root.seconds * 6 } Image { id: center source: "images/knob.png" } } } }
It contains a clock component and when run, will look like this. Even though it is only used once in the application, it already made sense to split it out from the main file.
Firstly, it keeps the logic in main.qml easy to understand: A timer that updates the hours, minutes and seconds of a clock component. That’s all that a reader of main.qml needs to understand if she plans to extend the functionality there.
Secondly, our Clock component does not have to care about positioning itself in the main view. Consider a Row element using our Clock component n-times. If Clock were to use ‘anchors.fill: parent’ in its root element then we’d be unable to use instances of it in the Row element: Each Clock instance would consume the full width_row, instead of the intended width_row / n. That is also why QML disallows most anchors for elements contained within a Row element. If we want to keep our Clock component reusable, then we cannot make too many assumptions about its usage. Summarized, the root element of a component should neither set anchors to its parent nor should it enforce specific layouts.
The Clock component sets a fixed width and height, though. From a semantical standpoint, it is the default size of our component. It does not restrict consumers of our component, as they can easily change the size on demand. Components with an empty size are treated as invisible element, so having a non-empty default size can avoid silly bugs.
There are other, less obvious benefits, such as the creation of composite elements and the encapsulation of implementation details.
Composing an element – let’s name it ComposedElement – out of simple elements ElementA, ElementB and ElementC makes it easier to add new behaviour. We can add new elements ElementD and ElementE to ComposedElement, without having to change ElementA, ElementB or ElementC. Our simple elements are isolated from each other and thus, will not break easily if one of them changes.
Components can be split into a public and a private part, just as it is known from Java and C++ classes. The public API of a component is the sum of all properties and functions defined in the root item (including inherited properties), meaning that those can be called and controlled from consumers of the component.
Any property or function defined in a nested item (that is, not in the root item) can be seen as entirely private API. This allows encapsulation of implementation details and should become a natural habit when creating components.
To prove the usefulness of encapsulation, we can remove the inner Item element named ‘impl’ from Clock.qml and run the application again (for example, via ‘$ qmlviewer main.qml’). It will show no errors, as the public API of the Clock component was left untouched. This means we are free to change the details of the ‘impl’ item, without any side effect for other components.
We can even extend this idea and let Clock.qml load a specific ‘impl’ element dynamically, depending on the situation. This would introduce the concept of polymorphism to QML, but it is left as an exercise for the reader.
Eventually, having a well designed, minimal public API for each component allows to program to interfaces, not to implementations.
Reusing components
Now let’s see whether Clock.qml is indeed a reusable component. It should be easy to use it for a world clock. In that case, we do not want to show the seconds of the clock. As it turns out, we can slightly modify the behaviour of Clock.qml to not show hours, minutes or seconds whenever their parameter is less than zero. For the Image element with id = thinhand, we can use a new property binding like so:
visible: root.seconds > -1
We have not changed the public API. We also assume that using an analog clock with a negative value for its seconds property didn’t make sense in the first place. That’s why we are reasonably sure that this change will not break the functionality for any existing consumer of our Clock component.
For our world clock example, we might also want to show local weather information. We can use one of the available weather API’s in combination with XmlListModel, which provides a declarative way of data retrieval. The timer that previously only updated the clock’s time will now be used to also refresh the weather data once per hour. Note how a refresh signal was introduced, which is connected to the XmlListModel’s reload function.
The modified example displays world clocks and local weather information for three cities.
The Clock component was trivial to integrate. We disabled the seconds and used UTC hours, together with an offset for the current city. This works because Clock was designed as a simple view. Had the timer been inside the component – instead of exposing hours, minutes and seconds as properties – this could have been more difficult. Sadly, main.qml has grown considerably. The Repeater element adds to the complexity, together with the cities and utcOffsets arrays.
download example / view online
main.qml:
Rectangle { ... property variant cities: ["Berlin", "Helsinki", "San Francisco"] property variant utcOffsets: [1, 2, -8] property variant now: new Date() signal refresh() Timer { ... property int hours onTriggered: { hours = root.now.getHours() root.now = new Date() // Fetch new weather data every hour: if (hours != root.now.getHours()) { root.refresh() } } } Row { anchors.horizontalCenter: parent.horizontalCenter Repeater { model: root.cities // Shows an analog clock with local time and local weather info for a given city. Rectangle { id: current width: 262 // acts as minimum width height: 320 // acts as minimum height property string city: cities[index] property int utcOffset: utcOffsets[index] XmlListModel { id: cityQuery ... } ListView { model: cityQuery anchors.fill: parent delegate: Item { Clock { id: clock anchors.left: parent.left anchors.top: parent.top // Make sure that UTC offset never turns hours negative, // otherwise hours might not be shown: hours: root.now.getUTCHours() + current.utcOffset + 24 minutes: root.now.getMinutes() seconds: -1 } Row { ... Image { id: icon source: "http://www.google.com" + model.iconUrl } Text { id: label text: current.city + ", "+ model.temperature + "°Cn" + model.humidity + "n" + model.windCondition } } } } } } } }
Our aim is to keep main.qml easy to understand. At the same time we do not want to add more complexity to Clock.qml, because then it might be impossible to still use it as a simple analog clock. That’s why we create a new component that is composed of the retrieved weather data and the Clock. It contains the timer, the refresh logic, the XmlListModel and the Clock integration. Instead of maintaining the cities and utcOffsets arrays, we add new public properties to the WeatherWorldClock component:
// public: property string city: "" property int utcOffset: 0
We can remove the arrays and the Repeater in main.qml.
download example / view online
main.qml:
import QtQuick 1.0 // Shows world time and current weather conditions for selected cities. Rectangle { id: root width: 786 height: 320 Row { id: cities anchors.fill: parent anchors.horizontalCenter: parent.horizontalCenter WeatherWorldClock { city: "Berlin" utcOffset: 1 } WeatherWorldClock { city: "Helsinki" utcOffset: 2 } WeatherWorldClock { city: "San Francisco" utcOffset: -8 } } }
The WeatherWorldClock component is isolated from changes in main.qml. We can extend and fix features in either file without having to worry about the other. Should WeatherWorldClock become too complex, we can split it up into more components. Most importantly though, the main logic of our application looks almost trivial now: We initialize WeatherWorldClock components and specify city and UTC offset, and that’s it!
Source Nokia Developer