Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Building a Responsive RSS News Reader with QML and XmlListModel

Notes May 17 4

UI Layout Demonstration

This article explores the implementation of a standard news client interface using QML. The application features a master-detail layout design, commonly found in desktop and mobile readers. As depicted in the reference design, the interface is split into two distinct areas: a sidebar (or top bar in portrait mode) containing a list of news categories, and a main content area displaying the detailed news feed.

Project Architecture Analysis

The project is organized into six QML files. In addition to utilizing built-in components like BusyIndicator and ScrollBar, the solution introduces custom components for specific UI elements. NewsDelegate acts as the visual template for individual news articles, while CategoryDelegate renders the list items for the category selector. The RssFeeds component supplies the data model for the sidebar, and the main aplication logic is encapsulated in the root QML file.

Main Application Implementation

The following code snippet illlustrates the entry point of the application. The root item manages the window dimensions, screen orientation states, and coordinates the interaction between the list views and data models.

import QtQuick 2.2
import QtQuick.XmlListModel 2.0
import QtQuick.Window 2.1
import "./components"

Rectangle {
    id: appWindow

    width: 800
    height: 480

    property string activeFeedUrl: feedCatalog.get(0).feedSource
    property bool isLoading: newsContentModel.status === XmlListModel.Loading
    property bool isPortraitMode: Screen.primaryOrientation === Qt.PortraitOrientation

    onIsLoadingChanged: {
        if (newsContentModel.status == XmlListModel.Ready)
            newsView.positionViewAtBeginning()
    }

    FeedCatalog { id: feedCatalog }

    XmlListModel {
        id: newsContentModel

        source: "https://" + appWindow.activeFeedUrl
        query: "/rss/channel/item[child::media:content]"
        namespaceDeclarations: "declare namespace media = 'http://search.yahoo.com/mrss/';"

        XmlRole { name: "headline"; query: "title/string()" }
        // Strip HTML anchor tags from the description
        XmlRole { name: "summary"; query: "fn:replace(description/string(), '\<a href=.*\/a\>', '')" }
        XmlRole { name: "thumbnail"; query: "media:content/@url/string()" }
        XmlRole { name: "articleLink"; query: "link/string()" }
        XmlRole { name: "publishDate"; query: "pubDate/string()" }
    }

    ListView {
        id: categoryView
        property int cellWidth: 190

        width: isPortraitMode ? parent.width : cellWidth
        height: isPortraitMode ? cellWidth : parent.height
        orientation: isPortraitMode ? ListView.Horizontal : ListView.Vertical
        anchors.top: parent.top
        model: feedCatalog
        delegate: CategoryDelegate { itemSize: categoryView.cellWidth }
        spacing: 3
    }

    ScrollBar {
        id: categoryScroller
        orientation: isPortraitMode ? Qt.Horizontal : Qt.Vertical
        height: isPortraitMode ? 8 : categoryView.height
        width: isPortraitMode ? categoryView.width : 8
        scrollArea: categoryView
        anchors.right: categoryView.right
    }

    ListView {
        id: newsView

        anchors.left: isPortraitMode ? appWindow.left : categoryView.right
        anchors.right: closeIcon.left
        anchors.top: isPortraitMode ? categoryView.bottom : appWindow.top
        anchors.bottom: appWindow.bottom
        anchors.leftMargin: 30
        anchors.rightMargin: 4
        clip: isPortraitMode
        model: newsContentModel
        footer: footerComponent
        delegate: ArticleDelegate {}
    }

    ScrollBar {
        scrollArea: newsView
        width: 8
        anchors.right: appWindow.right
        anchors.top: isPortraitMode ? categoryView.bottom : appWindow.top
        anchors.bottom: appWindow.bottom
    }

    Component {
        id: footerComponent

        Rectangle {
            width: parent.width
            height: closeIcon.height
            color: "#e0e0e0"

            Text {
                text: "RSS Feed from Yahoo News"
                anchors.centerIn: parent
                font.pixelSize: 14
            }
        }
    }

    Image {
        id: closeIcon
        source: "assets/images/close_btn.png"
        scale: 0.8
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.margins: 4
        opacity: (isPortraitMode && categoryView.moving) ? 0.2 : 1.0
        Behavior on opacity {
            NumberAnimation { duration: 300; easing.type: Easing.OutSine }
        }

        MouseArea {
            anchors.fill: parent
            onClicked: Qt.quit()
        }
    }
}

News Item Delegate

The ArticleDelegate component defines the visual structure for a single entry in the news feed. It includes logic to calculate relative timestamps (e.g., converting a publication date to "2 hours ago") and handles the layout of the thumbnail, headline, metadata, and body text.

import QtQuick 2.2

Column {
    id: articleRoot
    width: articleRoot.ListView.view.width
    spacing: 8

    function getRelativeTime(dateString) {
        var formatted = dateString;
        var parts = dateString.replace(',','').split(' ');
        
        if (parts.length === 6) {
            var eventDate = new Date([parts[0], parts[2], parts[1], parts[3], parts[4], 'GMT' + parts[5]].join(' '));

            if (!isNaN(eventDate.getDate())) {
                var diffMs = new Date() - eventDate;
                var totalMinutes = Math.floor(Number(diffMs) / 60000);
                
                if (totalMinutes < 1440) {
                    if (totalMinutes < 2)
                        formatted = qsTr("Just now");
                    else if (totalMinutes < 60)
                        formatted = totalMinutes + ' ' + qsTr("minutes ago")
                    else if (totalMinutes < 120)
                        formatted = qsTr("1 hour ago");
                    else
                        formatted = Math.floor(totalMinutes/60) + ' ' + qsTr("hours ago");
                } else {
                    formatted = eventDate.toDateString();
                }
            }
        }
        return formatted;
    }

    Item { height: 8; width: articleRoot.width }

    Row {
        width: parent.width
        spacing: 8

        Column {
            Item { width: 4; height: headerText.font.pixelSize / 4 }

            Image {
                id: newsImage
                source: thumbnail
            }
        }

        Text {
            id: headerText
            text: headline
            width: articleRoot.width - newsImage.width
            wrapMode: Text.WordWrap
            font.pixelSize: 26
            font.bold: true
        }
    }

    Text {
        width: articleRoot.width
        font.pixelSize: 12
        textFormat: Text.RichText
        font.italic: true
        text: getRelativeTime(publishDate) + " (<a articlelink="" href="\""">Link</a>)"
        onLinkActivated: Qt.openUrlExternally(articleLink)
    }

    Text {
        id: bodyText
        text: summary
        width: parent.width
        wrapMode: Text.WordWrap
        font.pixelSize: 14
        textFormat: Text.StyledText
        horizontalAlignment: Qt.AlignLeft
    }
}

Technical Comparison: Declarative vs. Imperative UI

Working with QML offers a distinct departure from traditional C++ widget development. The declarative nature of QML allows developers to focus solely on the UI structure and behavior without the boilerplate code required for compilation in imperative languages. Features like elastic list efffects, native loading states, and remote resource handling are significantly more streamlined. However, the flexibility comes with trade-offs. Since bindings between models and delegates often rely on string-based property names, typos or missing fields are not caught at compile time. This necessitates a more rigorous runtime testing strategy compared to the strict type-checking found in C++.

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.