Buscando APIs com Qt

Nessa ultimas duas semanas eu estava aprendendo como buscar APIs com Qt and eu finalmente consegui fazer alguma coisa funcional! Nesse post a gente vai criar uma pequena aplicação que busca informação sobre um usuário especifico do github.


Primeiras etapas

Eu vou estar usando esse tutorial Kirigami como um ponto de partida.

Primeiro a gente vai criar dois arquivos src/controller.h e src/controller.cpp e adicionar src/controller.cpp no arquivo src/CMakeLists.txt:

src/CMakeLists.txt

add_executable(... controller.cpp ...)

src/controller.h

#include <QObject>
#include <QNetworkAccessManager>

class Controller : public QObject
{
    Q_OBJECT

public:
    Q_INVOKABLE void fetch(QString username);

private:
    QNetworkAccessManager m_manager;
};

No arquivo de cabeçalho temos essa classe Controller que herda do QObject, ela tem um QNetworkAccessManager como membro que nós vamos utilizar para fazer requisições e uma função fetch que recebe um usuário do github como parametro. A macro Q_INVOKABLE é usado para que possamos chamar essa função pelo QML.

src/controller.cpp

#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>

#include "controller.h"

void Controller::fetch(QString username)
{
    if (username.isEmpty()) {
        qWarning() << "Username must not be empty";
        return;
    }

    QNetworkRequest request = QNetworkRequest(QString("https://api.github.com/users/%1").arg(username));
    QNetworkReply* reply = m_manager.get(request);

    QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
        if (reply->error()) {
            qWarning() << "Error fetching URL";
            qWarning() << reply->errorString();
        } else {
            // read data
            QString replyText = reply->readAll();

            // ask doc to parse it
            QJsonDocument document = QJsonDocument::fromJson(replyText.toUtf8());

            // we know first element in file is object, to try to ask for such
            QJsonObject object = document.object();

            qDebug() << object;
        }
        reply->deleteLater(); // make sure to clean up
    });
}

No arquivo .cpp primeiro verificamos se username está vazio, se sim mostramos um aviso e retornamos.

Depois nós criamos a request passando a URL da API do github como argumento pro QNetworkRequest e usamos uma QString para guardar o username.

Então a gente cria uma reply usando um metodo de um membro da nossa classe que a gente declarou antes, o metodo get(), que recebe a request que a gente acabou de criar como argumento e retorna uma QNetworkReply.

Então nós conectamos á um sinal do QNetworkReply que diz pra gente que está tudo pronto, verificamos se não tem um erro, se tiver mostramos um aviso e a mensagem de erro, se não tem nenhum erro nós continuamos.

Vamos guardar tudo que veio da requisição em replyData como uma QString e então transformar tudo em um QJsonDocument e guardar isso em document. O primeiro elemento da API do github é um objeto então a gente pode acessar esse objeto com document.object();

Se a gente printar isso vamos ter uma coisa tipo essa:

QJsonObject(
    {
        "avatar_url":"https://avatars.githubusercontent.com/u/52990296?v=4",
        "bio":"My KDE's gitlab profile is way more interesting",
        "blog":"fhek.gitlab.io",
        "company":"@KDE",
        "created_at":"2019-07-17T05:13:51Z",
        "email":null,
        "events_url":"https://api.github.com/users/FHEK789/events{/privacy}",
        "followers":13,
        "followers_url":"https://api.github.com/users/FHEK789/followers",
        "following":4,
        "following_url":"https://api.github.com/users/FHEK789/following{/other_user}",
        "gists_url":"https://api.github.com/users/FHEK789/gists{/gist_id}",
        "gravatar_id":"",
        "hireable":true,
        "html_url":"https://github.com/FHEK789",
        "id":52990296,
        "location":"Brazil",
        "login":"FHEK789",
        "name":"Felipe Kinoshita",
        "node_id":"MDQ6VXNlcjUyOTkwMjk2",
        "organizations_url":"https://api.github.com/users/FHEK789/orgs",
        "public_gists":0,
        "public_repos":3,
        "received_events_url":"https://api.github.com/users/FHEK789/received_events",
        "repos_url":"https://api.github.com/users/FHEK789/repos",
        "site_admin":false,
        "starred_url":"https://api.github.com/users/FHEK789/starred{/owner}{/repo}",
        "subscriptions_url":"https://api.github.com/users/FHEK789/subscriptions",
        "twitter_username":null,
        "type":"User",
        "updated_at":"2021-12-17T17:47:02Z",
        "url":"https://api.github.com/users/FHEK789"
    }
)

