📜 ⬆️ ⬇️

Qt / QML REST Client

I saw an article today in the feed and remembered what I wanted to write about my project a couple of lines on Habr.

In general, for a while I worked as a technical manager with iOS / Android programmers, who used a lot in their Django / Yii2 / proprietary API code. And having looked from the side at the tools that they have for working with the REST API, I decided to add something similar in Qt, since There were no normal tools for working with REST using Qt models.

No sooner said than done. The picture below shows the resulting scheme, and below it, the actual description of the idea, architecture and brief instructions for use.
')
image

So, here is what we discuss:

  1. Idea and features
  2. Architecture
  3. Usage example
  4. Source Code and Sample Application

Idea and features


At its core, any normally designed REST API is reduced to receiving HTTP requests and sending list / single data objects to JSON / XML to the client.

On the other hand, in Qt, there is a mechanism of models for a long time that can solve both reading and changing data problems; all you need to do is override the corresponding functions of the base classes of the models.

Based on these statements, I decided that the library should meet the following requirements:


In developing the requirements, I was guided by such server API creation tools as the Yii2-REST and Django REST framework , since This is in my opinion the most functional free solutions for creating REST services, besides, they come from completely different worlds and when analyzing their documentation, as well as writing test projects, I obtained data from different approaches to organizing REST on the server.

Architecture


So, as mentioned above, everything revolves around QAbstractListModel, because this is the native way to access data from Qt C ++ / QML. Let's get to the specifics.

According to the scheme above, we have two base classes: APIBase (QObject descendant) and BaseRestListModel (QAbstractListModel descendant).


Properties class APIBase:


In principle, this set of properties is enough to work with any service. Each property is available by itself in the heir class and in QML (since all of them are Q_PROPERTY).

To write your own class for working with APIs, you need to inherit from APIBase and at least implement the handleRequest method, as well as all the necessary methods for obtaining data from the server using the above properties, parameters from the model (will be lower) and protected methods get, post, put , deleteResource, head, options, patch (all correspond to the same methods in the HTTP protocol).

That's all, inside your methods for receiving data there should be a code for parsing the parameters transferred from the model (read from the application), and then the case of technology, all that remains is to generate and send the correct request to the server using QUrl / QUrlQuery. The result is a pointer to QNetworkReply, which is returned to the base class and already there is a subscription to its completion signals.

Consider different scenarios for using your API class, together with the models.

1. handleRequest and ready ReadOnly-models


This scenario is used when you do not need your own data models and you would like to get by with ready. There are two such models in the library - JsonRestListModel and XmlRestListModel.

Both of these models are ReadOnly and immediately ready for use from C ++ / QML

To work with ReadOnly-models, you need to implement the handleRequest method in the API class, here is its interface:

virtual QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination, QVariantMap filters = QVariantMap(), QStringList fields = QStringList(), QString id = 0) 

where path is an API method, sort is a sorting parameter, pagination is a paging parameter object, filters are filtering parameters, fields is a list of returned fields, id is a unique identifier for the record.

Each of the ReadOnly-models implements access to the API in the following form:

 QNetworkReply *JsonRestListModel::fetchMoreImpl(const QModelIndex &parent) { Q_UNUSED(parent) return apiInstance()->handleRequest(requests()->get(), sort(), pagination(), filters(), fields()); } 

