Привет! На связи команда разработчиков из Новосибирска.
Нам давно хотелось рассказать сообществу о том, как мы разрабатываем фичи в 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. Поэтому мы взяли ее и разобрались, что мы реально можем сделать:
Настроить генерацию KMM-кода для классов сообщений и для gRPC-клиента, должно даже на корутинах работать. Пример настройки плагина есть на сайте gRPC.
Из коробки есть реализация клиента для Android, которая под капотом использует OkHttp от этой же команды разработчиков. В клиенте есть возможность устанавливать параметры запросов, используя OkHttpClient.Builder.addInterceptor.
Из коробки нет реализации клиента для 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 для версионирования зависимостей. С него и начинаем:
Добавляем версию
wireв секцию [versions]:
# wire wireVersion = "4.0.0-alpha.15"
Добавляем библиотеки и плагин в секцию [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:
Поскольку
Wireхостится наjitpack.io, убедимся, что все плагины будут скачиваться в том числе и оттуда в/build-logic/build.gradle.kts:
repositories { mavenCentral() google() gradlePluginPortal() maven("https://jitpack.io") }
И здесь же сам плагин в
dependencies:
dependencies { ... api("com.squareup.wire:wire-gradle-plugin:4.0.0-alpha.15") }
Далее работаем с domain-модулем, добавляем плагин в секцию
pluginsв/mpp-library/domain/build.gradle.kts:
... id("com.squareup.wire") }
Добавляем в секцию
dependenciesбиблиотеку клиента и рантайма:
... commonMainImplementation(libs.wireGrpcClient) commonMainImplementation(libs.wireRuntime) }
Добавляем секцию
wireв конец файла и синхронизируем проект:
wire { sourcePath { srcDir("./src/proto") } kotlin { rpcRole = "client" rpcCallStyle = "suspending" } }
После синхронизации проекта появляется gradle-таска
generateProtos:

Итоги ее выполнения можно найти в
/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 реализацию этого интерфейса и передать на вход фабрики фичи.
Добавляем репозиторий как параметр в фабрику фичи
GrpcTestFactory.kt(коммит):
class GrpcTestFactory( private val repository: GrpcTestRepository ) { fun createViewModel( eventsDispatcher: EventsDispatcher<GrpcTestViewModel.EventsListener>, ) = GrpcTestViewModel( eventsDispatcher = eventsDispatcher, repository = repository ) }
Добавляем новое поле в конструкторы
SharedFactoryи сразу для кастомного конструктора используемsuspend-обертку клиента:
class SharedFactory( ... helloWorldClient: HelloWorldSuspendClient ) { //Специально для вызова со стороны iOS-платформы мы не используем аргумент со значением «по умолчанию» constructor( ... helloWorldCallbackClient: HelloWorldCallbackClient ) : this( ... helloWorldClient = HelloWorldSuspendClientImpl(helloWorldCallbackClient) ) ...
Создаем экземпляр этой фабрики, используем 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/, и теперь их можно использовать следующим образом:
Сделать папку для сгенерированных классов, например,
/ios-app/src/generated/proto.Находясь в корне проекта, вызвать команду для генерации классов сообщений:
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
Находясь в корне проекта, вызвать команду для генерации методов 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:
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 )
Итоги
Конечно, в приведенном решении еще много чего можно улучшить:
Заменить долгую реализацию копирования NSData в KotlinByteArray на использование memcpy.
Добавить в интерфейс клиента метод для установки значений заголовков запросов и пересоздавать канал и клиенты при его вызове.
Реализовать универсальный маппинг сообщений из
WireMessageвSwiftMessage.
Да и сам шаблон проекта мы еще будем развивать и дорабатывать. Надеемся, что цель статьи достигнута, и всем осилившим будет интересно заниматься разработкой на KMM и особенно новыми нестандартными задачами в ней.
До новых встреч!