Eba! Nossa primeira requisação usando Qt!


Criando algumas propriedades

Agora vamos criar algumas propriedades para guardar os valores que a gente quer do documento JSON, por enquanto vamos só guardar o nome do usuário:

src/controller.h

...

class Controller : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name NOTIFY nameChanged)

    ...

Q_SIGNALS:
    void nameChanged();

private:
    QNetworkAccessManager m_manager;

    QString m_name;
};

A gente usa essa macro Q_PROPERTY pra criar propriedades, que funcionam como membros mas com algumas funções adicionais, você pode ler mais sobre isso aqui, então a gente faz nosso sinal e membro guardar o nome do usuário.

src/controller.cpp

void Controller::fetch(QString username)
{
    ...

    QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
        if (reply->error()) {
            qWarning() << "Error fetching URL";
            qWarning() << reply->errorString();
        } else {
            // read data
            QString replyText = reply->readAll();

            // ask doc to parse it
            QJsonDocument document = QJsonDocument::fromJson(replyText.toUtf8());

            // we know first element in file is object, to try to ask for such
            QJsonObject object = document.object();

            // ask object for value
            QJsonValue name = object.value(QString("name"));
            m_name = name.toString();
            Q_EMIT nameChanged();
        }
        reply->deleteLater(); // make sure to clean up
    });
}

Agora invés de pegar todos os dados, vamos pedir apenas pelo nome do usuário. Nós podemos acessar esse valor usando essa notação .value(), e então definir m_name como uma versão string do valor em JSON e emitor o sinal nameChanged para que nossa aplicação saiba que nossa propriedade mudou.

Vamos repetir esse processo pra guardar todos os dados que a gente quer, que vai se parecer com alguma coisa tipo essa:

src/controller.h

...

class Controller : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString login READ login NOTIFY loginChanged)
    Q_PROPERTY(QString name READ name NOTIFY nameChanged)
    Q_PROPERTY(QString bio READ bio NOTIFY bioChanged)
    Q_PROPERTY(QString avatarURL READ avatarURL NOTIFY avatarURLChanged)
    Q_PROPERTY(QUrl profileURL READ profileURL NOTIFY profileURLChanged)

    ...

Q_SIGNALS:
    void loginChanged();
    void nameChanged();
    void bioChanged();
    void avatarURLChanged();
    void profileURLChanged();

private:
    QNetworkAccessManager m_manager;

    QString m_login;
    QString m_name;
    QString m_bio;
    QString m_avatarURL;
    QUrl m_profileURL;
};

src/controller.cpp

...

void Controller::fetch(QString username)
{
    ...

    QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
        if (reply->error()) {
            qWarning() << "Error fetching URL";
            qWarning() << reply->errorString();
        } else {
            // read data
            QString replyText = reply->readAll();

            // ask doc to parse it
            QJsonDocument document = QJsonDocument::fromJson(replyText.toUtf8());

            // we know first element in file is object, to try to ask for such
            QJsonObject object = document.object();

            // ask object for value
            QJsonValue login = object.value(QString("login"));
            m_login = login.toString();
            Q_EMIT loginChanged();

            QJsonValue name = object.value(QString("name"));
            m_name = name.toString();
            Q_EMIT nameChanged();

            QJsonValue bio = object.value(QString("bio"));
            m_bio = bio.toString();
            Q_EMIT bioChanged();

            QJsonValue avatarURL = object.value(QString("avatar_url"));
            m_avatarURL = avatarURL.toString();
            Q_EMIT avatarURLChanged();

            QJsonValue profileURL = object.value(QString("html_url"));
            m_profileURL = profileURL.toString();
            Q_EMIT profileURLChanged();
        }
        reply->deleteLater(); // make sure to clean up
    });
}

Declaramos alguns metodos que vão retornar essas propriedades:

src/controller.h

...

