Как использовать gRPC-клиент в проекте на Kotlin Multiplatform Mobile

от автора

Привет! На связи команда разработчиков из Новосибирска.

Нам давно хотелось рассказать сообществу о том, как мы разрабатываем фичи в KMM-проектах, и вот на одном из них подвернулась хорошая нестандартная задача. На ней, помимо собственно решения задачи, продемонстрируем путь добавления новой фичи в проект. Также мы очень хотим продвигать мультиплатформу именно в среде iOS-разработчиков, поэтому бонусом делаем особый акцент на этой платформе.

В чем суть задачи

Обычно в мобильных проектах общение с бэкендом происходит по REST API и спецификация оформляется в swagger-файлах. При таком раскладе мы спокойно используем Ktor и нашу библиотеку moko-network, в которой используем плагин для генерации кода запросов и моделей ответов по Swagger‘у. В очень редких случаях требовалось дополнительно немного использовать WebSockets или Sockets.IO. Это решалось индивидуально на каждой платформе. Позднее мы сделали для этого библиотеку moko-sockets-io.

В этот раз ситуация была интереснее: помимо набора swagger-файлов мобильный API был представлен несколькими gRPC-сервисами, и нам сразу же захотелось сделать процесс работы с ними максимально комфортным и приближенным к работе с REST API.

В статье описан полный путь интеграции gRPC в мультиплатформенный проект, пройденный нашей командой. Он включает и создание проекта, и настройку фичи в проекте. Если вас интересует gRPC-специфичная часть и вы уже обладаете знаниями о мультиплатформе, то шаги 2, 3 и 4 можно пропустить.

Для интеграции мы сразу же начали искать готовые библиотеки. В идеале хотелось следующего:

  • уметь генерировать kotlin-классы для моделей сообщений в common-коде;

  • уметь генерировать kotlin-классы для gRPC-клиента в common-коде;

  • иметь из коробки реализации этих классов для iOS и Android;

  • уметь настраивать gRPC-клиент из общего кода: подставлять адрес сервера, заголовки авторизации.

На тот момент нашлась только одна библиотека для работы с gRPC, в которой KMM-часть была реализована и поддерживалась, — Wire от коллег из Square. Поэтому мы взяли ее и разобрались, что мы реально можем сделать:

  1. Настроить генерацию KMM-кода для классов сообщений и для gRPC-клиента, должно даже на корутинах работать. Пример настройки плагина есть на сайте gRPC.

  2. Из коробки есть реализация клиента для Android, которая под капотом использует OkHttp от этой же команды разработчиков. В клиенте есть возможность устанавливать параметры запросов, используя OkHttpClient.Builder.addInterceptor.

  3. Из коробки нет реализации клиента для iOS, только интерфейс с заглушками.

Очевидно, что со стороны iOS библиотека не готова. Однако мы решили попробовать использовать хотя бы часть инструментов из нее: задачу решать надо, при этом со стороны Android все уже должно работать хорошо.

Основной путь решения проблемы продемонстрируем на проекте Hello world, заодно покажем, как с нулевого состояния поднять проект на основе шаблона и добавить туда новую фичу. Основной упор будет на iOS-платформу. В качестве спецификации возьмем готовый пример из gprc-go. Все шаги будут сопровождаться коммитами в репозитории.

В итоге в статье мы рассмотрим:

А также расскажем, что делать в Android-приложении.

Шаг 1. Подготавливаем тестовое окружение

Здесь все просто — берем из примера команды для установки сервера и клиента:

```  $ go get google.golang.org/grpc/examples/helloworld/greeter_client  $ go get google.golang.org/grpc/examples/helloworld/greeter_server  ```

Затем выполняем запуск в разных терминалах:

```  $ ~/go/bin/greeter_server  2022/02/13 20:04:13 server listening at 127.0.0.1:50051  2022/02/13 20:04:20 Received: world  ```  ```  $ ~/go/bin/greeter_client  2022/02/13 20:04:20 Greeting: Hello world  ```

Теперь терминал с клиентом нам не понадобится. Закрываем клиент, а сервер оставляем работать: вернемся к нему ближе к концу статьи.

Шаг 2. Стартуем новый MPP-проект

Мы в IceRock уже довольно давно для старта мультиплатформенных проектов используем свой шаблон и сейчас начнем с него же. Генерируем по нему проект на GitHub, импортируем всю папку в Android Studio или IDEA и смотрим, что для нас уже настроено.

В mpp-library/feature видим две готовые фичи — config и list:

