はじめに
こんにちは、iOSアプリ開発グループの永田です。
最近はオードリーのオールナイトニッポン in 東京ドームに参加しました。
ステージ裏の席でしたが最高に楽しめました。円盤化を待ちます。
さて、今回はKotlin Multiplatform(KMP)でプラットフォーム固有の実装を入れる際、
悩んだポイント・辿り着いた解決策についてご紹介します。
モチベーション
KMPは複数のプラットフォームにまたがるロジックを共通化し、そのインタフェースを抽象化できるという点でメリットがあります。
一方でその性質ゆえ、プラットフォーム固有の定義を共通インタフェースの一部として公開することができません。
私が直面した具体例をご紹介します。
KMPでiOS, Androidアプリの動画再生処理を共通化するため、commonMainで以下のような定義を追加しました。
// commonMain internal expect class VideoPlayer {} interface VideoPlayerViewModel {} internal class VideoPlayerViewModelImpl( private val videoPlayer: VideoPlayer ): VideoPlayerViewModel {} |
iosMain, androidMainでそれぞれ
actualの
VideoPlayerを実装し、それを
VideoPlayerViewModelImplに渡します。
iosApp, androidAppでは
interface VideoPlayerViewModelを参照して使う設計です。
iOSアプリで動画再生をするには、
AVPlayerViewControllerや
VideoPlayerなどの、
動画表示を司るクラスに
AVPlayerのインスタンスを渡す必要があります。
この設計だと、iosMainに定義した
actual VideoPlayerが持つ
AVPlayerのインスタンスをiosApp側まで公開できれば動画を表示できます。
しかし、 AVPlayerはiOS SDKに定義されたクラスであるため、commonMainに定義した VideoPlayerViewModelのプロパティとしては定義できません。これを解決したいと考えました。
当初の案
最初に考えたのは、iosMainで
AVPlayerをプロパティとして持つだけの
interfaceを定義することでした。
この
AVPlayerProvidingと
VideoPlayerViewModelに同時に準拠するクラスを実装し、画面側へ渡そうとしました。
// iosMain interface AVPlayerProviding { val avPlayer: AVPlayer } internal class VideoPlayerViewModelIOS: VideoPlayerViewModel, AVPlayerProviding { ... } |
次に、このクラスを画面側から参照する際に、具象のクラスではなく
interfaceに依存するようにする方法を検討しました。
Swiftだと、複数の
protocolに準拠していることを
&で明示できます。
protocol VideoPlayerViewModel {} protocol AVPlayerProviding {} let viewModel: VideoPlayerViewModel & AVPlayerProviding func provide() -> VideoPlayerViewModel & AVPlayerProviding |
Kotlinでも同様の機能がないかを調査しましたが、どうやらなさそうだということが判明しました。
複数の interfaceに準拠する interfaceを定義する
そこで、これらの
interfaceをまとめた
interfaceをiosMainで新たに定義することにしました。
この
interfaceに準拠したクラスを実装し、画面側からは
VideoPlayerViewModelWithAVPlayerとして参照します。
// iosMain interface VideoPlayerViewModelWithAVPlayer: VideoPlayerViewModel, AVPlayerProviding internal class VideoPlayerViewModelIOS: VideoPlayerViewModelWithAVPlayer { ... } class VideoPlayerViewModelProvider { companion object { fun provide(): VideoPlayerViewModelWithAVPlayer { return VideoPlayerViewModelIOS() } } } |
byで委譲する
ここまでくれば、あとは VideoPlayerViewModelIOSが VideoPlayerViewModel, AVPlayerProvidingのそれぞれに準拠するよう実装するだけです。
Kotlinには、
byというキーワードを用いて特定の
interfaceの実装を他のインスタンスへ委譲できる機能があります。
これを用いて、
VideoPlayerViewModelへの準拠を
VideoPlayerViewModelImplに、
AVPlayerProvidingへの準拠を
VideoPlayerへ委譲します。
// iosMain internal actual class VideoPlayer: AVPlayerProviding { override val avPlayer = AVPlayer() } internal class VideoPlayerViewModelIOS( viewModel: VideoPlayerViewModel, videoPlayer: VideoPlayer ): VideoPlayerViewModelWithAVPlayer, VideoPlayerViewModel by viewModel, AVPlayerProviding by videoPlayer { ... } class VideoPlayerViewModelProvider { companion object { fun provide(): VideoPlayerViewModelWithAVPlayer { val videoPlayer = VideoPlayer() return VideoPlayerViewModelIOS( VideoPlayerViewModelImpl(videoPlayer), videoPlayer ) } } } |
これで、commonMainで定義した interfaceをiOS向けに拡張でき、動画表示が可能になりました。
終わりに
今回は、KMPのcommonMainで定義した
interfaceをiOS向けに拡張する方法についてご紹介しました。
iOS以外でもさまざまなプラットフォーム向けに使える手法だと思うので、ぜひ参考にしてみてください。
依存関係を図に起こしてみるとちょっと複雑になってしまいましたが、
interfaceへの依存を遵守することでモッククラスのDIが容易になり、テスタビリティ等の観点でメリットが上回ると感じています。
欲を言えば、複数の
interfaceに準拠することを、Swiftのように簡潔に記述できるといいなあと思いました。
また、
byキーワードのような機能はSwiftにはないので、単一の言語ばかり書いていると得づらい知識が身につき楽しかったです。
今後も新しい知識を身につけつつ、それをiOSアプリ開発にも活かせるよう精進します。
参考文献
この記事を書いた人
-
iOSアプリを作っています
音楽とガジェットが好きです