Building a Responsive RSS News Reader with QML and XmlListModel
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++.