Еще есть реализация доменной логики для них в отдельном пакете domain:

Связывающая их фабрика в корне пакета mpp-library:

Шаг 3. Добавляем новый модуль фичи

Для ускорения скопируем модуль config с новым именем. Например, grpcTest. Почистим от логики и переименуем файлы:

Содержимое новых файлов (коммит):

  • /model/GrpcTestRepository.kt — интерфейс доменной логики для фичи, предоставляется из корневой фабрики проекта SharedFactory:

```  package org.example.library.feature.grpcTest.model  interface GrpcTestRepository {  }  ```
  • /presentation/GrpcTestViewModel.kt — пустая вью-модель. Она наследуется от dev.icerock.moko.mvvm.viewmodel.ViewModel, поэтому имеет coroutine scope для выполнения асинхронных вызовов. Также в ней объявляем интерфейс событий, которые вью-модель может кидать на платформенную часть и принимаем диспетчер этих событий (eventsDispatcher) в качестве параметра:

```  package org.example.library.feature.grpcTest.presentation  import dev.icerock.moko.mvvm.dispatcher.EventsDispatcher  import dev.icerock.moko.mvvm.dispatcher.EventsDispatcherOwner  import dev.icerock.moko.mvvm.viewmodel.ViewModel  import org.example.library.feature.grpcTest.model.GrpcTestRepository  class GrpcTestViewModel(     override val eventsDispatcher: EventsDispatcher<EventsListener>,     private val repository: GrpcTestRepository  ) : ViewModel(), EventsDispatcherOwner<GrpcTestViewModel.EventsListener> {     interface EventsListener {     }  }  ```
  • /di/GrpcTestFactory.kt — фабрика вью-модели для фичи. Создается в корневой фабрике проекта SharedFactory. Там же решается, какой будет реализация репозитория. Методы фабрики вызываются с нативной платформы:

```  package org.example.library.feature.grpcTest.di  import dev.icerock.moko.mvvm.dispatcher.EventsDispatcher  import org.example.library.feature.grpcTest.model.GrpcTestRepository  import org.example.library.feature.grpcTest.presentation.GrpcTestViewModel  class GrpcTestFactory(    private val repository: GrpcTestRepository  ) {      fun createViewModel(          eventsDispatcher: EventsDispatcher<GrpcTestViewModel.EventsListener>,      ) = GrpcTestViewModel(          eventsDispatcher = eventsDispatcher,          repository = repository      )  }  ```

EventsDispatcher реализован здесь и нужен для гарантированной отправки событий на платформу. Для iOS это будет происходить по умолчанию на главной очереди. Для Android — в рамках главного цикла.

Также добавим путь до модуля фичи в settings.gradle.kts в корне проекта (коммит):

```  include(":mpp-library:feature:grpcTest")  ```

Подключим модуль фичи к модулю mpp-library в /mpp-library/build.gradle.kts (коммит):

```  ...  dependencies {  ...  commonMainApi(projects.mppLibrary.feature.grpcTest) //Чтобы видеть классы фичи в SharedFactory  ...  }  ...  framework {    ...    export(projects.mppLibrary.feature.grpcTest)  // Чтобы классы фичи попали в фреймворк для iOS    ...  }  ```

И не забываем переименовать пакет в AndroidManifest.xml (коммит):

```  <?xml version="1.0" encoding="utf-8"?>  <manifest package="org.example.library.feature.grpcTest" />  ```

Шаг 4. Пишем логику фичи

Функции клиента у нас очень простые: нужно будет инициировать запрос и показать на экране ответ. Для использования метода объявим его в GrpcTestRepository (коммит):

```  interface GrpcTestRepository {      suspend fun helloRequest(word: String): String  }  ```

Для отображения текста в алерте (текст успешного ответа от сервера или текст ошибки) добавим новое событие в EventsListener (коммит):

```  interface EventsListener {      fun showMessage(message: String)  }  ```

Для отправки запроса сделаем метод в GrpcTestViewModel, который будем вызывать с нативной стороны по какому-нибудь событию. Заодно покажем ошибку, если что-то пойдет не так (коммит):

```  fun onMainButtonTap() {      viewModelScope.launch {          var message: String = ""          try {              message = repository.helloRequest("world")          } catch (exc: Exception) {              message = "Error: " + (exc.message ?: "Unknown error")          }          eventsDispatcher.dispatchEvent { showMessage(message) }      }  }  ```

