Зависимости
Для работы понадобится 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/
Добавить комментарий