QML API best practices

As you continue developing apps with the QML API from The Qt Company, you should know a few details that can make your apps perform better and encounter fewer problems. This topic discusses important details that you should keep in mind.

API Memory Model

The memory model used for QML apps is based on garbage collection. Unreferenced variables will be cleaned up by the QML Engine garbage collector. Therefore, all objects must be explicitly referenced to avoid garbage collection when they are still in use.

Object creation using JavaScript

For creating ArcGIS Runtime objects in JavaScript code, the ArcGISRuntime.createObject() method, and the clone() method available on certain objects, create and return new instances of an object back through the QML Engine. The engine will handle garbage collection of these new instances. If you need to keep a reference of these returned instances for layer use, store them in a variable so they are properly reference counted, thus avoiding unexpected garbage collection. For example:

property var storedPoint
property var storedSMS

Point {
    id: point
    x: 200
    y: -10
    spatialReference: SpatialReference {
        wkid: 4326
    }
}

...

var newPoint = point.clone();
storedPoint = newPoint; // store to persist object

var sms = ArcGISRuntime.createObject("SimpleMarkerSymbol", {size: 60, xOffset: 15, yOffset: 15, xAnchor: 15, yAnchor: 15});
storedSMS = sms; // store to persist object

Also, placing these new instances directly into a QML ListModel component does not reference count the instance. You must also store the returned instance in a property. For example:

var newPoint = point.clone();

myListModel.append(
    {"geometry", newPoint
    });  // will not ref count newPoint

myListModel.geoms.push(newPoint);  // prevent object from being garbage collected

...

ListModel {
  id: myListModel
  property var geoms: []
}

Working with models

Models will access objects implicitly. When working with models it is necessary to provide persisted data for the model to access. Although you can directly assign a list to a model as an input, it should be avoided because the model will create non-explicit references which will be garbage collected. For example:

GeodatabaseFeatureTable {
    id: featureTable
}

ComboBox {
    id: comboBox
    model: featureTable.fields // this should be avoided.
    textRole: "name"
}

For this to work correctly you must first assign all the list elements to a local list. These explicit references will persist and avoid early garbage collection. The following example builds a list component and inserts all the fields from the feature table. Then, it sets the list as the model of the comboBox.

var list: []
function updateModel() {
    for (var i = 0; i < featureTable.fields.length; ++i) {
        list.push(featureTable.fields[i]);
    }
    comboBox.model = list;
}

Object uniqueness

There is no guarantee that you will get the same object back even for the same getter or function call. As such, direct object comparison should be avoided. Here is an example.

Map { id: map }
...
var extent = map.extent;
// Avoid comparisons like the following code.
// Although the extent contents are identical, 
// the extent objects may be different objects.
if (extent === map.extent)

Custom/User properties

Although currently the following example may work for some objects, there is no guarantee that this functionality will be supported in a future release. This should be avoided.

Envelope {
    id: envelope
    property string userProperty: "some custom info" // this should be avoided
}

Custom user-provided properties may be stripped when retrieving the object from a getter. In this case if you access envelope.userProperty you can access the property, but it will not work in the following scenario:

map.extent = envelope;
var customPropertyString = map.extent.userProperty; // this will not work.

Chained assignments

It is common to get properties within properties by using dot notation "chains". For example, it is appropriate to access the minimum x-coordinate for a map's extent using map.extent.xMin.

However, using chained dot notation with property assignments will often not behave as expected. This is especially true for, but not limited to, Geometry objects. The following code will not work as expected:

Map {
    id: map
    ArcGISTiledMapServiceLayer {
        url: "http://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer"
    }
    onStatusChanged: {
        if (status === Enums.MapStatusReady) {
            map.extent.xMin = -13456407.770174276; // does not work
            map.extent.yMin = 3700589.272833122;   // does not work
            map.extent.xMax = -12478013.133102087; // does not work
            map.extent.yMax = 4189786.591369216;   // does not work
        }
    }
}

Instead, reassign the extent property using a new, fully-populated extent component, as follows:

Map {
    id: map
    ArcGISTiledMapServiceLayer {
        url: "http://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer"
    }
    onStatusChanged: {
        if (status === Enums.MapStatusReady) {
            var extent = map.extent;
            extent.xMin = -13456407.770174276;
            extent.yMin = 3700589.272833122;
            extent.xMax = -12478013.133102087;
            extent.yMax = 4189786.591369216;
            map.extent = extent;
        }
    }
}

Working with JSON

In order to simplify working with certain ArcGIS Runtime SDK for Qt QML API types, many API methods accept as valid input either an API object or a JSON representation of the API object. Methods with feature, graphic, geometry or symbols parameters have this capability. For example, the addGraphic() method on the GraphicsLayer accepts either a Graphic object or a JSON representation of a graphic. The following example creates a Graphic object through JSON and passes it into a GraphicsLayer.

GraphicsLayer {
    id: gl
}

function addGraphic() {
    var graphicJson = {
        "geometry": {
            "x": -75.237,
            "y": 72.891,
            "spatialReference": gl.spatialReference
        },
        "symbol": {
            "color":[255,0,0,255],
            "size":25,
            "style":"esriSMSCircle",
            "type":"esriSMS",
        }
    }
    
    // add the JSON representation of the graphic to the graphics layer
    gl.addGraphic(graphicJson);
}

Using JSON.stringify()

When using the JSON.stringify() method, avoid passing in the object reference itself. Use the json property of the object as the argument. For example: JSON.stringify(graphic.geometry.json)