Общий код модуля фичи на этом готов, теперь нужна имплементация собственно grpc-запросов и наша вью-модель с нативной стороны.

Шаг 5. Подключаем генерацию моделей сообщений по proto-файлам

Для начала берем файл спецификации нашего клиента helloworld.proto и помещаем в папку /domain/src/proto:

Теперь нужно будет очень аккуратно подключить wire-плагин к доменному модулю. Все шаги из этого блока намеренно собраны в один коммит, чтобы при воспроизведении не потеряться.

Мы используем libs.versions.toml для версионирования зависимостей. С него и начинаем:

  1. Добавляем версию wire в секцию [versions]:

``` # wire wireVersion = "4.0.0-alpha.15" ```
  1. Добавляем библиотеки и плагин в секцию [libraries]:

``` # wire wireGradle = { module = "com.squareup.wire:wire-gradle-plugin", version.ref = "wireVersion"} wireRuntime = { module = "com.squareup.wire:wire-runtime", version.ref = "wireVersion"} wireGrpcClient = { module = "com.squareup.wire:wire-grpc-client", version.ref = "wireVersion"} ``` 

Затем цепляем сам плагин и настраиваем в /mpp-library/domain/build.gradle.kts:

  1. Поскольку Wire хостится на jitpack.io, убедимся, что все плагины будут скачиваться в том числе и оттуда в /build-logic/build.gradle.kts:

``` repositories {     mavenCentral()     google()      gradlePluginPortal()     maven("https://jitpack.io") } ```
  1. И здесь же сам плагин в dependencies:

``` dependencies {   ...   api("com.squareup.wire:wire-gradle-plugin:4.0.0-alpha.15") } ```
  1. Далее работаем с domain-модулем, добавляем плагин в секцию plugins в /mpp-library/domain/build.gradle.kts:

```   ...   id("com.squareup.wire") } ```
  1. Добавляем в секцию dependencies библиотеку клиента и рантайма:

```   ...   commonMainImplementation(libs.wireGrpcClient)   commonMainImplementation(libs.wireRuntime) }
  1. Добавляем секцию wire в конец файла и синхронизируем проект:

``` wire {     sourcePath {       srcDir("./src/proto")     }     kotlin {         rpcRole = "client"         rpcCallStyle = "suspending"     } } ```
  1. После синхронизации проекта появляется gradle-таска generateProtos:

  1. Итоги ее выполнения можно найти в /mpp-library/domain/build/generated/source:

Здесь у нас довольно объемные сгенерированные классы для запроса (HelloRequest) и ответа (HelloReply) метода, интерфейс клиента (GreeterClient) и его gRPC-реализация (GrpcGreeterClient).

Забегая вперед: на Android мы используем все эти классы, на iOS — только классы сообщений.

Шаг 6. Объявляем MPP-интерфейс для gRPC-клиента

На текущий момент у нас есть сгенерированные модельки HelloReply и HelloRequest и интерфейс для репозитория конечной фичи GrpcTestRepository. Поскольку использовать сгенерированный готовый клиент в общем коде мы не сможем, нужно объявить его интерфейс, а реализовать по отдельности на платформах.

В нашем случае интерфейс gRPC-клиента будет выглядеть так:

``` interface HelloWorldSuspendClient {     suspend fun sendHello(message: HelloRequest): HelloReply } ```

Однако для iOS реализовать интерфейс с suspend-методами не получится, поэтому понадобится еще один интерфейс на callback‘ах:

``` interface HelloWorldCallbackClient {     fun sendHello(message: HelloRequest, callback: (HelloReply?, Exception?) -> Unit) } ```

И реализация, переводящая методы с callback‘ами в suspend-методы:

``` class HelloWorldSuspendClientImpl(     private val callbackClientCalls: HelloWorldCallbackClient ): HelloWorldSuspendClient {      //Пока что у нас в интерфейсе всего один метод, но на будущее очень пригодится generic-функция для конвертации, сразу реализуем ее     private suspend fun <In, Out> convertCallbackCallToSuspend(         input: In,         callbackClosure: ((In, ((Out?, Throwable?) -> Unit)) -> Unit),     ): Out {         return suspendCoroutine { continuation ->             callbackClosure(input) { result, error ->                 when {                     error != null -> {                         continuation.resumeWith(Result.failure(error))                     }                     result != null -> {                         continuation.resumeWith(Result.success(result))                     }                     else -> { //both values are null                         continuation.resumeWith(Result.failure(IllegalStateException("Incorrect grpc call processing")))                     }                 }             }         }     }      override suspend fun sendHello(message: HelloRequest): HelloReply {         return convertCallbackCallToSuspend(message, callbackClosure = { input, callback ->             callbackClientCalls.sendHello(input, callback)         })     } } ```

Размещаем все это там же, где генерировали модельки, в domain-модуле (коммит):

Теперь в общем коде осталось только принять на вход в SharedFactory реализацию этого интерфейса и передать на вход фабрики фичи.

  1. Добавляем репозиторий как параметр в фабрику фичи GrpcTestFactory.kt (коммит):

``` class GrpcTestFactory(     private val repository: GrpcTestRepository ) {     fun createViewModel(         eventsDispatcher: EventsDispatcher<GrpcTestViewModel.EventsListener>,     ) = GrpcTestViewModel(         eventsDispatcher = eventsDispatcher,         repository = repository     ) } ```
  1. Добавляем новое поле в конструкторы SharedFactory и сразу для кастомного конструктора используем suspend-обертку клиента:

``` class SharedFactory(     ...     helloWorldClient: HelloWorldSuspendClient ) {   //Специально для вызова со стороны iOS-платформы мы не используем аргумент со значением «по умолчанию» constructor(     ...     helloWorldCallbackClient: HelloWorldCallbackClient ) : this(     ...     helloWorldClient = HelloWorldSuspendClientImpl(helloWorldCallbackClient) ) ... ```
  1. Создаем экземпляр этой фабрики, используем gRPC-клиент как репозиторий (коммит):

``` val grpcTestFactory = GrpcTestFactory(     repository = object : GrpcTestRepository {         override suspend fun helloRequest(word: String): String {             return helloWorldClient.sendHello(HelloRequest(word)).message         }     } ) ``` 

В общем коде все готово, осталось реализовать gRPC-клиент со стороны платформ.

Шаг 7. iOS: генерация классов gRPC-клиента

Для генерации классов возьмем библиотеку и генератор gRPC-Swift. Сначала поставим генератор, например через Homebrew:

``` brew install swift-protobuf grpc-swift ```

Затем нам понадобятся плагины к нему, устанавливаются через cocoapods:

``` pod 'gRPC-Swift-Plugins' ```

Если все прошло успешно, то оба плагина появятся по пути /ios-app/Pods/gRPC-Swift-Plugins/bin/, и теперь их можно использовать следующим образом:

  1. Сделать папку для сгенерированных классов, например, /ios-app/src/generated/proto.

  2. Находясь в корне проекта, вызвать команду для генерации классов сообщений:

``` protoc \ --plugin=./ios-app/Pods/gRPC-Swift-Plugins/bin/protoc-gen-swift \ --swift_out=./ios-app/src/generated/proto \ --proto_path=./mpp-library/domain/src/proto \ ./mpp-library/domain/src/proto/helloworld.proto ```
  1. Находясь в корне проекта, вызвать команду для генерации методов gRPC-клиента:

``` protoc \ --plugin=./ios-app/Pods/gRPC-Swift-Plugins/bin/protoc-gen-grpc-swift \ --grpc-swift_out=./ios-app/src/generated/proto \ --grpc-swift_opt=Client=true,Server=false \ --proto_path=./mpp-library/domain/src/proto \ ./mpp-library/domain/src/proto/helloworld.proto ```

В итоге получаем два файла: helloworld.grpc.swift, helloworld.pb.swift. Добавляем их в проект и в Podfile саму библиотеку gRPC-Swift (коммит):

``` pod 'gRPC-Swift', '~> 1.7.0' ```

Шаг 8. iOS: реализация HelloWorldClient

Создаем новый класс, реализующий HelloWorldCallbackClient. Сделаем так, чтобы при его инициализации сразу создавались и сохранялись gRPC-канал и gRPC-клиент:

``` class HelloWorldCallbackBridge: HelloWorldCallbackClient {      private var commonChannel: GRPCChannel?     private var helloClient: Helloworld_GreeterClient?      init() {          //Настраиваем логгер         var logger = Logger(label: "gRPC", factory: StreamLogHandler.standardOutput(label:))         logger.logLevel = .debug          //loopCount — сколько независимых циклов внутри группы работают внутри канала (могут одновременно отправлять/принимать сообщения)         let eventGroup = PlatformSupport.makeEventLoopGroup(loopCount: 4)          //Создаем канал, указываем тип защищенности, хост и порт         let newChannel = ClientConnection             //Можно вместо .insecure использовать .usingTLS, но к нашему тестовому серверу так подключиться не выйдет, у него нет сертификата             .insecure(group: eventGroup)             //Логгируем события самого канала             .withBackgroundActivityLogger(logger)             .connect(host: "127.0.0.1", port: 50051)          //Работаем без дополнительных заголовков, логгируем запросы         let callOptions = CallOptions(             customMetadata: HPACKHeaders([]),             logger: logger         )          //Создаем и сохраняем экземпляр клиента         helloClient = Helloworld_GreeterClient(             channel: newChannel,             defaultCallOptions: callOptions,             interceptors: nil         )         //Сохраняем канал         commonChannel = newChannel     } ... ```

Реализуем метод sayHello(..):

``` func sendHello(message: HelloRequest, callback: @escaping (HelloReply?, KotlinException?) -> Void) {     //Проверяем что все идет по плану     guard let client = helloClient else {         callback(nil, nil)         return     }      //Создаем SwiftProtobuf.Message из WireMessage     var request = Helloworld_HelloRequest()     request.name = message.name      //Получаем экземпляр вызова     let responseCall = client.sayHello(request)     DispatchQueue.global().async {         do {             //В фоне дожидаемся результата вызова             let swiftMessage = try responseCall.response.wait()             DispatchQueue.main.async {                 //Конвертируем SwiftProtobuf.Message в WireMessage (объект ADAPTER умеет парсить конкретный класс WireMessage из бинарного формата)                 let (wireMessage, mappingError) = swiftMessage.toWireMessage(adapter: HelloReply.companion.ADAPTER)                 //Обязательно вызываем callback на том же потоке на котором фактически создался wireMessage, иначе получим ошибку в KotlinNative-рантайме                 callback(wireMessage, mappingError)             }         } catch let err {             DispatchQueue.main.async {                 callback(nil, KotlinException(message: err.localizedDescription))             }         }     } } ```

Функция toWireMessage(..) довольно простая: она берет представление SwiftMessage в виде NSData, переводит в KotlinByteArray и отдает на вход адаптеру:

``` fileprivate extension SwiftProtobuf.Message {     func toWireMessage<WireMessage, Adapter: Wire_runtimeProtoAdapter<WireMessage>>(adapter: Adapter) -> (WireMessage?, KotlinException?) {         do {             let data = try self.serializedData()             let result = adapter.decode(bytes: data.toKotlinByteArray())              if let nResult = result {                 return (nResult, nil)             } else {                 return (nil, KotlinException(message: "Cannot parse message data"))             }         } catch let err {             return (nil, KotlinException(message: err.localizedDescription))         }     } } ```

Самый примитивный вариант конвертации NSData в KotlinByteArray:

й примитивный вариант конвертации NSData в KotlinByteArray: ``` fileprivate extension Data {     //Побайтово копируем NSData в KotlinByteArray     func toKotlinByteArray() -> KotlinByteArray {         let nsData = NSData(data: self)          return KotlinByteArray(size: Int32(self.count)) { index -> KotlinByte in             let byte = nsData.bytes.load(fromByteOffset: Int(truncating: index), as: Int8.self)             return KotlinByte(value: byte)         }     } } ```

Сохраняем все и пробуем проверить прямо в AppDelegate (коммит):

``` @UIApplicationMain class AppDelegate: NSObject, UIApplicationDelegate {      var window: UIWindow?      let gRPCClient = HelloWorldCallbackBridge()      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {          let request = HelloRequest(name: "AppDelegate", unknownFields: OkioByteString.companion.EMPTY)         gRPCClient.sendHello(message: request) { reply, error in             print("Reply: \(reply?.message) - Error: \(error?.message)")         }         return true     } } ```

В терминале с запущенным сервером увидим сообщение:

``` 2022/02/17 23:51:28 Received: AppDelegate ```

А в консольном выводе XCode — много логов по состоянию канала и наш print:

```  2022-02-17T23:51:27+0700 debug gRPC : old_state=idle grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 new_state=connecting connectivity state change  2022-02-17T23:51:27+0700 debug gRPC : grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 connectivity_state=connecting vending multiplexer future  2022-02-17T23:51:27+0700 debug gRPC : grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 making client bootstrap with event loop group of type NIOTSEventLoop  2022-02-17T23:51:27+0700 debug gRPC : grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 Network.framework is available and the EventLoopGroup is compatible with NIOTS, creating a NIOTSConnectionBootstrap  2022-02-17 23:51:28.487194+0700 mokoApp[34306:38235189] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed  2022-02-17T23:51:28+0700 debug gRPC : connectivity_state=connecting grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 activating connection  2022-02-17T23:51:28+0700 debug gRPC : h2_settings_max_frame_size=16384 grpc.conn.addr_remote=127.0.0.1 grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 grpc.conn.addr_local=127.0.0.1 HTTP2 settings update  2022-02-17T23:51:28+0700 debug gRPC : connectivity_state=active grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 connection ready  2022-02-17T23:51:28+0700 debug gRPC : grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 old_state=connecting new_state=ready connectivity state change  2022-02-17T23:51:28+0700 debug gRPC : grpc.conn.addr_remote=127.0.0.1 grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 grpc_request_id=682A7FB4-4543-4609-A2C0-498B8A1445A3 grpc.conn.addr_local=127.0.0.1 activated stream channel  2022-02-17T23:51:28+0700 debug gRPC : grpc.conn.addr_local=127.0.0.1 grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 grpc.conn.addr_remote=127.0.0.1 h2_stream_id=HTTP2StreamID(1) h2_active_streams=1 HTTP2 stream created  2022-02-17T23:51:28+0700 debug gRPC : h2_active_streams=0 grpc.conn.addr_remote=127.0.0.1 grpc.conn.addr_local=127.0.0.1 grpc_connection_id=7E6AA2F6-3F83-4448-BEB1-F0C3C85131AD/0 h2_stream_id=HTTP2StreamID(1) HTTP2 stream closed  Reply: Optional("Hello AppDelegate") - Error: nil  ```

Шаг 9. iOS: проверяем работу gRPC-клиента внутри фичи

Пожалуй, не будем создавать новый контроллер. Добавим еще одну вью-модель на ConfigViewController, будем вызывать ее метод при появлении контроллера на экране и показывать алерт по событию из EventsListener (коммит):

``` override func viewDidLoad() {   ...   grpcTestViewModel = AppComponent.factory.grpcTestFactory.createViewModel(eventsDispatcher: EventsDispatcher(listener: self))   }  override func viewDidAppear(_ animated: Bool) {     super.viewDidAppear(animated)     grpcTestViewModel.onMainButtonTap() }  deinit {     //Очищаем вью-модель, чтобы сразу же остановить все корутины     viewModel.onCleared()     grpcTestViewModel.onCleared() } ...  extension ConfigViewController: GrpcTestViewModelEventsListener {     func showMessage(message: String) {         let alert = UIAlertController(title: "gRPC test", message: message, preferredStyle: .alert)         present(alert, animated: true, completion: nil)     } } ```

В результате при запуске приложения получаем:

Что делать для Android-приложений

С стороны Android-платформы можно использовать именно сгенерированный код Wire-клиента, дав ему экземпляр платформенного клиента. Выглядеть это может примерно так:

  • CommonMain-код:

``` class WireClientWrapper(grpcClient: GrpcClient): HelloWorldSuspendClient {     private val greeterClient = GrpcGreeterClient(grpcClient)     override suspend fun sendHello(message: HelloRequest): HelloReply {         return greeterClient.SayHello().execute(message)     } } ```
  • AndroidMain-код:

``` val grpcOkhttpClient = OkHttpClient().newBuilder()     .protocols(listOf(okhttp3.Protocol.HTTP_2, okhttp3.Protocol.HTTP_1_1))     .build()  val grpcClient = GrpcClient.Builder()     .client(grpcOkhttpClient)     .baseUrl("127.0.0.1:50051")     .build()  val helloClient = WireClientWrapper(grpcClient)  return SharedFactory(            settings = settings,            antilog = antilog,            newsUnitsFactory = newsUnitFactory,            baseUrl = BuildConfig.BASE_URL,            helloWorldClient = helloClient         ) ```

Итоги

Конечно, в приведенном решении еще много чего можно улучшить:

  1. Заменить долгую реализацию копирования NSData в KotlinByteArray на использование memcpy.

  2. Добавить в интерфейс клиента метод для установки значений заголовков запросов и пересоздавать канал и клиенты при его вызове.

  3. Реализовать универсальный маппинг сообщений из WireMessage в SwiftMessage.

Да и сам шаблон проекта мы еще будем развивать и дорабатывать. Надеемся, что цель статьи достигнута, и всем осилившим будет интересно заниматься разработкой на KMM и особенно новыми нестандартными задачами в ней.

До новых встреч!


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


Комментарии

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

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