Looking ahead a bit, I'll show you how using QML in QML looks like:

 ... MyApi { id: myApi } JsonRestListModel { id: jsonSampleModel api: myApi //  API  idField: 'id' // -    //    API  handleRequest // ReadOnly    readOnly  get ( )  getDetails (   ) //  -  .  Requests requests { get: "/v1/coupon" getDetails: "/v1/coupon/{id}" } //    filters: {'isArchive': '0'} //    fields: ['id','title'] //  sort: ['-id'] //   pagination { policy: Pagination.PageNumber // -    perPage: 20 //-     } //         Component.onCompleted: { reload(); } } ... 

The practice of using ready-made models allows you to implement only the API class and not to steam with the inheritance of models. Of course, as in the next scenario, here the API class must have full functionality.

2. Writing your own models


If ReadOnly models do not suit us, then we can inherit from AbstractJsonRestListModel and AbstractXmlRestListModel and create our own model with all the necessary methods for data manipulation. We will talk more about this type of model in the use case.

Add that for data manipulation, you can use two options:

Here I decided not to introduce any restrictions, but the second option will be more accurate. I will explain why. Each element of the model is an instance of the RestItem class and there are helper methods in the model itself, such as:

Thus, in the model there is a mechanism for direct work with model elements, which is absent in the API class, and also in the class API, the minimum necessary functionality for working with the server remains and all data preparation / processing occurs in the models.

The RestItem class itself is needed for the data () and roleNames () model methods, which are standard Qt methods, to represent the available keys / values ​​in QML.

3. Using the API class directly


Finally, we can do without models at all, having described all the logic of working in the APi class, for this it is enough to override the replyFinished method and make requests via the API directly.

Hmm ... I got carried away with the description of the scenarios of course ... We go further to the models. As I said, the base class of all models is BaseRestListModel. Actually, this base class does almost all the work.

So, the list of class properties:


Here too, all properties are Q_PROPERTY.

In addition to properties, each model inherited from this class has the following methods available:


Inheriting from AbstractJsonRestListModel or AbstractXmlRestListModel, to create a working model, you must also implement a number of methods:


The model of course contains a lot of everything else, but all this can be seen in the source code, there are enough comments to understand and rewrite the model to fit your individual needs. All add. methods in the protected section.

As I said yours, two specific classes are associated with models - Pagination and DetailsModel.
With DetailsModel, in principle, everything is simple. When you click on the list item in the application, we request data, fill this model with it, give the application a pointer. In the application, the truth will have to be a little distorted and create a non-interactive ListView with one element, passing it a delegate and a pointer to a detailed model are needed - this is how we get the “detailed information page”.

There should be no problems with Pagination either. This class defines only the parameters of pagination and stores the current state for the model. everything is also set through a set of properties:


That's all, then the model itself will fix the download of new data together with the ListView and stop loading when it reaches max. count

Oh yeah, there is also the Requests class, which is used in ReadOnly models and in QML. Look at the source, everything is simple)

That's about the architecture I got. Of course, I wrote the library and tried to immediately put it into practice, so I made a demo application that you can compile and see. Here on his example now and we will sort on use library.

Usage example


I have one small project on Yii2, which is located at ... I will not say which one, otherwise I know Habr))

So, here in this project I actually implemented several API methods, which I used when developing demos.

Below are the API methods and data they return.

/ v1 / categories
 [{ "id": 1, "sourceServiceId": 2, "categoryName": "", "categoryCode": "aktsii", "categoryIdentifier": "0", "parentCategoryIdentifier": "0", "categoryAdditionalInfo": "0", "isActive": 1 }, { "id": 2, "sourceServiceId": 2, "categoryName": "", "categoryCode": "kupony", "categoryIdentifier": "28", "parentCategoryIdentifier": "28", "categoryAdditionalInfo": "https://blizzard.kz/kuponator/categ/28", "isActive": 1 } , ...] 


