怠慢プログラマーの備忘録

怠慢でナマケモノなプログラマーの備忘録です。

【Kotlin Multiplatform Mobile】 Ktorを使ったApiClientをSwiftのCombineで使う(備忘録)

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)

みたいな感じになります。

【Kotlin Multiplatform Mobile】build.gradle経由でXCFrameworkを作成する(備忘録)

前回の記事でiOS向けのframeworkを生成してiOS AppにEmbedするところまでいきました。

yutaabe200.hatenablog.com

しかしarm64とx64のアーキテクチャそれぞれのframeworkを生成するので現実的ではありません。 そこでそれぞれのアーキテクチャのframeworkを内包したxcframeworkをbuild.gradle経由で作成する方法が下記になります。

続きを読む

KMM Shared Moduleの追加[備忘録]

初期アプリからKMM導入は過去記事参考

yutaabe200.hatenablog.com

今回は現在進行形で運用されているAndroid/iOSのネイティブアプリに部分的にKMMを導入したい事例の為にSharedModuleを使用したパターンの手順です。

続きを読む

Kotlin Multiplatform Mobile入門(備忘録)

モチベーション

  • AndroidiOSもネイティブで作れるけど疲れた
  • Kotlin大好き
  • Android大好き
  • AndroidStudioはXcodeに比べると神
  • iOSは好きだけどSwiftはKotlinに比べるとそんな好きじゃない
  • iOS SDKをKotlinで触りたいと2017年からずっと思ってた
  • 個人アプリがiOS先行、Androidほぼ未着手だからいい実験台がある
  • 自分のスキルスタック的にDartは除外(なんか肌に合わなかった)、ReactNativeもそこそこいけるけど現状アサインしている会社・プロジェクト周辺で特段Webフロントリソースは潤沢なのにモバイルエンジニアが...みたいなシーンがない
  • 基本的にドメインロジック以降が共通化できればよくて、UIはSwiftUI/Jetpack Composeを先々使っていきたいマン
  • UIはそれぞれのOSで異なった方が理想だと考える宗教
  • 最終的にはフルスクラッチで導入より、既存各ネイティブ構成に導入できるところまで
  • やるぞ!!!
続きを読む

Notification Service ExtensionのアーカイブでMultiple commands produce 'GoogleUtilities.framework'が出て困った話(備忘録)

Notification Service Extensionを追加時にPodfileに以下のように追加しました。

続きを読む

既存プロジェクトへSwiftUIの導入する時の話

2021年も早いもので既に3ヶ月が経過しようとしている中で、2020年は主にAndroidのお仕事が多かったのですが、年明けからなぜかシフトしてiOSを見ることが多くなりました。

最近さまざまなところで顔を出していますが、既存アプリにSwiftUIを実際にプロダクション導入することも増えてきたので、SwiftUI導入するにあたって感じたことを好き勝手に書いていきます。

続きを読む

【SwiftUI】ScrollViewで列挙したリストにpull to refreshの挙動を追加する[備忘録]

SwiftUIのScrollViewにはPull to refreshのようなコンポーネントはUIScrollViewのようには付属されていないので自作した時のメモです。

続きを読む

趣味が欲しかったので「美容」にしてみた

20歳くらいからホームページの作成から始まってそれがソフトウェアに、そしてスマホアプリにと媒体は変化したものの、一貫して趣味は「プログラミング」として生きてきましたが、そろそろ他の趣味というかハマるものがもう少し欲しくて「美容(体のメンテナンス含む)」を頑張ってみようかと思ったのでその心意気です。

続きを読む

最近リリースしたアプリの技術スタックと事業ドメインに関して

最近こんなアプリを作りました。

体調管理・子育て日記アプリ COTETE[コテテ]

体調管理・子育て日記アプリ COTETE[コテテ]

  • Yuta Abe
  • ヘルスケア/フィットネス
  • 無料
apps.apple.com

事業ドメインに関して「えっ?」って思いそうなのですが経緯は以下の通りです。

続きを読む

次世代LiveDataであるKotlin Coroutines StateFlow・SharedFlowについて

