Зависимости

Для работы понадобится 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!