Давайте рассмотрим, каким образом настроить и использовать последнюю на данный момент версию клиента apollo в многомодульном приложении под android.

Что такое Apollo и когда он используется?

Apollo Android - это клиент GraphQL, который генерирует Kotlin модели на основе запросов GraphQL. Эти модели предоставляют вам типобезопасный API для работы с серверами GraphQL. Apollo помогает вам объединять, организовывать и легко получать доступ к вашим операторам запросов GraphQL.

Ссылка на официальный Github

Создаем проект подключаем плагин и зависимости

Создаем новый проект на основе Empty Activity, я назову его ApolloConfig_Example

Создание нового проекта

Создадим несколько модулей, я назову их 'common', и 'modules',последний в свою очередь будет содержать 'module_1' и 'module_2'

Модули проекта

Подключаем JS GraphQL плагин - он добавляет поддержку языка, подсветку синтаксиса GraphQL

Плагин JS GraphQL

Подключаем плагин Apollo 3.0 во всех gradle файлах наших модулях

id 'com.apollographql.apollo3' version("3.0.0-alpha07")
Плагин Apollo 3.0

После добавления apollo плагина появится сообщение о необходимости синхронизации проекта, но перед этим необходимо добавить настройки для плагина

Для gradle файла модуля 'app'

apollo {
        //Устанавливает имя пакета куда будут помещены автоматически генерируемые классы.
        packageName.set("com.alab.app")
        //Устанавливает путь к вашей схеме (В данном случае указывает на файл в корне проекта).
        schemaFile.set(file("../schema.graphqls"))
    }

Для gradle файла модуля 'common'

apollo {
        //Устанавливает имя пакета куда будут помещены автоматически генерируемые классы.
        packageName.set("com.alab.common")
        //Устанавливает путь к вашей схеме (В данном случае указывает на файл в корне проекта).
        schemaFile.set(file("../schema.graphqls"))
        //Устанавливает сопоставление скаляра из graphql схемы к вашему классу.
        customScalarsMapping = [
                "DateTime" : "java.util.Date"
        ]
    }

Для gradle файла модуля 'module_1' и 'module_2'

apollo {
        //Устанавливает имя пакета куда будут помещены автоматически генерируемые классы.
        packageName.set("com.alab.module_1")
        //Устанавливает путь к вашей схеме (В данном случае указывает на файл в корне проекта).
        schemaFile.set(file("../../schema.graphqls"))
        //Устанавливает путь к пакету с файлами .qraphql, которые описывают ваши запросы.
        srcDir(file("src/main/java/com/alab/module_1/services/"))
        //Устанавливает сопоставление скаляра из graphql схемы к вашему классу.
        customScalarsMapping = [
                "DateTime" : "java.util.Date"
        ]
    }
apollo {
        //Устанавливает имя пакета куда будут помещены автоматически генерируемые классы.
        packageName.set("com.alab.module_2")
        //Устанавливает путь к вашей схеме (В данном случае указывает на файл в корне проекта).
        schemaFile.set(file("../../schema.graphqls"))
        //Устанавливает путь к пакету с файлами .qraphql, которые описывают ваши запросы.
        srcDir(file("src/main/java/com/alab/module_2/services/"))
        //Устанавливает сопоставление скаляра из graphql схемы к вашему классу.
        customScalarsMapping = [
                "DateTime" : "java.util.Date"
        ]
    }

Нажимаем кнопку 'Sync Now'

Кнопка 'Sync Now'

Добавляем зависимости в модули 'common', 'module_1' и 'module_2'

implementation "com.apollographql.apollo3:apollo-runtime:3.0.0-alpha07"
implementation "com.apollographql.apollo3:apollo-api:3.0.0-alpha07"
Зависимости

В Gradle файлах 'app', 'module_1' и 'module_2' добавляем в зависимости модуль 'common'. Он станет видим для этих модулей.

implementation(project(":common"))

Создаем необходимые классы

Начнем с файла scheme.graphqls, это файл описывает, какие данные могут быть запрошены, разместить его нужно в корне проекта, на одном уровне с папками 'app', 'common', 'modules'.

schema {
  query: Query
}

type Query {
  "Возвращает сотрудников.\n\n\n**Returns:**\nСотрудник, если он найден."
  employee("Идентификатор сотрудника." id: String): Employee
}

"Представляет сотрудника."
type Employee @source(name: "Employee", schema: "Employees") {
  "Возвращает id пользователя"
  id: String
  "Возвращает полное имя."
  fullName: String!
  "Возвращает табельный номер."
  personnelNumber: String!
  "Возвращает статус, показывающий присутствие сотрудника на работе."
  workStatus: Employees_WorkingPeriod!
}

"Определяет рабочий период."
type Employees_WorkingPeriod @source(name: "NonWorkingPeriod", schema: "Employees") {
  "Возвращает дату начала."
  beginDate: DateTime!
  "Возвращает дату окончания."
  endDate: DateTime!
}

"Annotates the original name of a type."
directive @source("The original name of the annotated type." name: Name! "The name of the schema to which this type belongs to." schema: Name!) repeatable on ENUM | OBJECT | INTERFACE | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE

