Привет! На связи команда разработчиков из Новосибирска.
Нам давно хотелось рассказать сообществу о том, как мы разрабатываем фичи в 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. Все шаги будут сопровождаться коммитами в репозитории.
В итоге в статье мы рассмотрим:
подготовку тестового окружения (шаг 1);
создание новой фичи в проекте (шаги 2, 3, 4);
подключение wire-плагина к common-коду (шаг 5);
настройку модуля фичи из корневой фабрики (шаг 6);
генерацию и настройку gRPC-клиента для iOS (шаги 7, 8);
реализацию KMM-интерфейса через нативный gRPC-клиент (шаг 8);
проверку работы gRPC-клиента внутри фичи (шаг 9).
А также расскажем, что делать в 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:
й примитивный вариант конвертации 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 и особенно новыми нестандартными задачами в ней.
До новых встреч!