2024.12.19
システムの穴を運用でカバーしようとしてミス多発… バグが大量発生、決算が合わない状態から業務効率化を実現するまで
リンクをコピー
記事をブックマーク
鈴木大貴氏:それでは『iOSアプリにKMMを導入するtips』の発表を始めます。簡単に自己紹介をします。株式会社AbemaTVでAbemaのアプリを開発しています、鈴木大貴と申します。iOSアプリを担当しています。
今日のアジェンダですが、大きく分けて4項目です。KMM(Kotlin Multiplatform Mobile)のフレームワークを取り込んだアプリをQAを通してリリースブランチに乗せていて、これは来週以降(※取材当時)実際にリリースされるものです。
KMMを導入するにあたって詰まった部分などをまとめています。1つ目は「Suspend functionをiOSでキャンセルできるようにする」というものです。2つ目は「テストのときだけ任意のモジュールをimportしたい」というもので、3つ目が「KMMのフレームワークをプライベートリポジトリのReleasesにアップロードして、CocoaPods経由でダウンロードできるようにする」です。最後は「Kotlinで実装されたNSObjectのサブクラスをSwizzleするとクラッシュする問題を解決する」というものです。
さっそく「Suspend functionをiOSでキャンセルする」について話します。以前「Qiita」に簡単にまとめたこともあったんですが、それをより詳しく話していきます。
これはKotlinのコードです。例えばビデオのランキングに関するAPIを1つのインターフェイスとしてまとめたときに、ジャンルごとのランキングを取得するSuspend functionを定義した場合、以下のようになると思います。
これをSwift側で呼び出すと、インターフェイス的にはcompletionHandlerとしてコールバックを渡して、そこからレスポンスを受け取るかたちになります。ただこの場合だと、メソッド自体の返り値はVoid型になるので、Swift側からキャンセルする術がありません。
なので、サブスクライブして、最終的に返ってきたjobのオブジェクトからcancelを呼び出せるように変更していきます。このやり方は、Working with Kotlin Coroutines and RxSwiftという記事でも紹介していて、それを参考にしています。
1つ目の作業なんですが、Suspend function自体をラップするクラスを作ります。名前はSuspendWrapperで置いています。
コンストラクターの引数としてLambdaでsuspend functionを取得して、AndroidからSuspend functionをそのまま呼び出す用のメソッドを定義しつつ、iOSでsubscribeをして、最終的にjobでキャンセルできるインターフェイスにするものを定義します。
subscribeでは成功したときのハンドラーと失敗したときのハンドラーを定義しています。
実際にインターフェイスに定義すると、先ほどは返り値がそのままGenreRankingResponseになっていたと思うんですが、そこをただSuspendWrapperというクラスで括って、ジェネリックなものとして返してあげます。
ほかにもDeferredというKotlin側で実際に定義されているインターフェイスを使う方法もあるんですが、kotlin-nativeの仕様でObjective-Cのprotocolに変換されます。
Objective-Cのprotocol自体がGeneric Argumentをもたない仕様になっているので、Tと定義しても実際に利用するとAnyになってしまうという問題があります。なので、今回は1つ型を用意してラップするというかたちにしています。
Swiftで実際に以下のように呼び出せるようになります。これはiOS向けにcancelを呼び出せるインターフェイスです。Android側で本来不要なはずのsuspendというfunctionをいったん呼び出さなくてはいけないというデメリットがあります。
また、いろいろとレイヤーなどを分けてモジュールを追加していくと、いろいろなところでSuspendWrapperをインポートしないといけません。
そこで、このSuspendWrapperをさらに活用していきます。GitHubでKoruというものが公開されていて、これを使うと自動的に先ほどのSuspendWrapperを生やすことができます。このライブラリも先ほど紹介したブログを参考に作られているようです。
実際に自動生成されるクラスがどういうふうになるかというと、先ほど定義したランキング用のAPIを1個ラップしたNativeというサフィックスが付いたクラスが生成されます。
SuspendWrapperを返り値として、内部でsuspend functionをMainScopeで呼び出す実装をしたメソッドとそのクラスが自動生成されます。
こちらは実際に自動生成されたものです。Kotlin側でどういうふうに実装するかというと、@ToNativeClassでアノテーションを追加するだけです。
そのアノテーションにどのCoroutineScopeで動かすかを指定する必要があるので、MainScopeを指定しつつ、インターフェイス上はSuspendWrapperではなくてGenreRankingResponseというただのクラスを返すので、もともとAndroid側でも利用したかった通常のSuspend functionの状態で定義できます。
こちらで使っているMainScopeProviderがどういうものかというと、CoroutineScopeとしてMainScopeを提供するクラスとして定義しています。@ExportedScopeProviderのアノテーションを指定すると、先ほどの@ToNativeClassというアノテーションに指定できるScopeProviderとなります。
先ほどと同じなんですが、Suspend functionをSwift側でそのまま呼び出すと、completionHandlerのクロージャになります。
これを自動生成されたVideoRankingApiNativeに渡すと、APIはSuspendWrapperで自動的に生成されたインターフェイスにアクセスできるようになって、subscribeを呼ぶことができます。返り値としてjobが返ってくるので、キャンセルも可です。
subscribeとjobが返ってくる関係をiOSで使っているRxSwiftで表現すると、単一実行したものが結果として1回だけ返ってくるという状態なので、RxSwiftのSingleに置き換えられる状態になります。
こちらに置き換えた状態で、例えばRepositoryでランキングを取得するものを実際に作るとすると、引数にはインターフェイスとして定義しているVideoRankingApiを取りつつ、中で利用するときにはSuspendWrapperとして自動生成されたApiNativeのインスタンスを保持します。それを実際に利用して、RxSwiftのSingleに変換して外側に返します。
SuspendWrapperを利用したクラスをテストしたくなることがあると思うんですが、先ほどMainScopeProviderで使っていたMainScope自体はCoroutineScopeの中でDispatchers.Mainを使っているので、iOS上のXCテストで実行したときには内部的に非同期で実行されます。
上から順番にfakeを作って、mockを生成して、実際にテスト対象となるRepositoryをインスタンス化したあとに、非同期のものを待つためにexpectationでexpectを生成して、実際にsubscribeを呼び出します。
ここではまだ呼び出しは発生していなくて、呼び出しを受け取るという実装をするだけですね。
そのあとにいったんDispatchQueue.main.asyncで非同期に実行させる実装だけをして、expectを0.1秒間待つという処理を書きます。次に番号①でWaitの部分の定義をして、②ではDispatchQueue.main.asyncでRunLoop先で実行させるものを1個定義します。
③で待つ処理に入って、④で実際に値として想定しているものをレスポンスとしてSuspendWrapperのmockに渡すとsubscribeのコールバックが呼ばれて、⑤でexpectのfulfillが呼ばれてテストが成功します。
ただこれは内部の値が適切に渡せているかを確認しただけなので、非同期なテストを書く必要は実際はありません。ここで同期的に書く場合、先ほどのDispatchers.Mainの部分をimmediateに変えれば同期的にメインスレッドから呼ばれてdispatchされるはずです。ただ、現状PullRequestがあがっていて、immediateで実装されていても同期的に実行されずにdispatchが呼ばれて非同期になってしまうという問題があります。
なのでiOS側で同期的に呼び出しを可能にするためには、ImmediateScopeを定義します。CoroutineDispatcher自体を実装して、dispatchのメソッドの中でただblockをrunするだけのもので、同一スレッドで即時実行されるScopeです。
先ほどのMainScopeProviderの中にImmediateScopeをiOS側から別なScopeとしてセットできるsetterを追加して、実際にImmediateScopeで動く実装を追加します。そうするとMainScopeProvider自体がSingleton扱いになるので、そこから呼び出しを行って実際にscopeをsetします。
Suspend functionを自身で初期化する場合はScopeProviderが必要になるので、これもiOS側の実装としてImmediateScopeProviderを定義して、ImmediateScopeを返せるようにします。
実際にそれをSuspend functionのmockとして値を返せる状態にするのですが、KotlinSuspendfunction0というprotocolがいて、これはkotlin-native側で自動的に吐き出されたSuspend functionを意味しています。
これがインターフェイスとして定義された場合、必ず実装しないといけないものが、invoke(completionHandler:)です。これはSuspend functionが実行されたときに呼び出されるメソッドになっています。
completionHandlerの引数を受け取ったら、Suspend functionのmockでcompletionHandlerをほかのクロージャ内で保持をして、respondの部分で成功値か失敗値を返せるようにします。
それをSuspendWrapperとして返せるように、asSuspendWrapperという関数としてImmediateScopeProviderを渡します。
そうすると先ほど非同期で書いていたテストが、上からImmediateScopeをsetして、fakeのAPIを生成したあとにmockのSuspend functionを定義して、responseとしてSuspendWrapperを渡します。
そのあとに実際のテストのターゲットになるRepositoryを初期化して、responseを待つ用にBehaviorRelayで変数を1個置きます。そこでGenreのsubscribeをして、responseに対してbindできるようにしたあとに、最後に実際に値として渡したいものをsuccessというかたちでrespondで渡すと、そのままテストが通過できます。
これが実際に疎通ができているかのテストです。
(次回へつづく)
2024.12.20
日本の約10倍がん患者が殺到し、病院はキャパオーバー ジャパンハートが描く医療の未来と、カンボジアに新病院を作る理由
2024.12.19
12万通りの「資格の組み合わせ」の中で厳選された60の項目 532の資格を持つ林雄次氏の新刊『資格のかけ算』の見所
2024.12.16
32歳で成績最下位から1年でトップ営業になれた理由 売るテクニックよりも大事な「あり方」
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
PR | 2024.12.20
モンスター化したExcelが、ある日突然崩壊 昭和のガス工事会社を生まれ変わらせた、起死回生のノーコード活用術
2024.12.12
会議で発言しやすくなる「心理的安全性」を高めるには ファシリテーションがうまい人の3つの条件
2024.12.18
「社長以外みんな儲かる給与設計」にした理由 経営者たちが語る、優秀な人材集め・会社を発展させるためのヒント
2024.12.17
面接で「後輩を指導できなさそう」と思われる人の伝え方 歳を重ねるほど重視される経験の「ノウハウ化」
2024.12.13
ファシリテーターは「しゃべらないほうがいい」理由 入山章栄氏が語る、心理的安全性の高い場を作るポイント
2024.12.10
メールのラリー回数でわかる「評価されない人」の特徴 職場での評価を下げる行動5選
Climbers Startup JAPAN EXPO 2024 - 秋 -
2024.11.20 - 2024.11.21
『主体的なキャリア形成』を考える~資格のかけ算について〜
2024.12.07 - 2024.12.07
Startup CTO of the year 2024
2024.11.19 - 2024.11.19
社員の力を引き出す経営戦略〜ひとり一人が自ら成長する組織づくり〜
2024.11.20 - 2024.11.20
「確率思考」で未来を見通す 事業を成功に導く意思決定 ~エビデンス・ベースド・マーケティング思考の調査分析で事業に有効な予測手法とは~
2024.11.05 - 2024.11.05