In this article, we will detail how to create a basic RSS reader from scratch using our Ubuntu SDK. After we complete this exercise, we will be familiar with the development process of Ubuntu applications.

Note: See the article “How to turn on Developer mode in Ubuntu Phones” in the emulator to turn on developer mode so you can deploy your application to the emulator. If we want to deploy our apps on our phones, we need to do the same thing on our phones.

\

  \

\

Our entire application will look like the image above. Let’s get started right now.

\

1) Install your SDK

\

We knew we couldn’t develop our app without the SDK. We can refer to the article “Ubuntu SDK Installation” to install our environment on our Ubuntu Desktop.

\

\

2) Create a basic application framework

\

First, let’s open our Qt Creator to create a project called “RssReader”. We use the “QtQuick App with QML UI (Qmake)” template. \

\

\

 

 \

\

Note the maintainer format here. If there is a red error, check to see if there is a space left to the left of the <.

To make it look more like a mobile phone, we set the dimensions in “main. QML” as follows: \

    width: units.gu(60)
    height: units.gu(85)
Copy the code

Resolution independent: An important feature of Ubuntu’s UI toolkit is to match user defined device sizes. The approach taken is to define a new cell type, grid cell (gu for short). Grid units convert to pixel values depending on the type of application running on the screen and device. Here are some examples:

Device Conversion
Most laptops 1 gu = 8 px
Retina laptops 1 gu = 16 px
Smart phones 1 gu = 18 px

More information about resolution independence can be found at the link. Developers can also refer to my article “How to Get screen and available display area size and Use resolution-independent programming”. \

We first select “Ubuntu SDK Desktop Kit” and we can click on the green Run button at the bottom left of the SDK screen, or use the hot key (Ctrl +R) to run the application. As shown below: \

\

\

In our application, some code is C++ code. Let’s ignore this code for now. Let’s focus directly on our QML file. These are the files that make up our UI. The original application is nothing. You can change the text in the box by pressing a button. Let’s start designing our application. \

\

2) Remove code we don’t need

\

Because the original code is actually not much use for us to come to the book. Let’s now modify our code: 1) Remove unwanted code in “main.qml” so that the code looks like this: \

\

\

2) Change the page title to “POCO photography”. Rerun our application:

\

\

3) Add a PageStack

\

PageStack allows us to push one Page onto another. It tracks the changes to those pages and automatically provides a “back” button that takes me back to the previous page. Now let’s use PageStack to redesign our application. Replace the entire Page code in “main.qml” with the following code: \

\

Import QtQuick 2.4 import Ubuntu.Components 1.3 MainView {// objectName for Functional Testing Purposes (autopilot-qt5)  objectName: "mainView" // Note! applicationName needs to match the "name" field of the click manifest applicationName: "rssreader.liu-xiao-guo" /* This property enables the application to change orientation when the device is rotated. The default is false. */ //automaticOrientation: true width: units.gu(60) height: units.gu(85) PageStack { id: pageStack anchors.fill: parent Component.onCompleted: {console.log('pagestack created') pagestack.push (listPage)} Page {id: listPage title: i18n.tr("POCO photography ")}}Copy the code

Here, we can see that an Event event onCompleted is called after each Component is loaded. We can use this method to initialize what we need to do next. Here, we push the listPage onto the stack even though there’s nothing there. If we rerun the program, we will see no new changes in the interface. This is because we don’t have any data in the page. We will find the following Output in the “Application Output” window: \

\

qml: pagestack created
Copy the code


\

\

\

This shows that our code is running successfully. \

\

\

4) Add our own controls

\

We will add a new QML control. The name of this control is “ArticleGridView”. It will be defined in a file called “articlegridview.qml”. Control names usually start with a capital letter. \

First we create a directory called “Components” in the project directory (do either of the following) : \

\

  \

\

  \

\

  \

\

  \

\

We right-click the project and add a file named “articlegridview.qml”. And put the file in the “Components” directory. By default, “articegridview.qml” does not define anything other than a box. Let’s add what we need to it. \

\

1) Change the code in “articlegridview.qml” into the following code:

\

ArticleGridView.qml

Import QtQuick 2.4 import QtQuick. XmlListModel 2.0 import 1.3 import Ubuntu.Com Ubuntu.Com ponents ponents. ListItems 1.0 Item { id: root signal clicked(var instance) anchors.fill: parent function reload() { console.log('reloading') model.clear(); pocoRssModel.reload() } ListModel { id: model } XmlListModel { id: picoRssModel source: "http://www.8kmm.com/rss/rss.aspx" query: "/rss/channel/item" onStatusChanged: { if (status === XmlListModel.Ready) { for (var i = 0; i < count; i++) { // Let's extract the image var m, urls = [], str = get(i).content, rex = /<img[^>]+src\s*=\s*['"]([^'"]+)['"][^>]*>/g; while ( m = rex.exec( str ) ) { urls.push( m[1] ); } var image = urls[0]; var title = get(i).title.toLowerCase(); var published = get(i).published.toLowerCase(); var content = get(i).content.toLowerCase(); var word = input.text.toLowerCase(); if ( (title ! == undefined && title.indexOf( word) > -1 ) || (published ! == undefined && published.indexOf( word ) > -1) || (content ! == undefined && content.indexOf( word ) > -1) ) { model.append({"title": get(i).title, "published": get(i).published, "content": get(i).content, "image": image }) } } } } XmlRole { name: "title"; query: "title/string()" } XmlRole { name: "published"; query: "pubDate/string()" } XmlRole { name: "content"; query: "description/string()" } } GridView { id: gridview width: parent.width height: parent.height - inputcontainer.height clip: true cellWidth: parent.width/2 cellHeight: cellWidth + units.gu(1) x: Units.gu (1.2) Model: Model Delegate: GridDelegate {} Scrollbar {flickableItem: gridview } } Row { id:inputcontainer anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter height: units.gu(5) spacing:12 Icon { width: height height: parent.height name: "search" anchors.verticalCenter:parent.verticalCenter; } TextField {id:input placeholderText: "width:units.gu(25) text:"" onTextChanged: { console.log("text is changed"); reload(); }}}}Copy the code

\

\

To display each of our GridView items, we also add the Griddelegate.qml file to the same components directory:

\

GridDelegate.qml

\

Import QtQuick 2.0 Item {width: parent. Width /2 * 0.9 height: width Image {anchors. Fill: parent anchors. CenterIn: parent source: image fillMode: Image.PreserveAspectCrop MouseArea { anchors.fill: parent onClicked: { root.clicked(model); } } Text { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottomMargin: Unit.gu (1) horizontalAlignment: Text.AlignHCenter Text: {return title.replace("[POCO photography - portrait] : ", ""); } clip: true color: "white" font.pixelSize: units.gu(2) } } }Copy the code

\

\

So here we’re using a GridView. It needs a Model to provide the data to display. This Model data can be provided by the ListModel or XmlListModel in QML. The “delegate” is used to show how each item in the list is displayed. The width of each grid is half the screen width times 0.9. The height and width are the same. Each grid displays a picture and a text on the picture. As a developer, we can design the content of each of our grid displays any way we want. It all depends on the designer. We can take a look at the content of our RSS feed’s address:

\

\

\

\

5) Use ArticleGridView

\

We designed our own Component in the previous section. In this section, we’ll use it just like any other control we already have. Let’s put our designed ArticleListView into our designed “main.qml”. Add the following Page to “listPage” in your main.qml :\

\

           ArticleGridView {
                id: articleList
                objectName: "articleList"
                anchors.fill: parent
                clip: true
            }
Copy the code

\

Also don’t forget to add the following sentence at the beginning of “main.qml” : \

\

import "components"  
Copy the code

If we run our application at this time, we will find an error:

\

qrc:///Main.qml:3:1: "components": no such directory
Copy the code

\

The reason for this error is that our main.qml exists in a file called rssreader.qrc. This can be seen in the main. CPP file.

\

main.cpp

\

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    view.setSource(QUrl(QStringLiteral("qrc:///Main.qml")));
    view.setResizeMode(QQuickView::SizeRootObjectToView);
    view.show();
    return app.exec();
}
Copy the code

QML is in a Qt resource file, so we have to import the QML file we just created into our QRC file (rssreader.qrc). In the left frame of the project, right-click on “rssReader.qrc” and click “Add Existing Directory”.

\

   

\

\

\

After importing our file, our rssReader.qrc file structure is as follows (if you can’t see the updated file, you can run the project, or close the project and reopen the project) :

\

\

\

In fact, the format of our QRC file is an XML file:

\

rssreader.qrc

<RCC>
    <qresource prefix="/">
        <file>Main.qml</file>
        <file>components/GridDelegate.qml</file>
        <file>components/ArticleGridView.qml</file>
        <file>components/ArticleContent.qml</file>
        <file>components/ArticleListView.qml</file>
        <file>components/ListDelegate.qml</file>
        <file>components/images/arrow.png</file>
        <file>components/images/rss.png</file>
    </qresource>
</RCC>
Copy the code

We can even edit it directly using a file editor if we want. For more information about Qt Resource, see “The Qt Resource System”.

\

To re-run our application (Ubuntu SDK Desktop Kit) :

\

\

\

\

6) Create a new Component

\

Like the ArticleGridView we created above, let’s create a new ArticleContent component. The file name for this component is “articlecontent.qml”. The document is located in the same components as the ArticleGridView. Let’s add what we need to the newly created Component. Open the file “component/ArticleContent. QML”, and enter the following code:

ArticleContent.qml

Import QtQuick 2.0 import Ubuntu.Components 1.3 Item {property alias text: content.text Flickable {id: flickableContent anchors.fill: parent Text { id: content textFormat: Text.RichText wrapMode: Text.WordWrap width: parent.width } contentWidth: parent.width contentHeight: content.height clip: true } Scrollbar { flickableItem: flickableContent } }Copy the code


   \

\

Similarly, we need to add our newly created articleconten.qml to our rssReader.qrc file:

\

\

\

7) Connect ArticleContent to the rest of the app

\

So far, we have created an ArticleContent control. We can use it in our application. Each time an item in the ArticleListView is clicked, we can use it to display the detailed content. First, we must generate a signal when each item is clicked in ArticleGridView and connect this signal to the action we need to generate. We can define a signal called “clicked”. 1) Open the “articlegridview.qml” file and check signal:\

