Парсим данные из 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'а выглядит следующим образом:
import okhttp3.ResponseBody
import retrofit2.http.GET
interface GSheetsService {
@GET("gviz/tq?tqx=out:json")
suspend fun getSheetData(): ResponseBody
}
Где ResponseBody - класс для хранения всех данных с запроса, но нам от него понадобится лишь метод string(), который позволит получить строковое представление данных
Далее идет реализация этого интерфейса с помощью Retrofit:
import retrofit2.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 позволяет указать ключ поля, данные из которого должны быть записаны в эту переменную в случае, когда есть несколько переменных одного типа.
import com.google.gson.annotations.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
)
Теперь, когда выходных классы данных определены, дело остается только за конвертацией:
import com.google.gson.GsonBuilder
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!