class Controller : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString login READ login NOTIFY loginChanged)
    Q_PROPERTY(QString name READ name NOTIFY nameChanged)
    Q_PROPERTY(QString bio READ bio NOTIFY bioChanged)
    Q_PROPERTY(QString avatarURL READ avatarURL NOTIFY avatarURLChanged)
    Q_PROPERTY(QUrl profileURL READ profileURL NOTIFY profileURLChanged)

public:
    Q_INVOKABLE void fetch(QString username);

    QString login();
    QString name();
    QString bio();
    QString avatarURL();
    QUrl profileURL();

    ...
};

E definir eles no arquivo .cpp:

src/controller.cpp

...

QString Controller::login()
{
    return m_login;
}

QString Controller::name()
{
    return m_name;
}

QString Controller::bio()
{
    return m_bio;
}

QString Controller::avatarURL()
{
    return m_avatarURL;
}

QUrl Controller::profileURL()
{
    return m_profileURL;
}

Deixando nossa classe disponivel em QML

Finalmente deixamos essa classe disponivel pra nossa aplicação:

src/main.cpp

...

#include "controller.h"

int main(int argc, char *argv[])
{
    ...

    QQmlApplicationEngine engine;

    qmlRegisterSingletonInstance("org.kde.githuby", 1, 0, "Controller", new Controller);

    ...
}

qmlRegisterSingletonInstance é usado para fazer nossa classe disponivel em QML, agora a gente pode chamar ela nos nossos arquivos QML assim:

src/contents/main.qml

import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami

import org.kde.githuby 1.0

...

Você pode mostrar essas informação de qualquer jeito que quiser, aqui está como eu fiz:

/*
    SPDX-License-Identifier: GPL-2.0-or-later
    SPDX-FileCopyrightText: 2022 Felipe Kinoshita <kinofhek@gmail.com>
*/

import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import QtGraphicalEffects 1.15

import org.kde.githuby 1.0

Kirigami.ApplicationWindow {
    id: root

    title: i18n("githuby")

    minimumWidth: Kirigami.Units.gridUnit * 20
    minimumHeight: Kirigami.Units.gridUnit * 20
    width: Kirigami.Units.gridUnit * 25
    height: Kirigami.Units.gridUnit * 25

    header: QQC2.ToolBar {
        RowLayout {
            anchors.fill: parent

            Kirigami.SearchField {
                id: searchField
                Layout.fillWidth: true

                placeholderText: i18n("Insert github username")
                autoAccept: false
                onAccepted: Controller.fetch(text)
            }
        }
    }

    pageStack.initialPage: Kirigami.Page {
        id: page

        globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None

        Kirigami.PlaceholderMessage {
            id: placeholder

            anchors.centerIn: parent
        }

        ColumnLayout {
            id: content
            visible: false

            width: parent.width - (Kirigami.Units.largeSpacing * 4)
            anchors.centerIn: parent

            Image {
                id: img

                Layout.alignment: Qt.AlignHCenter
                sourceSize.height: Kirigami.Units.gridUnit * 10
                fillMode: Image.PreserveAspectFit
                source: Controller.avatarURL

                layer.enabled: true
                layer.effect: OpacityMask {
                    maskSource: Item {
                        width: img.width
                        height: img.height
                        Rectangle {
                            anchors.centerIn: parent
                            width: img.width
                            height: img.height
                            radius: Kirigami.Units.smallSpacing
                        }
                    }
                }
            }
            Kirigami.Heading {
                Layout.alignment: Qt.AlignHCenter
                text: Controller.name
                type: Kirigami.Heading.Primary
                font.underline: mousearea.containsMouse

                MouseArea {
                    id: mousearea
                    anchors.fill: parent

                    cursorShape: Qt.PointingHandCursor
                    hoverEnabled: true

                    onClicked: Qt.openUrlExternally(Controller.profileURL)
                }
            }
            Kirigami.Heading {
                Layout.alignment: Qt.AlignHCenter
                text: Controller.login
            }
            QQC2.Label {
                Layout.fillWidth: true
                Layout.alignment: Qt.AlignHCenter
                horizontalAlignment: Qt.AlignHCenter
                text: Controller.bio
                wrapMode: Text.Wrap
            }
        }
    }
}

Como que ficou

E é assim que ela se parece: githuby app

Isso é tudo por enquanto! :)