\

 signal clicked(var instance)
Copy the code

\

2) In our “griddelegate.qml” file, look at the following code:

\

MouseArea { anchors.fill: parent onClicked: { root.clicked(model); }}Copy the code

The code above shows that when we click on each grid we send a signal called clicked. This signal can be captured in the code below and connected to a slot. This is also the concept behind Qt’s very powerful signal slots.

\

3) Use the ArticleContent control we have created. We create a new Page in the “main. QML “file and use PageStack\

\

Main.qml

\

Import QtQuick 2.4 import Ubuntu.Components 1.3 import "components" MainView {// objectName for functional testing purposes (autopilot-qt5) objectName: "mainView" // Note! applicationName needs to match the "name" field of the click manifest applicationName: "rssreader.liu-xiao-guo" width: units.gu(60) height: units.gu(85) PageStack { id: pageStack anchors.fill: parent Component.onCompleted: {console.log('pagestack created') pagestack.push (listPage)} Page {id: listPage title: i18n.tr("POCO photography ") visible: false head.actions: [ Action { iconName: "reload" text: "Reload" onTriggered: articleList.reload() } ] ArticleGridView { id: articleList anchors.fill: parent clip: true onClicked: { console.log('[flat] article clicked: '+instance.title) articleContent.text = instance.content pageStack.push(contentPage) } } } Page { id: contentPage title: i18n.tr("Content") visible: false ArticleContent { id: articleContent objectName: "articleContent" anchors.fill: parent } } } Action { id: reloadAction text: "Reload" iconName: "reload" onTriggered: articleList.reload() } }Copy the code

When we click on each item in the grid, we pass:

\

               onClicked: {
                    console.log('[flat] article clicked: '+instance.title)
                    articleContent.text = instance.content
                    pageStack.push(contentPage)
                }
Copy the code

To capture the event and switch to another page called contentPage. \

When we run the program, we see the following image. We can click the items in the main interface and view the specific content :\

\

    \

\

We can also deploy it on our phones and it will look like this:

\

\

\

We chose “Ubuntu SDK for ARMHF (GCC for Ubuntu-SDK-15.04-Vivid)” (developers can choose according to the version of ARMHF they have installed on their computer). The following information is displayed:

\

   \

\

At this point, we have completed one of our most basic exercises. The entire project source code at the address: github.com/liu-xiao-gu…

\

\

8) Use UbuntuListView to display our content

