KMPジャーニー


NortonLifeNokでは、我々のモジュールのいくつかの機能を統合しようとして外部チームからの要求があったとして、我々のコードをモジュール化&共有しようとしてきた.それから、我々は、我々のAPI呼び出しまたは会社の内部のチームを露出させるために、細い図書館をつくるという考えでからかっていました.同じ頃、私たちはこの記事に出くわしましたNetflix & KMPがアルファにあったというニュースが出た.明らかに、AndroidとIOSのために共有された非UIライブラリを開発することを計画していたので、これをチェックする価値がありました.
私は約6年間のAndroidアプリケーションを開発しているが、任意のIOSの開発に関するゼロ知識を持っています.私は、我々の旅について、Kotlinのマルチプラットフォームを試して、それを生産について書きたかったです.

スパイク
我々がどんな種類の新しいテクノロジーでも探求するときはいつでも、我々は複雑さに基づいて1/2のためにスパイクをします.KMPのために、我々は1週が評価するのに十分でなければならないと考え始めました、しかし、それは我々にほとんどスプリントをしました.これは我々がスパイクで行ったことです.
  • 最初のサンプルアプリケーションの助けを借りて構築tutorial
    KMP設定ページから.
  • サンプルアプリで私たち自身のAPIの呼び出しを追加&私たちの通常のAndroidアプリからAPIを呼び出す&サンプルIOSのアプリからこの呼び出しを行う
  • 私はチームにこれを発表して、私のチーム/マネージャーをこれに入れさせました.そして、このフレームワーク統合が生産アプリで滑らかであるならば、我々はIOS開発者に試してみたかったです.
  • データクラスがちょうど消費するアプリからSDKに移されたので、APIの統合はAndroid面でより滑らかでした.
  • これはIOSのアプリではなかったので、Android側よりもIOS側に多くの変更があった.だから、我々はKMPモジュールからフレームワークを統合し、IOSのアプリからのネットワークコールを行うことができるかどうかを試してスパイクの目的のために.レスポンスはログでチェックされました、そして、これが成功したならば、我々は我々が前進することができて、それを生産することができると決めました.

  • 実装
  • 私たちは新しいKMPプロジェクトを始めました.しかし、これはスパイクではなく、私は道路ブロックの多くに直面した.
  • 我々はスパイクから作業コードを持っていたので、我々はそれが生産にそれを取るために多くの仕事ではないと思いました.私たちはとても間違っていた!
  • 最初のロードブロックが構築された.我々は、IOSのビルドのためのAndroid&MacマシンのCentOSマシンとCIのジェンキンズを使用します.MacマシンのどれもAndroid SDK/Javaがインストールされていません.それで、AndroidとIOSモジュールを造ることができるマシンをセットアップするためにビルトインチームを得ることは、第一歩でした.
  • 次のタスクは、IOSのフレームワークをIOSのアプリケーションにKMPによって生成統合されました.スパイクの間、我々はXcodeにフレームワークをコピーすることによって、これを手動で行いました.明らかに、これは前進したかった道ではなかった.
  • また、IOSのチームは、会社/チーム内からバイナリフレームワークを使用するのは初めてだった.IOSのアプリは、カルタージとGithubからのみサードパーティの依存関係を使用して何も、ノートン内から.Artifactoryの我々のrepoはパスワード保護されています、そして、明らかにcarthageはパスワード認証を支持しません.それを理解するのに1日かかった.したがって、我々の次のタスクは、我々のRPOは、IOSのアプリケーションのための匿名アクセスを与えられたKMPモジュールから生成されたフレームワークをダウンロードできるようにすることでした.
  • 以下のいくつかのタスクは、私たちを把握し、それらを動作させるために少しの時間を取った.

  • FatFrameworkのリソースへの適用
  • 私たちは両方のシミュレータ&デバイスで動作するようにFatFrameworkを生成しましたが、Gradleによって提供されるFatFrameworkタスクはリソースをコピーしないことを発見しました.しかし、我々のフレームワークが必要moko resources パッケージされ、彼らが提供したタスクは私たちのために動作しませんでした.
  • 私はissue 彼らのgithubページで、我々のために働いた解決を掲示しました.
  • open class FatFrameworkWithResources : org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask() {
    
        @TaskAction
        protected fun copyBundle() {
            super.createFatFramework()
    
            frameworks.first().outputFile.listFiles()
                    ?.asSequence()
                    ?.filter { it.name.contains(".bundle") }
                    ?.forEach { bundleFile ->
                        project.copy {
                            from(bundleFile)
                            into("${fatFrameworkDir.absolutePath}/${bundleFile.name}")
                        }
                    }
        }
    }
    
  • Gradleは簡単な方法を提供するフレームワークを&我々はそれを使用して簡単なタスクを作成することができます.
  • val zipFramework = tasks.register("zipFatFramework", Zip::class.java) {
                it.dependsOn(tasks.named("releaseFatFramework"))
    
                it.from("$buildDir/fat-framework")
                it.archiveFileName.set("shared.framework.zip")
                it.destinationDirectory.set(file("$buildDir/fat-framework-zip"))
            }
    
  • Carthageはフレームワークのバージョンを指しているJSONファイルを必要とするので、JSONをダウンロード/作成し、再びアーティファクトにアップロードするGradleタスクを思いつきました.
  • val downloadAndModifyJson = tasks.register("downloadAndModifyFrameworkJson") {
                it.dependsOn("downloadJson")
                val buildNumber = project.properties["buildNumber"]
    
                it.doLast {
                    file("$buildDir/shared.json").apply {
                        if (exists()) {
                            val currentContent = readText()
                            val jsonObject = JsonParser.parseString(currentContent).asJsonObject
                            jsonObject.addProperty("$version.$buildNumber", "https://artifactory.corp.com/artifactory/${pathToFramework}/$version.$buildNumber/shared.framework.zip")
                            writeText(jsonObject.toString())
                        } else {
                            parentFile.mkdirs()
                            createNewFile()
                            writeText(
                                """{ "$version.$buildNumber" : "https://artifactory.corp.com/artifactory/${pathToFramework}/$version.$buildNumber/shared.framework.zip" }""".trimIndent()
                            )
                        }
                    }
                }
            }
    

    凍結とthreadlocal
  • 私はKollinのネイティブスレッドでの問題について読んだが、サンプルのIOSのアプリで作業するときに任意の問題に直面しなかった.だから、私はそれに対処する必要はないと思った(我々はほとんどデータを読んでいて、どんな場所でも変更しない).
  • ときに、実際のIOSのアプリに統合すると、我々はこの問題に直面した.データにアクセスしようとしている間、IOSアプリはクラッシュしていました、そして、我々の同僚はそれが異なる糸からそれにアクセスしている変化があるかもしれないと言いました.
  • このブログhttps://helw.net/2020/04/16/multithreading-in-kotlin-multiplatform-apps/ ] 本当に私が何が起こっているかを理解するのを助けました、しかし、我々はIOS層のためにオブジェクトを凍結したかったので、我々はAndroidとIOSのために異なるAPIクラスを使わなければなりませんでした、そして、それは一般的な層ではなくKotlinで利用できるだけです.(したがって、実際に公開されている一般的なAPIの共有は行われません.これは、2つのAPIクラスを維持しなければならないので、私にとっては大きな打撃でした.
  • のAPIクラス
    expect class Api
    
    アンドロイドの例
    actual class Api : KoinComponent {
    
        internal val api: NetworkingClient by inject()
    
        @Throws(Exception::class)
        suspend fun getSampleJson(
            param1: String,
            param2: String = "en_US"
        ): SampleData {
            return api.getUser(param1, param2)
        }
    }
    
    のAPIの例
    actual class Api : KoinComponent {
    
        internal val api: NetworkingClient by inject()
    
        @Throws(Exception::class)
        suspend fun getSampleJson(
            param1: String,
            param2: String = "en_US"
        ): SampleData {
            return api.getUser(param1, param2).freeze()
        }
    }
    

    カスタムKTORインターセプター
  • 我々は、ネットワークのための他のAndroidアプリの数のようなOkHTTPクライアントを使用し、カスタムOkHTTPインターセプターの束を持っていた.我々がAndroidとIOSの向こうでそれを共有したいならば、悲しいことに、我々はKTORのためにOkHTTPクライアントを使用することができません.
  • 我々は、大いに頼りますMockey テストのために我々のAPI応答を模擬するために、我々がKTORクライアントで働くために移植した最初のインターセプターのうちの1つ.以下は我々のインターセプターのサンプルコードです.デバッグ設定を使用してインターセプターの使用を切り替えます.
  • internal class MockeyUrlSelectionInterceptor(
        private val preferences: DebugPreferences?
    ) {
    
        class Config {
            constructor(
                preferences: DebugPreferences? = null
            ) {
                this.preferences = preferences
            }
    
            var preferences: DebugPreferences?
    
            fun build(): MockeyUrlSelectionInterceptor = MockeyUrlSelectionInterceptor(preferences)
        }
    
        companion object Feature :
            HttpClientFeature<Config, MockeyUrlSelectionInterceptor> { // Creates a unique key for the feature.
            override val key =
                AttributeKey<MockeyUrlSelectionInterceptor>("MockeyUrlSelectionInterceptor")
    
            override fun prepare(block: Config.() -> Unit): MockeyUrlSelectionInterceptor =
                Config().apply(block).build()
    
            override fun install(feature: MockeyUrlSelectionInterceptor, scope: HttpClient) {
                scope.requestPipeline.intercept(HttpRequestPipeline.Transform) {
    
                    if (feature.preferences?.shouldMockApi == true) {
                        val newHttpUrl = Url("http://${feature.preferences.mockIP}:8080/service/${context.url.protocol.name}://${context.url.host}/${context.url.encodedPath}")
                        context.url(url = newHttpUrl)
                    }
    
                    proceedWith(subject)
                }
            }
        }
    }
    
  • AndroidとIOS全体の共通の設定を管理するには、我々はこの偉大なライブラリを使用してTouchlab
  • 私はこのポストは、将来のプロジェクトのためのKMPを考慮して誰にとっても便利です願っています!これは私の初めてのブログを書くので、任意のフィードバックを感謝です!
    私はnortonlifeock従業員です.このサイトの私の意見は私自身であり、必ずしもNontonListockを反映しません.