Kotlin: 1.7.0.
KMM Plugin: 0.3.4.
Ktor: 2.0.3.
SharedModule/build.gradle.kts
sourceSets { val commonMain by getting { dependencies { //Network implementation("io.ktor:ktor-client-core:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-logging:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-serialization:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-json:${findProperty("version.ktor")}") implementation("io.ktor:ktor-serialization-kotlinx-json:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-content-negotiation:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-auth:${findProperty("version.ktor")}") implementation("org.jetbrains.kotlinx:kotlinx-datetime:${findProperty("version.kotlin_datatime")}") } } val androidMain by getting { dependencies { //Network implementation("io.ktor:ktor-client-okhttp:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-android:${findProperty("version.ktor")}") } } val androidTest by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting val iosX64Main by getting val iosMain by creating { dependsOn(commonMain) iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) iosX64Main.dependsOn(this) dependencies { //Network implementation("io.ktor:ktor-client-ios:${findProperty("version.ktor")}") implementation("io.ktor:ktor-client-darwin:${findProperty("version.ktor")}") } } }
commonMain/ApiClient.kt
expect class SharedApiClient(bearerToken: String?) { val client: HttpClient val dispatcher: CoroutineDispatcher }
androidMain/ApiClient.kt
actual class SharedApiClient actual constructor(private val bearerToken: String?) { actual val client: HttpClient = HttpClient { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true }) } install(DefaultRequest) { header(HttpHeaders.ContentType, ContentType.Application.Json) } bearerToken?.let { install(Auth) { bearer { loadTokens { // Load tokens from a local storage and return them as the 'BearerTokens' instance BearerTokens(bearerToken, "") } } } } } actual val dispatcher: CoroutineDispatcher = Dispatchers.Default }
iosMain/ApiClient.kt
actual class SharedApiClient actual constructor(private val bearerToken: String?) { actual val client: HttpClient = HttpClient(Darwin) { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true allowSpecialFloatingPointValues = true useArrayPolymorphism = false }) } install(Auth) { bearer { loadTokens { // Load tokens from a local storage and return them as the 'BearerTokens' instance bearerToken?.let { BearerTokens(bearerToken, "") } } } } } actual val dispatcher: CoroutineDispatcher = Dispatcher(dispatch_get_main_queue()) } class Dispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatchQueue) { block.run() } } }
commonMain/SharedNoteRequest.kt
class SharedNoteRequest(private val token: String?, private val apiClient: SharedApiClient = SharedApiClient(token), private val url: String = "https://test.com/v1/notes", ) { suspend fun getNotes(): SharedNoteEntity { return apiClient.client.get(url).body() } }
gradle.properties
kotlin.native.binary.memoryModel=experimental
↑これがないとiOS側でクラッシュします。
以上のSharedModuleをxcframework化してSwiftPackageManagerで配布しています(この辺りの詳細は割愛)。
NoteRepository.swift
import Combine import Foundation import FractalSharedModule public protocol NoteRepositoryProtocol { func fetchNotes() -> AnyPublisher<Note, Error> } public final class NoteRepository: NoteRepositoryProtocol { public init() {} public func fetchNotes() -> AnyPublisher<Note, Error> { return Future<Note, Error> { promise in let apiClient = SharedApiClient() SharedNoteRequest(apiClient: apiClient, url: "https://test.com/api/v1/notes") .getNotes(completionHandler: { entity, error in if let error { debugPrint(error.localizedDescription) promise(.failure(error)) } if let entity { // SharedModuleのEntityをiOS側で使うmodelに変換してるだけ promise(.success(SharedTranslator.translator(shared: entity))) } }) } .eraseToAnyPublisher() } }
後は使いたいところで、
repository.fetchNotes(path: "magazines/m7a867df0f98f/notes") .subscribe(on: DispatchQueue.global()) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in if case .failure(let error) = completion { fatalError(error) } }, receiveValue: { note in // successfully } ) .store(in: &cancellables)
みたいな感じになります。