2024.10.01
自社の社内情報を未来の“ゴミ”にしないための備え 「情報量が多すぎる」時代がもたらす課題とは?
リンクをコピー
記事をブックマーク
鈴木大貴氏:それでは『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.10.29
5〜10万円の低単価案件の受注をやめたら労働生産性が劇的に向上 相見積もり案件には提案書を出さないことで見えた“意外な効果”
2024.10.24
パワポ資料の「手戻り」が多すぎる問題の解消法 資料作成のプロが語る、修正の無限ループから抜け出す4つのコツ
2024.10.28
スキル重視の採用を続けた結果、早期離職が増え社員が1人に… 下半期の退職者ゼロを達成した「関係の質」向上の取り組み
2024.10.22
気づかぬうちに評価を下げる「ダメな口癖」3選 デキる人はやっている、上司の指摘に対する上手な返し方
2024.10.24
リスクを取らない人が多い日本は、むしろ稼ぐチャンス? 日本のGDP4位転落の今、個人に必要なマインドとは
2024.10.23
「初任給40万円時代」が、比較的早いうちにやってくる? これから淘汰される会社・生き残る会社の分かれ目
2024.10.23
「どうしてもあなたから買いたい」と言われる営業になるには 『無敗営業』著者が教える、納得感を高める商談の進め方
2024.10.28
“力を抜くこと”がリーダーにとって重要な理由 「人間の達人」タモリさんから学んだ自然体の大切さ
2024.10.29
「テスラの何がすごいのか」がわからない学生たち 起業率2年連続日本一の大学で「Appleのフレームワーク」を教えるわけ
2024.10.30
職場にいる「困った部下」への対処法 上司・部下間で生まれる“常識のズレ”を解消するには