Kotlin Coroutines1.3.6で投入されたStateFlow、1.4.0で投入されたSharedFlowに関しての備忘録です。

続きを読む

「事業がわかるエンジニア」を深堀りする

www.timakin.com

今ホットな当話題に関して、個人的にすごく抽象的で一見上記記事内で言う「経営層」と「エンジニア」のそれぞれの領域を曖昧にしてしまう内容にも見ることができる気がしたので、ちょっと書いておきます。

そしてこの記事も個人的なものなので、これに対する見解はそれぞれ異なっていいものだと思ってます。

「事業がわかる」エンジニアとは

記事内で、

技術特化で成果を出す

チーム内の改善、UX改善の提案・実装にコミットする

他部署を巻きこみプロジェクトを推進できる

経営ビジョンを加味した技術選定、組織編成、戦略決定ができる

とあります。その中で経営層が「事業がわかる」と評価する「経営ビジョンを加味した技術選定、組織編成、戦略決定ができる」に若干のミスリード?行きすぎる点があるように感じます。

なぜなら「経営ビジョンを加味した技術選定、組織編成、戦略決定ができる」と言う課題や最終的な意思決定を達成すべきなのはエンジニアやプログラマーではなく経営層であるからです。

「事業がわかるエンジニア」がみな「経営ビジョンを加味した技術選定、組織編成、戦略決定」をしてよいとするのであれば事態の収拾がつかないことになりかねません。

その「事業がわかるエンジニア」を善としてみながそれを目指し、遂行し始めたら組織構造のレイヤーにかかわらず皆が組織編成に口を出し、戦略決定事項に異議を発言し、最終的には「この会社はエンジニアの意見を尊重してくれない」などの理由でその組織を去っていくこともあります。

つまりここで言う「事業がわかるエンジニア」とは「経営層(マネジメント層)」であるべきと考えます。

(仮にここで言う「事業のわかるエンジニア」が「経営ビジョンを加味した技術選定、組織編成、戦略決定」を行っていたとして、またその上層や別に事業の意思決定をするポジションの人間が他にいたとしたらその人たちは何をするのでしょうか?と私は疑問に思います。)

ではエンジニアやプログラマーは事業に対してどのような姿勢でコミットすべきか?と言う疑問に関しては、「経営ビジョンを加味した技術選定、組織編成、戦略決定のそれぞれの選択肢やその選択肢のバックグラウンドを経営層に正しく伝えることができるか」と言う視点で留めておくべきだと思います。

事業の遂行には多くの様々なシーンで意思決定を行う必要があるかと思います。その度にその事業の責任者や経営層、スタートアップなどでは社長が行っているかと思いますが、かなりの心身的な責任・負担がのしかかります。

その責任・負担を達成・解決するのはその負担を背負っている人物あるいはレイヤーでしか行えないものだと認識しています。

ただその事業遂行の中でエンジニアリング観点での意思決定が必要なシーンに「エンジニアが経営層に選択肢をそれぞれ提示する」必要があるかと思います。

その上で経営層あるいはマネジメント層が経営ビジョンと照らし合わせて最適な意思決定を行っていくべきだと思います。

つまりここではエンジニア・プログラマーは「事業がわかるエンジニア」ではなく「事業に沿った選択肢を考慮できるエンジニア」であるべきかと思います。

そして本当の意味で「事業がわかるエンジニア」は「エンジニアリングがわかる経営者・事業責任者」である必要があるかとも思います。

おわり

この記事を書いた理由は、該当記事の認識を間違って「じゃあ経営・マネジメントレベルの意思決定にも積極的に口を出した方が喜ばれるんだ!」とエンジニアが誤認してしまったり「うちのエンジニアたちは経営者視点・当事者意識が薄い」みたいな責任転嫁とも取れるようなことを感じてしまう経営者が増えないといいなぁ、と言う思いで書いてみました。

またエンジニアリングとビジネスのそれぞれの責任とそれを負うべきポジションの人達が、変に責任の押し付け合いや責任転嫁からくる不満を持たないで欲しいので、うまく役割分担して欲しいなぁと思ってます。