Why you should use Qt/QML for your next cross-platform application — part 2 — mobile — Todo app example

part 1 — Desktop
part 2 — Mobile
part 3 — TV

In the previous article we touched in general on why you should use Qt/QML for app development and provided a bit of code to show what one can expect.

In this one we are going to develop one of the most used examples — a TODO app — running on multiple platforms.

Although I’m not really a fan of such an app showing a possibility of some framework, it’s still something people are used to for getting a quick taste of an framework.

For many more and better examples please visit official docs!

UI mockup

First off we have to think about how our app is structured. In general we talk about structuring UI components. This is nothing new and here we take into consideration that you have experience with UI development and UI frameworks so this is something you are used to.

Since this is very simple app we create our simple mock-up:

Our Todo app mock-up

We can immediately see the layout and components making our app.

There is one input field and one button to add new items. These will be positioned next to each other.
Then we have one component to represent the data we added. This is going to be a (scrollable) list of our tasks where each can be considered as one component containing again one input field and one button.

QML app setup

As mentioned in the previous article you can find examples on how to setup the environment in the official docs.

Once you have your environment ready we are going to create our app window.

In this examples we expect Qt 5.12 LTS (you can use older versions, but then you need to lower the import version used in these examples i.e. use QtQuick 2.9).

This is nothing different than what is done in the official docs, so this is where we start:

import QtQuick 2.12
import QtQuick.Window 2.12
Window { visible: true
width: 480
height: 640
color: "#333333"
title: qsTr("Todo")
}

We went just a bit ahead here but lets explain it. The Window component has a color attribute. This we can use to set a background color. By default is white, so we changed it to a dark grey. Note that you are perfectly free to write “grey” “black” or using a rgb value as explained in detail https://doc.qt.io/qt-5/qml-color.html.

qsTr is used as part of localization. Although that is outside of the scope of this tutorial, feel free to visit linked documentation to get a better understanding. Here we are showing it as there is no harm in doing it, so it’s always good to use it even if you don’t plan to support multiple languages at the beginning, since this will probably happen down the road.

Now if you run this on your PC you get something like:

Our Todo app in action

Wait, PC?

Yes! So even if we are writing mobile app for Android/iOS, the benefit is that we can run exactly the same app on our PC, be it Linux, Windows or MacOS.

The width/height attributes will be automatically adapted by default to match the size of your mobile device when it is ran, while on PC it will use the values we provided.

This way, even if we target only mobile apps, we can quickly prototype, test and develop without having to test it on the real device or emulator.

Read more about deploying Qt apps for different platforms by visiting official docs.

QMLScene

So even if you have your app setup ready there is even easier way to test a piece of code and see how it would look like.

QMLScene is a binary which you can use to create QML apps just by writing QML in a file — as always, see official docs for great examples.

As an example, putting this in an app.qml:

import QtQuick 2.12    
import QtQuick.Controls 2.12
TextField {
placeholderText: qsTr("New")
onTextChanged: text === "quit" && Qt.quit()
}

and running it as qmlscene app.qml will result in:

A working application with a text field

And writing “quit” will close the application.

UI components

We know that we have an input field and a button which will enable us to add new content.

QML comes with a big set of available controls which we can use for that purpose.

When using QML controls check that you use version 2 https://doc.qt.io/qt-5/qtquickcontrols-index.html as you might get confused with older version which has similar components

Input field and add button

In this case we are interested in the TextField and RoundButton. For a much bigger list and what they can do, see official docs.

Reading the docs of both, it’s pretty easy to see how these are used.

To use them, we first import them:

import QtQuick.Controls 2.12

Now we can just write:

TextField {
placeholderText: qsTr("New")
}

It’s obvious but this will create a text field which has a placeholder text inside: “New”.

We also want a button:

RoundButton {
text: "+"
}

This one is probably even more self explanatory.

Scrollable list

The last and most interesting component is our scrollable list for which we are going to use a ListView.

Now, this one is where the real work starts due to the fact that list needs to show some data. As part of that, one needs a model to store our data and render it. This could be just a plain array which was fetched from the server, but QML also provides us with an efficient non visual component, ListModel.

This ListModel can be used for different things but ListView is perfectly capable of using it to render the data that the ListModel contains.

To read more about possible models visit official docs on the topic.

So in this case we would create something like:

ListModel { id: todoModel }ListView {
model: todoModel
}

But to actually better present what is happening lets link the official docs on the topic:

QML — model, view, delegate

Simply put, applications need to form data and display the data. Qt Quick has the notion of models, views, and delegates to display data. They modularize the visualization of data in order to give the developer or designer control over the different aspects of the data. A developer can swap a list view with a grid view with little changes to the data. Similarly, encapsulating an instance of the data in a delegate allows the developer to dictate how to present or handle the data.

To continue, we will actually manage the data in the model, while rendering happens automatically with provided Delegate which holds visual representation.

When we add or update data in our ListModel with one of the methods like .append, .insert or .set, this will be automatically rendered without additional logic for the delegate.

Layouting

In QML, there are actually multiple ways on how to position elements. As always, please visit official docs on the topic — Important Concepts In Qt Quick — Positioning.

