A scalable application is an application that can run on more than one form factor. In particular, it can cope with different screen sizes, DPI, and aspect ratios. You need to consider scalability when:
- your application will be deployed to more than one device handset, or more than one device form factor.
- your application will be deployed for a long period of time, so that new device handsets might appear on the market after your initial deployment.
This document discusses how scalable applications can be created.
Developing Scalable UIs
This section shows the basics of how we advice scalable applications to be implemented using QML. We recommend that you follow these techniques:
- Create separate top-level layout definitions for each form factor.
- Keep the layouts small and let components scale relative to their immediate parent.
- Define device independent measurements, such as dp (device independent pixels), and use these to scale components and for layout measurement.
- Define layouts in a proportional way using the built-in layout features of QML.
Using small top-level layouts makes your codebase smaller and easier to maintain. Also, components that scales relative to their parent are more reusable. The layouts should be children of the application's root item. You can change between them by, for instance, using the opacity property of Item; that is, if your application has more tham one top-level layout. Such a top-level layout is also often referred to as a page, i.e., a layout that uses the entire screen. For instance, an organizer application will typically have separate pages for showing the calender and editing todo items.
QML provides several ways of laying out components, e.g, using anchor based layout, the more classic Grid; Column; and Row elements, and by setting the dimensions of Items directly. When laying out components in scalable applications, you should generally prefer using anchors and set width and height based on parent size where possible. Layouts are not only relevant to top-level layouts; components often contain child Items.
The following sections describe in more detail the different aspects of scalability that should be considered in order to achieve the desired level of flexibility within your application.
Implementing the Top-Level Layouts
As mentioned, each application should use separate top-level layout QML definitions to support separate layout configurations / form factors.
Consider an application that has to be deployed to at least two devices, which both have very different screen sizes and DPI values. The two form factors of the application will share many common components and attributes, and will most likely connect to the same data model.
Therefore, the top-level definitions should be quite straightforward and short, with the majority of the functionality refactored into contained Components. It is important to try to avoid unnecessary duplication between these top-level definitions, in order to improve maintainability.
There are some patterns that you might consider when designing your top level layouts:
- In some cases, the contents of an entire page in a smaller handset could form a component element of a layout in a larger device. Therefore, consider making that a separate component (i.e. defined in a separate QML file), and in the smaller handset, the Page will simply contain an instance of that component. On the larger device, there may be enough space to show two separate items. For example, in an email viewer, if the screen is large enough, it may be possible to show the email list view, and the email reader view side by side.
- In some cases, the contents of a view might be quite similar on all screen sizes, but with an expanded content area. In this case, it may be possible to re-use the same layout definition, if defined appropriately using anchors.
The Loader component can be used to load separate QML files based on some criteria, such as Device Profile (configuration of screen pixel resolution and DPI density). In the case of form factor, this information will not change during the application's lifetime, therefore there is no issue with memory usage or performance.
When you are defining the measurements within an application or component layout, there are a number aspects to consider:
- The layout structure, the high-level relationship between items. Which item is the parent? How are the items arranged relatively on the screen? Are they in a grid or column?
- The layout measurements. How big is an item, or a margin inside the edge of an item, or an anchor between items?
- The implicit size of contained items. Some child items will require a certain amount of space, such as a button containing a text. That may also depend on the current platform and style. How do you ensure that you leave enough space, and what happens if your children change size?
These aspects combine together to resolve the final layout for a given Device Profile. However, although there are dependencies between them, it is important to manage and control the different aspects separately.
It is strongly recommended that Layout measurements should be stored in a separate place from the component layout structure definition files. The reason for this is that layout structure, for a given form factor, can be re-used for different Device Profiles. However, measurements will almost always vary between Device Profiles or Device Categories.
If the opposite approach (complete duplication of entire QML files) was taken, then all of the layout states and structure definitions would be duplicated between the copied QML files, and only the measurement values would change.
The main benefit of using separate measurement definition files are:
- To reduce the amount of duplication, and hence increase maintainability.
- It becomes much easier to change the layout structure, perhaps due to subsequent specification changes. In that case, the layout structure can be modified once, and many or all of the layout measurements would remain unchanged.
- It becomes much easier to add support for additional Device Profiles, simply by adding another measurement definition file.
Using QML's Layout Features
For a given form factor, top-level Layouts structure definitions, or component layout structure definitions, should in general be defined in a proportional way using a combination of
- anchors within an Item
- Row / Column / Grid
There are some limitations of the basic grid type layouts. They are designed to accommodate a number of Items, but use the current sizes of those items. There is a similar issue with the basic anchor type layout. In particular, it can be difficult to spread a number of child items proportionately across an area of their container.
Here are some things not to do with layouts:
- Don't define all of your layouts using x, y, width and height. Reserve this for items that cannot easily be defined using anchors (anchors are evaluated in a more efficient way).
- Don't make assumptions about the container size, or about the size of child items. Try to make flexible layout definitions that can absorb changes in the available space.
Application top-level page definitions, and reusable component definitions, should use one QML layout definition for the layout structure. This single definition should include the layout design for separate Device Orientations and Aspect Ratios. The reason for this is that performance during an orientation switch is critical, and it is therefore a good idea to ensure that all of the components needed by both orientations are loaded when the orientation changes.
On the contrary, you should perform thorough tests if you choose to use a Loader to load additional QML that is needed in separate orientations, as this will affect the performance of the orientation change.
In order to enable layout animations between the orientations, the anchor definitions must reside within the same containing component. Therefore the structure of a page or a component should consist of a common set of child components, a common set of anchor definitions, and a collection of states (defined in a StateGroup) representing the different aspect ratios supported by the component. (However note that orientation change animations are not possible on Symbian due to compatibility support for S60 applications).
If a component contained within a page needs to be hosted in numerous different form factor definitions, then the layout states of the view should depend on the aspect ratio of the page (its immediate container). Similarly, different instances of a component might be situated within numerous different containers in a UI, and so its layout states should be determined by the aspect ratio of its parent. The conclusion is that layout states should always follow the aspect ratio of the direct container (not the "orientation" of the current device screen).
There are a few additional cases to consider:
- What if you have a single page that looks completely different between landscape and portrait, i.e. all of the child items are different? For each page, have two child components, with separate layout definitions, and make one or other of the items have zero opacity in each state. You can use a cross-fade animation by simply applying a NumberAnimation transition to the opacity.
- What if you have a single page that shares 30% or more of the same layout contents between portrait and landscape? In that case, consider having one component with landscape and portrait states, and a collection of separate child items whose opacity (or position) depends on the orientation state. This will enable you to use layout animations for the items that are shared between the orientations, whilst the other items are either faded in/out, or animated on/off screen.
- What if you have two pages on a handheld device that need to be on screen at the same time, for example on a larger form factor device? In this case, notice that your view component will no longer be occupying the full screen. Therefore it's important to remember in all components (in particular, list delegate items) should depend on the size of the containing component width, not on the screen width. It may be necessary to set the width in a Component.onCompleted() handler in this case, to ensure that the list item delegate has been constructed before the value is set.
- What if the two orientations take up too much memory to have them both in memory at once? Use a Loader if necessary, if you cannot keep both versions of the view in memory at once, but beware performance on the cross-fade animation during layout switch. One solution could be to have two "splash screen" items that are children of the Page, then you cross fade between those during rotation. Then you can use a Loader to load another child component that loads the actual model data to another child Item, and cross-fade to that when the Loader has completed.