"The name scalar represents a valid GraphQL name as specified in the spec and can be used to refer to fields or types."
scalar Name

"The `DateTime` scalar represents an ISO-8601 compliant date time type."
scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time")

В модуле 'common' создадим класс 'ApolloClient', в нем напишем клиент, поскольку он находится в общем модуле, он будет виден всем другим модулям.

/**
 * Представляет клиент Apollo.
 */
val apolloClient = ApolloClient(
    networkTransport = HttpNetworkTransport(
        serverUrl = "https://your_url.com/api/graphQl",   //Адресс Вашего Api
        okHttpClient = OkHttpClient.Builder()
            .addInterceptor(Interceptor { chain ->
                val newRequestBuilder = chain.request().newBuilder()
                newRequestBuilder.apply {
                    addHeader(
                        "Authorization",
                        "Bearer <Your Token>"   //Ваш токен
                    )
                    addHeader("Content-Type", "application/json")
                        .build()
                }
                val newRequest = newRequestBuilder.build()
                return@Interceptor chain.proceed(newRequest)
            }).build()
    ) )
    .withCustomScalarAdapter(DateTime.type, dateTimeAdapter)

В этом же модуле создадим класс 'GraphqlAdapters', в нем напишем скалярный адаптер для типа DateTime, который присутствует в схеме, данные которые будут приходить с этим типом, будут автоматически преобразованы в привычный класс Date.

/**
 * Адаптер для преобразования Any в Date.
 */
val dateTimeAdapter = object : Adapter<Date> {
    override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): Date {
        val source = reader.nextString()
        val date: Date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S'Z'", Locale.getDefault())
            .parse(source) ?: throw RuntimeException("Не удалось распарсить строку '${source}' в дату")
        return date
    }

    override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: Date) {
        AnyAdapter.toJson(writer, value)
    }
}

Перейдем к module_1 и module_2, следующие файлы и классы будут почти идентичны для обоих модулей, и сделано для примера.
Создадим файл 'GetEmployee.graphql', он будет на основе scheme.graphqls описывать наш запрос, а apollo на основе 'GetEmployee.graphql' сгенерирует все необходимые классы.

query GetEmployee($id: String) {
    employee(id: $id) {
        id,
        fullName,
        personnelNumber,
        workStatus {
            beginDate,
            endDate
        }
    }
}

Далее создадим класс ApiService и cоответствующий ему интерфейс IApiService, там будем описывать наши запросы.

Интерфейс 'IApiService'

/**
 * Описывает методы запросов к api.
 */
interface IApiService {

    /**
     * Возвращает сотрудника по указанному id.
     * @param id Идентификатор сотрудника.
     */
    suspend fun getEmployee(id: String): GetEmployeeQuery.Employee?

}

Класс 'ApiService'

/**
 * Представляет сервис запросов к api.
 * @param apolloClient Apollo client.
 */
class ApiService(
    private val apolloClient: ApolloClient,
) : IApiService {

    override suspend fun getEmployee(id: String): GetEmployeeQuery.Employee? {
        return apolloClient.query(GetEmployeeQuery(id)).data?.employee
    }

}

Все готово для написания самого запроса, создадим класс фрагмента, а в нем запрос

/**
 * Представляет фрагмент экрана.
 */
class ModuleFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return View(requireContext())
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        GlobalScope.launch {
            val employeeResponse = apolloClient.query(GetEmployeeQuery("12345")).data?.employee
        }

    }
}
Структура классов в module_1 и module_2

Скачать пример можно на моем GitHub.

Комментарии (1)


  1. ermadmi78
    10.10.2021 20:56

    Основная проблема такого подхода заключается в том, что вы в compile time должны знать все запросы, которые будете исполнять на клиенте. Т.е. по факту каждый клиент будет генерировать свои собственные запросы. На мой взгляд более перспективный подход - это генерировать полноценный клиентский DSL по GraphQL схеме, который позволит нам выполнить любой GraphQL запрос в runtime, не зная о нем заранее в compile time.

    Какие преимущества даст нам такой подход? Мы можем распространять такой DSL как клиентскую библиотеку. Представьте, что вы пишете микросервис с API на GraphQL. Мы можем разделить этот микросервис на 2 модуля - API и реализацию. В модуле API мы разместим схему GraphQL, и сгенерируем по этой схеме клиентский DSL. Затем мы опубликуем сгенерированный DSL в Maven репозитории, и любой клиент нашего микросервиса сможет подключить к себе этот артефакт как зависимость. Мы не знаем заранее, какие запросы потребуются клиентам нашего микросервиса, но нам и не нужно этого знать, так как сгенерированный DSL позволит выполнить любой запрос.

    Еще один неочевидный бонус такого подхода заключается в том, что если наш микросервис изменит свой API, то мы обо всех проблемах несовместимости узнаем во время компиляции, подключив новую версию клиентского DSL.

    Для генерации клиентского Kotlin DSL по GraphQL схеме можно использовать Kobby Plugin:

    https://github.com/ermadmi78/kobby