In this example we are going to use Layouts. They are specially designed for responsive interfaces, and when you get a hang of them they are really great to work with. After many years of working with all ways of elements positioning in QML, Layouts are the ones that I always go to when things get complex. They also need less code in general due to sane defaults and are easy to understand by just noticing them in the code without having to read into details.

So lets extend our example to show that one can immediately understand what will happen:

RowLayout {    
TextField {
placeholderText: qsTr("New")
}
RoundButton {
text: "+"
}
}

It’s easy to explain that we will get a text field next to a button (in a same row) without even reading properties set on each field.

Layouts are also convenient because they have defaults which are suited for creating elements in interfaces. For example, spacing is already included (which can be changed with “spacing” property) and they do automatic vertical/horizontal centering.

Combining it all together

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
Window { visible: true
width: 480
height: 640
color: "#333333"
title: qsTr("Todo")
// prevent window resize below these values
minimumWidth: 200
minimumHeight: 200
// model to manage our data
ListModel { id: todoModel }
// show children in a column (one below another)
// this will show input field and button above
// and scrolling list below
ColumnLayout {
anchors.fill: parent
anchors.margins: 15
// show children in a row (one next to each other)
RowLayout {
Layout.fillWidth: true
spacing: 15
// show this above our todoList view
z: 1
TextField {
id: todoInputField
placeholderText: qsTr("New")
Layout.fillWidth: true
}
RoundButton {
text: "\u2795" // unicode heavy plus sign
onClicked: {
// add data to our model
todoModel.append({ content: todoInputField.text })
todoInputField.text = ''
}
}
}
ListView {
id: todoList
// use the model we defined above to render list
model: todoModel
Layout.fillWidth: true
Layout.fillHeight: true
delegate: ColumnLayout {
width: todoList.width
height: 55
RowLayout {
Layout.fillWidth: true
spacing: 15
TextField {
color: "#fff"
background: Item {}
// render data from model
text: model.content
Layout.fillWidth: true
}
RoundButton {
text: "\u2796" // unicode heavy minus sign
// remove this element from model
onClicked: todoModel.remove(model.index)
height: parent.height
}
}
Rectangle { color: "#666"; height: 1; Layout.fillWidth: true }
}
}
}
}

Which results in:

So we put a bit more but let’s take it bit by bit as there is nothing special here.

We use a ColumnLayout to position our elements on top of each other. So we treat input field and button as one element — wrapped in RowLayout so they are positioned next to each other— and a ListView as another element.

We set some properties on all of them but they are all pretty straightforward. z:1 is the only one that probably needs more explanation and as always official docs will get you covered.

Just keep in mind that z order is by default set in the order of how you defined your items for rendering. So in this case remove the z property to see the behavior:

ListView overlapping the input field and button

Which is not what we want. Now we could use a clip property and put a clip:true to not allow ListView to render outside it’s boundaries but this has some performance implications so it’s much better to set a proper z order.

So with an increase of z:1 we get our wanted behavior:

ListView overlapped by the input field and button

Now the interesting thing starts to happen with TextField which we identified with an id property todoInputField. This enables us to easily access this component and every method that is exposed.

So we use it simply when clicking the button:

onClicked: {
// add data to our model
todoModel.append({ content: todoInputField.text })
todoInputField.text = ''
}

First we push data to our model by connecting logic to onClicked signal of the RoundButton component. Immediately we can start writing Javascript code which also has access to our todoInputField(see QML scoping) so after we added data we also clear the text field.

The other interesting things happen in the ListView. When we provide the model to the ListView it tries to render the data by rendering the component in the delegate for each item in the ListModel.

Whenever there is an item added, removed or updated in the ListModel, the ListView will get the information and update the view accordingly.

Now how do we actually access the data in the delegate from the model?

Well, that’s very simple. When we put some data in the ListModel we create it with some properties, in our case {content: "something"} .

This will automatically enable usage of this keyword in the delegate. So just trying to use content property will work. However, to make it more clear, using model.content will also work and makes no issue with the scoping (you might have a content property in some of the outer components which you also want to access).

Using array instead of a ListModel you will need to write modelData.content

Deployment

So we have our app running and working on a PC so we would like to do it for our phone.

This topic is outside of the scope of the app, just because it is again greatly covered by official docs so repeating it here is not very useful and could get outdated in the future.

Follow instructions for Android, iOS or any other supported platform.

Here we are running it on android:

Our app running on android phone

Conclusion

Qt docs are amazing! If you skipped through most of the linked docs (I don’t blame you, we all do it!) you might have a lot of questions now.

However, reading docs on each of the topics will give you a great understanding. If this is too much or you like to take it step by step, then I again recommend official docs for getting started with QML and/or https://qmlbook.github.io.

In this example we just scratched the surface. We would still want to store this tasks and enable proper modification after storing. We can store it locally by using LocalStorage or by using a familiar XMLHttpRequest to make network request to some API to even sync cross platforms.

It would also be convenient to support keys so pressing Enter/Return would also add a new task. Thankfully this is very easy by using Keys property.

But in the end it’s just a small intro to QML and real development starts after it! Happy coding!

You can get the full source code of the app on github

Stay tuned for the Part-3 in which we tackle (Android)TV development!

Developer by day, architect at night — never satisfied

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store