/ v1 / coupon
 [{ "id": 1, "sourceServiceId": 1, "cityId": 1, "createTimestamp": "2015-03-12 14:01:57", "lastUpdateDateTime": "2016-10-20 03:54:47", "recordHash": "e7b01c1a69bc66e1f1a62d8fcb0825de", "title": "Home Club", "shortDescription": "    , , ,    ", "longDescription": "   –       .   ,    –   -  Home Club.           50%!      , ,    .   ! ", "conditions": " <p class="e-condition__text">:</p> <ul class="b-conditions-list"> <li class="e-condition">      -  Home Club.</li> <li class="e-condition"> <strong></strong>:  50%   - 1 500 .  3 000 .</li> <li class="e-condition">         ( ).</li> <li class="e-condition">      .</li> <li class="e-condition">    10 .</li> <li class="e-condition"> <strong>VIP-     .   VIP-     .</strong> </li> <li class="e-condition">        .</li> <li class="e-condition"> <strong>    :</strong><br> +7 (727) 308-23-63,<br> +7 (747) 841-42-51,<br> +7 (701) 985-90-72.</li> <li class="e-condition"> <strong>   .</strong> </li> <li class="e-condition">      2 ,     ( ).</li> <li class="e-condition">        ,       «».</li> <li class="e-condition">      Home Club  :  ,    ,  , Home Club.</li> <li class="e-condition">         ,   ,    .</li> <li class="e-condition"> <strong>     .</strong> </li> <li class="e-condition">    12  2015 . ().</li> <li class="e-condition"> <span hashstring="deal_refunds_policy" hashtype="content">&nbsp</span> </li> <li class="e-condition"> <span hashstring="deal_standard_conditions" hashtype="content">&nbsp</span> </li> </ul> <p class="e-offer__features"></p> <ul class="b-offer__features-list"> <li class="e-offer__feature ">  ,    ,  , Home Club </li> <li class="e-offer__feature "> :<br> +7 (727) 308-23-63<br>+7 (747) 841-42-51<br>+7 (701) 985-90-72<br> </li> <li class="e-offer__feature "> :<br> : </li> </ul>", "features": " <p class="e-offer__features">:</p> <ul class="b-offer__features-list"> <li class="e-offer__feature">Home Club            ,    ,     .</li> <li class="e-offer__feature"> ​   10  (5    5 Vip-).  Vip-      , , -, ,  .</li> <li class="e-offer__feature"> ​ Home Club    -  3- : <ul> <li> ;</li> <li>;</li> <li> .</li> </ul> </li> <li class="e-offer__feature"> ​  : <ul> <li> 4-  7-  ;</li> <li>1 :  , -, , ;</li> <li>2 : 2-  ,  ,    , ,   ;</li> <li>3 : 2  ,   .</li> </ul> </li> <li class="e-offer__feature"> ​VIP-: <ul> <li> 4-  11-  ;</li> <li>1 : ,    10 ,  , ,   , ,  ;</li> <li>2 : -12 ,  2-  4-   (    ), 2 ..</li> </ul> </li> <li class="e-offer__feature"> : <a data-seohide-href="/deal/away/20056/" class="e-offer__feature--link seohide-link" target="_blank" rel="nofollow" title="http://www.home-club.kz/">www.home-club.kz/</a> </li> </ul>", "imagesLinks": [ "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/1_20150312023051426147565.7364.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/2_20150312023051426147565.9348.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/4_20150312093171426174997.7985.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/5_20150312093171426174997.944.jpg", "https://www.chocolife.me/" ], "timeToCompletion": null, "mainImageLink": "https://www.chocolife.me/", "originalCouponPrice": "30 000", "originalPrice": "30 000", "discountPercent": "-51%", "discountPrice": "18 000", "discountType": "full", "boughtCount": "1", "sourceServiceCategories": "1 , 82 , 8 , 2", "pageLink": "https://www.chocolife.me//20056-arenda-kottedzha-s-dvumya-spalnyami-gorki-sauna-darts-i-mnogoe-drugoe-v-prirodno-razvlekatelnom-parke-home-club-skidka-do-50", "isArchive": 1, "tryToUpdateCount": 0, "viewCount": "0", "serviceName": "Chocolife.me", "cityName": "" }, { "id": 2, "sourceServiceId": 1, "cityId": 1, "createTimestamp": "2015-03-12 14:01:57", "lastUpdateDateTime": "2016-11-01 12:39:53", "recordHash": "dce10232f1acb53b1ee7a8bf3902e0c0", "title": "    AquaBike Centre", "shortDescription": "       ", "longDescription": null, "conditions": null, "features": " <p class="e-offer__features">:</p> <ul class="b-offer__features-list"> <li class="e-offer__feature">        .     .</li> <li class="e-offer__feature"> Aquabike – : <ul> <li> ;</li> <li> ;</li> <li>  ;</li> <li>  ;</li> <li>  ;</li> <li>    ;</li> <li> ;</li> <li> .</li> </ul> </li> <li class="e-offer__feature"> <strong> :</strong> <ul> <li>    ;</li> <li>         ;</li> <li> ,  ;</li> <li>  ;</li> <li>   ,    ;</li> <li>  ,    ;</li> <li>   ;</li> <li>   , ,  .</li> </ul> </li> <li class="e-offer__feature"> <strong>     1    .</strong> </li> <li class="e-offer__feature">  AquaBike Centre  : <ul> <li>  ,   2 ;</li> <li>   ;</li> <li>,  45 .</li> </ul> </li> </ul>", "imagesLinks": [ "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20016/660x305/1_20150314013241426318344.7033.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20016/660x305/2_20150314013241426318344.8157.JPG", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20016/660x305/4_20150311053411426073981.6524.JPG", "https://www.chocolife.me/" ], "timeToCompletion": null, "mainImageLink": "https://www.chocolife.me/", "originalCouponPrice": "3 000", "originalPrice": "3 000", "discountPercent": "-50%", "discountPrice": "1 500", "discountType": "full", "boughtCount": "58", "sourceServiceCategories": "1 , 68 , 36 , 2", "pageLink": "https://www.chocolife.me//20016-novinka-iz-francii-vse-dlya-vashey-krasoty-zdorovya-i-relaksacii-trenirovki-po-akvabaykingu-a-takzhe-pressoterapiya-so-skidkoy-50-v-aquabike-centre", "isArchive": 1, "tryToUpdateCount": 0, "viewCount": "0", "serviceName": "Chocolife.me", "cityName": "" }, ...] 


/ v1 / coupon / {id}
 { "id": 1, "sourceServiceId": 1, "cityId": 1, "createTimestamp": "2015-03-12 14:01:57", "lastUpdateDateTime": "2016-10-20 03:54:47", "recordHash": "e7b01c1a69bc66e1f1a62d8fcb0825de", "title": "Home Club", "shortDescription": "    , , ,    ", "longDescription": "   –       .   ,    –   -  Home Club.           50%!      , ,    .   ! ", "conditions": " <p class="e-condition__text">:</p> <ul class="b-conditions-list"> <li class="e-condition">      -  Home Club.</li> <li class="e-condition"> <strong></strong>:  50%   - 1 500 .  3 000 .</li> <li class="e-condition">         ( ).</li> <li class="e-condition">      .</li> <li class="e-condition">    10 .</li> <li class="e-condition"> <strong>VIP-     .   VIP-     .</strong> </li> <li class="e-condition">        .</li> <li class="e-condition"> <strong>    :</strong><br> +7 (727) 308-23-63,<br> +7 (747) 841-42-51,<br> +7 (701) 985-90-72.</li> <li class="e-condition"> <strong>   .</strong> </li> <li class="e-condition">      2 ,     ( ).</li> <li class="e-condition">        ,       «».</li> <li class="e-condition">      Home Club  :  ,    ,  , Home Club.</li> <li class="e-condition">         ,   ,    .</li> <li class="e-condition"> <strong>     .</strong> </li> <li class="e-condition">    12  2015 . ().</li> <li class="e-condition"> <span hashstring="deal_refunds_policy" hashtype="content">&nbsp</span> </li> <li class="e-condition"> <span hashstring="deal_standard_conditions" hashtype="content">&nbsp</span> </li> </ul> <p class="e-offer__features"></p> <ul class="b-offer__features-list"> <li class="e-offer__feature ">  ,    ,  , Home Club </li> <li class="e-offer__feature "> :<br> +7 (727) 308-23-63<br>+7 (747) 841-42-51<br>+7 (701) 985-90-72<br> </li> <li class="e-offer__feature "> :<br> : </li> </ul>", "features": " <p class="e-offer__features">:</p> <ul class="b-offer__features-list"> <li class="e-offer__feature">Home Club            ,    ,     .</li> <li class="e-offer__feature"> ​   10  (5    5 Vip-).  Vip-      , , -, ,  .</li> <li class="e-offer__feature"> ​ Home Club    -  3- : <ul> <li> ;</li> <li>;</li> <li> .</li> </ul> </li> <li class="e-offer__feature"> ​  : <ul> <li> 4-  7-  ;</li> <li>1 :  , -, , ;</li> <li>2 : 2-  ,  ,    , ,   ;</li> <li>3 : 2  ,   .</li> </ul> </li> <li class="e-offer__feature"> ​VIP-: <ul> <li> 4-  11-  ;</li> <li>1 : ,    10 ,  , ,   , ,  ;</li> <li>2 : -12 ,  2-  4-   (    ), 2 ..</li> </ul> </li> <li class="e-offer__feature"> : <a data-seohide-href="/deal/away/20056/" class="e-offer__feature--link seohide-link" target="_blank" rel="nofollow" title="http://www.home-club.kz/">www.home-club.kz/</a> </li> </ul>", "imagesLinks": [ "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/1_20150312023051426147565.7364.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/2_20150312023051426147565.9348.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/4_20150312093171426174997.7985.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/5_20150312093171426174997.944.jpg", "https://www.chocolife.me/" ], "timeToCompletion": null, "mainImageLink": "https://www.chocolife.me/", "originalCouponPrice": "30 000", "originalPrice": "30 000", "discountPercent": "-51%", "discountPrice": "18 000", "discountType": "full", "boughtCount": "1", "sourceServiceCategories": "1 , 82 , 8 , 2", "pageLink": "https://www.chocolife.me//20056-arenda-kottedzha-s-dvumya-spalnyami-gorki-sauna-darts-i-mnogoe-drugoe-v-prirodno-razvlekatelnom-parke-home-club-skidka-do-50", "isArchive": 1, "tryToUpdateCount": 0, "viewCount": "0", "serviceName": "Chocolife.me", "cityName": "" } 


Well, something like that ... I tried to make it so that there were many different types of data, including a subarray with pictures.

What do we do with all this good? Handle of course! Here and later in this section there will be a dry code with comments, but in the correct sequence. So everything should be clear.

So, to begin with, let's create an API SkidKZApi class and implement the methods for working with server data.

skidkzapi.h
 #ifndef SKIDKZAPI_H #define SKIDKZAPI_H #include "apibase.h" #include <QtQml> class SkidKZApi : public APIBase { Q_OBJECT public: Q_INVOKABLE explicit SkidKZApi(); // .      QML static void declareQML() { qmlRegisterType<SkidKZApi>("com.github.qtrestexample.skidkzapi", 1, 0, "SkidKZApi"); } //      ReadOnly  QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination, QVariantMap filters = QVariantMap(), QStringList fields = QStringList(), QString id = 0); //      /v1/coupon QNetworkReply *getCoupons(QStringList sort, Pagination *pagination, QVariantMap filters = QVariantMap(), QStringList fields = QStringList()); //      /v1/coupon/{id} QNetworkReply *getCouponDetail(QString id); //      /v1/categories QNetworkReply *getCategories(QStringList sort, Pagination *pagination); }; #endif // SKIDKZAPI_H 


skidkzapi.cpp
 #include "skidkzapi.h" #include <QFile> #include <QTextStream> #include <QUrlQuery> SkidKZApi::SkidKZApi() : APIBase(0) { } QNetworkReply *SkidKZApi::handleRequest(QString path, QStringList sort, Pagination *pagination, QVariantMap filters, QStringList fields, QString id) { // ,           if (path == "/v1/coupon") { return getCoupons(sort, pagination, filters, fields); } else if (path == "/v1/coupon/{id}") { return getCouponDetail(id); } else if (path == "/v1/categories") { return getCategories(sort, pagination); } } //  ,   ,           QNetworkReply *SkidKZApi::getCoupons(QStringList sort, Pagination *pagination, QVariantMap filters, QStringList fields) { //   QUrl url = QUrl(baseUrl()+"/v1/coupon"); QUrlQuery query; // if (!sort.isEmpty()) { query.addQueryItem("sort", sort.join(",")); } //      switch(pagination->policy()) { case Pagination::PageNumber: query.addQueryItem("per-page", QString::number(pagination->perPage())); query.addQueryItem("page", QString::number(pagination->currentPage())); break; case Pagination::None: case Pagination::Infinity: case Pagination::LimitOffset: case Pagination::Cursor: default: break; } // .  ,     -    if (!filters.isEmpty()) { QMapIterator<QString, QVariant> i(filters); while (i.hasNext()) { i.next(); query.addQueryItem(i.key(), i.value().toString()); } } //       if (!fields.isEmpty()) { query.addQueryItem("fields", fields.join(",")); } //  url.setQuery(query.query()); //     GET QNetworkReply *reply = get(url); return reply; } //      QNetworkReply *SkidKZApi::getCouponDetail(QString id) { if (id.isEmpty()) { qDebug() << "ID is empty!"; return 0; } //         GET QUrl url = QUrl(baseUrl()+"/v1/coupon/"+id); QNetworkReply *reply = get(url); return reply; } //    ,   QNetworkReply *SkidKZApi::getCategories(QStringList sort, Pagination *pagination) { // QUrl url = QUrl(baseUrl()+"/v1/categories"); QUrlQuery query; // if (!sort.isEmpty()) { query.addQueryItem("sort", sort.join(",")); } // switch(pagination->policy()) { case Pagination::PageNumber: query.addQueryItem("per-page", QString::number(pagination->perPage())); query.addQueryItem("page", QString::number(pagination->currentPage())); break; case Pagination::None: case Pagination::Infinity: case Pagination::LimitOffset: case Pagination::Cursor: default: break; } url.setQuery(query.query()); QNetworkReply *reply = get(url); return reply; } 


The API class is ready, in a few simple methods we have implemented all the work with the server we need at the moment. Next, consider two options for using the model. For categories, we will use the JsonRestListModel model built into the library, and for coupons, the model inherited from the AbstractJsonListModel.

couponmodel.h
 #ifndef COUPONMODEL_H #define COUPONMODEL_H #include "abstractjsonrestlistmodel.h" #include "api/skidkzapi.h" class CouponModel : public AbstractJsonRestListModel { Q_OBJECT public: explicit CouponModel(QObject *parent = 0); //   QML (    main.cpp   QML) static void declareQML() { AbstractJsonRestListModel::declareQML(); qmlRegisterType<CouponModel>("com.github.qtrestexample.coupons", 1, 0, "CouponModel"); } protected: //    API QNetworkReply *fetchMoreImpl(const QModelIndex &parent); QNetworkReply *fetchDetailImpl(QString id); //    QVariantMap preProcessItem(QVariantMap item); }; #endif // COUPONMODEL_H 


couponmodel.cpp
 #include "couponmodel.h" CouponModel::CouponModel(QObject *parent) : AbstractJsonRestListModel(parent) { } QNetworkReply *CouponModel::fetchMoreImpl(const QModelIndex &parent) { Q_UNUSED(parent) //   API  return static_cast<SkidKZApi *>(apiInstance())->getCoupons(sort(), pagination(), filters(), fields()); } QNetworkReply *CouponModel::fetchDetailImpl(QString id) { //   API  return static_cast<SkidKZApi *>(apiInstance())->getCouponDetail(id); } QVariantMap CouponModel::preProcessItem(QVariantMap item) { //      createTimestamp QDate date = QDateTime::fromString(item.value("createTimestamp").toString(), "yyyy-MM-dd hh:mm:ss").date(); item.insert("createDate", date.toString("dd.MM.yyyy")); //  -  originalCouponPrice QString originalCouponPrice = item.value("originalCouponPrice").toString().trimmed(); if (originalCouponPrice.isEmpty()) { originalCouponPrice = "?"; } QString discountPercent = item.value("discountPercent").toString().trimmed().remove("—").remove("-").remove("%"); if (discountPercent.isEmpty()) { discountPercent = "?"; } QString originalPrice = item.value("originalPrice").toString().trimmed(); if (originalPrice.isEmpty()) { originalPrice = "?"; } QString discountPrice = item.value("discountPrice").toString().remove(".").trimmed(); if (discountPrice.isEmpty()) { discountPrice = "?"; } //    discountString,     API QString discountType = item.value("discountType").toString(); QString discountString = tr("Undefined Type"); if (discountType == "freeCoupon" || discountType == "coupon") { discountString = tr("Coupon: %1. Discount: %2%").arg(originalCouponPrice).arg(discountPercent); } else if (discountType == "full") { discountString = tr("Cost: %1. Certificate: %2. Discount: %3%").arg(originalPrice).arg(discountPrice).arg(discountPercent); } item.insert("discountString", discountString); return item; } 


Done!We have everything you need to get data, sometimes linking it to the GUI part.

To begin with, do not forget to call the declareQML methods in main.cpp, an example will be in the sources.

Well, then - as usual we create a QML application and use our models as a data source:

somewhere.qml
 ... import com.github.qtrestexample.skidkzapi 1.0 import com.github.qtrest.jsonrestlistmodel 1.0 import com.github.qtrest.pagination 1.0 import com.github.qtrest.requests 1.0 ... //API ,    ,   -  =) SkidKZApi { id: skidKZApi baseUrl: "http://api.skid.kz" authTokenHeader: "Authorization" authToken: "Bearer 8aef452ee3b32466209535b96d456b06" Component.onCompleted: console.log("completed!"); } // ,  ReadOnly  // ,            -     ,      JsonRestListModel { id: categoriesRestModel api: skidKZApi idField: 'id' requests { get: "/v1/categories" } sort: ['categoryName'] pagination { policy: Pagination.PageNumber perPage: 20 currentPageHeader: "X-Pagination-Current-Page" totalCountHeader: "X-Pagination-Total-Count" pageCountHeader: "X-Pagination-Page-Count" } Component.onCompleted: { console.log(pagination.perPage); reload(); } } //  CouponModel ,     requests, ..    fetchMoreImpl. CouponModel { id: coupons; api: skidKZApi filters: {'isArchive': '0'} idField: 'id' fields: ['id','title','sourceServiceId','imagesLinks', 'mainImageLink','pageLink','cityId','boughtCount', 'shortDescription','createTimestamp', 'serviceName', 'discountType', 'originalCouponPrice', 'originalPrice', 'discountPercent', 'discountPrice'] sort: ['-id'] pagination { policy: Pagination.PageNumber perPage: 20 currentPageHeader: "X-Pagination-Current-Page" totalCountHeader: "X-Pagination-Total-Count" pageCountHeader: "X-Pagination-Page-Count" } Component.onCompleted: { console.log(pagination.perPage); reload(); } } 


Well, that's all, I will not show an example of using the model in ListView, whoever is interested - everything is in the source code of the project.

Source Code and Sample Application


Well, actually move on to the most interesting. The whole project is on GitHub at the following addresses:


I hope all of the above will be useful for someone and will allow you not to waste time on developing client API for your own needs.

PS: It’s a pity, there have been no discussions under technical articles for three years already, so if you are interested in the topic - be sure to write comments, what if I missed something in implementation? =)

Source: https://habr.com/ru/post/314984/


All Articles