\

In this section, we’ll use UbuntuListView to display our content. As the name suggests, a ListView is used to display a List. UbuntuListView actually inherits from Qt’s ListView, but it also has some properties of its own. Similar to the above method, we create articlelistView.qml and listdelegate.qml:

ArticleListView.qml

Import QtQuick 2.4 import QtQuick. XmlListModel 2.0 import 1.3 import Ubuntu.Com Ubuntu.Com ponents ponents. ListItems 1.0 Item { id: root signal clicked(var instance) anchors.fill: parent function reload() { console.log('reloading') model.clear(); picoRssModel.reload() } ListModel { id: model } XmlListModel { id: picoRssModel source: "http://www.8kmm.com/rss/rss.aspx" query: "/rss/channel/item" onStatusChanged: { if (status === XmlListModel.Ready) { for (var i = 0; i < count; i++) { // Let's extract the image var m, urls = [], str = get(i).content, rex = /<img[^>]+src\s*=\s*['"]([^'"]+)['"][^>]*>/g; while ( m = rex.exec( str ) ) { urls.push( m[1] ); } var image = urls[0]; var title = get(i).title.toLowerCase(); var published = get(i).published.toLowerCase(); var content = get(i).content.toLowerCase(); var word = input.text.toLowerCase(); if ( (title ! == undefined && title.indexOf( word) > -1 ) || (published ! == undefined && published.indexOf( word ) > -1) || (content ! == undefined && content.indexOf( word ) > -1) ) { model.append({"title": get(i).title, "published": get(i).published, "content": get(i).content, "image": image }) } } } } XmlRole { name: "title"; query: "title/string()" } XmlRole { name: "published"; query: "pubDate/string()" } XmlRole { name: "content"; query: "description/string()" } } UbuntuListView { id: listView width: parent.width height: parent.height - inputcontainer.height clip: true visible: true model: model delegate: ListDelegate {} // Define a highlight with customized movement between items. Component { id: highlightBar Rectangle { width: 200; height: 50 color: "#FFFF88" y: listView.currentItem.y; Behavior on y { SpringAnimation { spring: 2; Damping: 0.1}}}} highlightFollowsCurrentItem: true focus: true / / highlight: highlightBar Scrollbar { flickableItem: listView } PullToRefresh { onRefresh: { reload() } } } Row { id:inputcontainer anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter height: units.gu(5) spacing:12 Icon { width: height height: parent.height name: "search" anchors.verticalCenter:parent.verticalCenter; } TextField {id:input placeholderText: "width:units.gu(25) text:"" onTextChanged: { console.log("text is changed"); reload(); }}}}Copy the code

\

ListDelegate.qml

\

Import QtQuick 2.0 Item {width: listview.view. width height: units.gu(14) Row {spacing: units.gu(1) width: Width height: parent. Height X: units.gu(0.2) Image {id: img property int borderLength: 2 source: Image height: parent.height *.9 width: height anchors.verticalCenter: parent.verticalCenter } Column { id: right y: LeftMargin: Units.gu (1) anchors. LeftMargin: Units.gu (0.1) width: parent-width-img.width-parent. Units. Gu (0.2) Text {Text: {var TXT = published. Replace ("GMT", ""); return txt; } font.pixelSize: units.gu(2) font.bold: true } Text { width: parent.width * .9 text: {var TMP = title.replace("[POCO photography - human] : ", ""); if ( tmp.length > 35) return tmp.substring(0, 35) + "..." ; else return tmp; } // wrapMode: Text.Wrap clip: true font.pixelSize: units.gu(2) } } } Image { source: "images/arrow.png" anchors.right: Parent. Right anchors. VerticalCenter: Parent. VerticalCenter anchors. RightMargin: Units. -90 } MouseArea { anchors.fill: parent onClicked: { console.log("it is clicked"); console.log("currentidex: " + listView.currentIndex); console.log("index: " + index); listView.currentIndex = index; root.clicked(model); } } Keys.onReturnPressed: { console.log("Enter is pressed!" ); listView.currentIndex = index; root.clicked(model); }}Copy the code

Careful developers may have noticed that the documents in our articlelistview.qml are almost identical to the content in our articlegridview.qml. We just used the UbuntuListView method instead of the GridView method to display our content in this file. This is in line with our MODEL-View-Control design. Finally, don’t forget to add our newly created file to our rssReader.qrc file. Otherwise, these files will not be recognized. In addition, we need to modify the listPage section in main.qml:

\

Main.qml

\

Page {id: listPage title: i18n.tr("POCO photography ") visible: false head.actions: [Action {iconName: "reload" text: "Reload" onTriggered: articleList.reload() } ] ArticleListView { id: articleList anchors.fill: parent clip: true onClicked: { console.log('[flat] article clicked: '+instance.title) articleContent.text = instance.content pageStack.push(contentPage) } } }Copy the code

\

We used ArticleListView instead of the previous ArticleGridView. Rerun our application:

\

 



The entire project source code:Github.com/liu-xiao-gu…

\

\

9) Add cache to the application to make the UI smoother

\

We find that when we open an image and return to our previous page, there are new web requests. Images will be re-downloaded. On the one hand, it wastes traffic and makes the user experience worse. In our application, we can add a cache mechanism to avoid unnecessary repeated downloads. We mainly modify for main.cpp:

\

main.cpp

\

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>
#include <QStandardPaths>
#include <QDebug>
#include <QDir>
#include <QQmlNetworkAccessManagerFactory>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>QString getCachePath() { QString writablePath = QStandardPaths:: writableLocation(QStandardPaths::DataLocation); qDebug() << "writablePath: " << writablePath; QString absolutePath = QDir(writablePath).absolutePath(); qDebug() << "absoluePath: " << absolutePath; absolutePath += "/cache/"; // We need to make sure we have the path for storage QDir dir(absolutePath); if ( dir.mkpath(absolutePath) ) { qDebug() << "Successfully created the path!" ; } else { qDebug() << "Fails to create the path!" ; } qDebug() << "cache path: " << absolutePath; return absolutePath; } class MyNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { public: virtual QNetworkAccessManager *create(QObject *parent); }; QNetworkAccessManager *MyNetworkAccessManagerFactory::create(QObject *parent) { QNetworkAccessManager *nam = new QNetworkAccessManager(parent); QString path = getCachePath(); QNetworkDiskCache* cache = new QNetworkDiskCache(parent); cache->setCacheDirectory(path); nam->setCache(cache); return nam; } int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQuickView view; qDebug() << "Original factory: " << view.engine()->networkAccessManagerFactory(); qDebug() << "Original manager: " << view.engine()->networkAccessManager(); QNetworkDiskCache* cache = (QNetworkDiskCache*)view.engine()->networkAccessManager()->cache(); qDebug() << "Original manager cache: " << cache; view.engine()->setNetworkAccessManagerFactory(new MyNetworkAccessManagerFactory); view.setSource(QUrl(QStringLiteral("qrc:///Main.qml"))); view.setResizeMode(QQuickView::SizeRootObjectToView); view.show(); return app.exec(); }Copy the code

\

Run our app on mobile:

\

   \

The entire project source at: github.com/liu-xiao-gu…

\

When you’re done with this exercise, we can move on to “convergence design for dynamic layout using AdaptivePageLayout.”

\

\