Парсим данные из Google Sheets с помощью Kotlin и Retrofit в Android

от автора

Зависимости

Для работы понадобится Retrofit и Gson

implementation ("com.squareup.retrofit2:retrofit:2.11.0") implementation ("com.squareup.retrofit2:converter-gson:2.11.0")

Формат зависимостей — старый, поскольку он подходит как для проектов с version catalog, так и без него.

Получение данных с Google Sheets

Retrofit позволяет получить данные с Google Sheets (далее GS) довольно просто, а главное — без всяких API-ключей, по ссылке: для этого нужно только открыть доступ к таблице и получить на неё ссылку.

Для начала нам понадобится сервис, реализующий запросы к GS. Запрос имеет следующую форму: https://docs.google.com/spreadsheets/d/<ID документа>/<какой-то запрос>. Одним из вариантов получения данных — JSON, и запрос на его получение выглядит так: «gviz/tq?tqx=out:json».

Сервис с одним запросом на получение Json’а выглядит следующим образом:

interface GSheetsService {      @GET("gviz/tq?tqx=out:json")       suspend fun getSheetData(): ResponseBody  }

Где ResponseBody — класс для хранения всех данных с запроса, но нам от него понадобится лишь метод string(), который позволит получить строковое представление данных

Далее идет реализация этого интерфейса с помощью Retrofit:

val gSService = Retrofit.Builder()       .baseUrl(     "https://docs.google.com/spreadsheets/d/<ID документа>/"   ).build().create(GSheetsService::class.java)

P.S. Я не использую здесь convert factory, об этом будет позже при рассмотрении структуры тела ответа.

Непосредственно, данные

В качестве тестовых данных я возьму табличку, и помещу туда следующие данные:

С помощью gSService, который мы создали выше, считаем данные с этой таблички:

val data: String = gSService.getSheetData().string()

Данные:

/O_o/  google.visualization.Query.setResponse( {     "version": "0.6",     "reqId": "0",     "status": "ok",     "sig": "123456789",     "table": {         "cols": [             {                 "id": "A",                 "label": "",                 "type": "string"             },             {                 "id": "B",                 "label": "",                 "type": "string"             },             {                 "id": "C",                 "label": "",                 "type": "string"             },             {                 "id": "D",                 "label": "",                 "type": "string"             }         ],         "rows": [             {                 "c": [                     {                         "v": "text1"                     },                     {                         "v": "1"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text2"                     },                     {                         "v": "2"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text3"                     },                     {                         "v": "3"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text4"                     },                     {                         "v": "4"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text5"                     },                     {                         "v": "5"                     },                     null,                     {                         "v": null            ж         }                 ]             }         ],         "parsedNumHeaders": 0     } }); 

Первые 2 строчки ответа не позволяют использовать converter factory, поскольку сразу получаем ошибку: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 2 path

Эта ошибка указывает на неправильную структуру JSON-файла или же лишние пробелы. В нашем случае это как раз тот самый эмодзи, за которые следует еще и google.visualization.Query.setResponse(, только после которого следует нужный нам JSON-файл. Значит, ничего не остается, как парсить вручную)

Парсим JSON: создаем класс для самостоятельной конвертации с помощью GSON

Так как наш запрос состоит из <смайлик> + google.visualization.Query.setResponse( + <нужные нам данные> + );, то сразу уберем этот мусор:

fun String.dataToJsonString(): String =    substringAfter("Query.setResponse(").let {     it.substring(0, it.lastIndex - 1) }

Итак, наш JSON теперь выглядит следующим образом:

{     "version": "0.6",     "reqId": "0",     "status": "ok",     "sig": "123456789",     "table": {         "cols": [             {                 "id": "A",                 "label": "",                 "type": "string"             },             {                 "id": "B",                 "label": "",                 "type": "string"             },             {                 "id": "C",                 "label": "",                 "type": "string"             },             {                 "id": "D",                 "label": "",                 "type": "string"             }         ],         "rows": [             {                 "c": [                     {                         "v": "text1"                     },                     {                         "v": "1"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text2"                     },                     {                         "v": "2"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text3"                     },                     {                         "v": "3"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text4"                     },                     {                         "v": "4"                     },                     null,                     {                         "v": null                     }                 ]             },             {                 "c": [                     {                         "v": "text5"                     },                     {                         "v": "5"                     },                     null,                     {                         "v": null                     }                 ]             }         ],         "parsedNumHeaders": 0     } } 

Первые 4 поля определить несложно, остальные будем определять постепенно, все глубже и глубже:
Немного справки: аннотация SerializedName позволяет указать ключ поля, данные из которого должны быть записаны в эту переменную в случае, когда есть несколько переменных одного типа.

    @SerializedName("version") val version: String,     @SerializedName("reqId") val reqId: String,     @SerializedName("status") val status: String,     @SerializedName("sig") val sig: String,      @SerializedName("table") val table: Table

Я отделил пятое, о нём как раз сейчас и пойдет речь.
Внутри тэга table находится еще 3 тэга: cols, rows, parsedNumHeaders

Так и запишем:

data class Table(     @SerializedName("cols") val cols: List<Column>,     @SerializedName("rows") val rows: List<Row>,     @SerializedName("parsedNumHeaders") val parsedNumHeaders: Int )

И далее аналогичным образом разберемся с внутренностями каждого из полей с составным типом:
Внутри тэга cols находятся трехстрочные объекты:

Так и запишем:

data class Column(     @SerializedName("id") val id: String,     @SerializedName("label") val label: String,     @SerializedName("type") val type: String )

Что касается тэга rows, то там все немного сложнее:

Внутри данного тэга лежит список объектов с картинки выше, которые внутри себя содержат еще список из объектов. Т.к. это список строк, то несложно догадаться, что список строк содержит в себе в качестве объекта саму строку, а строка содержит в себе список объектов — клеточек с данными. Класс строки будет выглядеть так:

data class Row(     @SerializedName("c") val cells: List<Cell?> ) 

Тип внутри списка — nullable, поскольку даже в нашем случае появились значения null. Ну и класс клетки содержит в себе лишь значение в этой клетке:

data class Cell(     @SerializedName("v") val value: String )

Теперь, когда выходных классы данных определены, дело остается только за конвертацией:

fun String.getResponseFromJson(): ResponseWrapper =     GsonBuilder().setLenient().create()   .fromJson(this, ResponseWrapper::class.java)

На этом моменте мы получаем тот самый класс, который написали выше на основе JSON‑файла.

Скорее всего, самая полезная часть этого класса — данные в тэге rows, потому что это как раз те самые клеточки, получение данных из которых — тема данной статьи.

Заключение

В этой статье был рассмотрен процесс преобразования данных Google Sheets из JSON формата в классы Kotlin.

Более того, в статье фигурировала suspend функция — «Best wishes, coroutines» — с расчётом на то, что читатель имеет минимальные представления о том, как работать с этими функциями.

No errors, no warnings, gentlemen and ladies!


ссылка на оригинал статьи https://habr.com/ru/articles/828812/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *