目次

目次

【Kotlin Multiplatform】commonMainのinterfaceを特定のプラットフォーム向けに拡張する

アバター画像
永田駿平
アバター画像
永田駿平
最終更新日2024/03/01 投稿日2024/03/01

はじめに

こんにちは、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でそれぞれ actualVideoPlayerを実装し、それをVideoPlayerViewModelImplに渡します。 iosApp, androidAppでは interface VideoPlayerViewModelを参照して使う設計です。

architecture_base.png

iOSアプリで動画再生をするには、AVPlayerViewControllerVideoPlayerなどの、 動画表示を司るクラスにAVPlayerのインスタンスを渡す必要があります。 この設計だと、iosMainに定義した actual VideoPlayerが持つAVPlayerのインスタンスをiosApp側まで公開できれば動画を表示できます。

しかし、 AVPlayerはiOS SDKに定義されたクラスであるため、commonMainに定義したVideoPlayerViewModelのプロパティとしては定義できません。これを解決したいと考えました。

当初の案

最初に考えたのは、iosMainで AVPlayerをプロパティとして持つだけのinterfaceを定義することでした。 この AVPlayerProvidingVideoPlayerViewModelに同時に準拠するクラスを実装し、画面側へ渡そうとしました。

// 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で委譲する

ここまでくれば、あとは VideoPlayerViewModelIOSVideoPlayerViewModel, 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向けに拡張でき、動画表示が可能になりました。

architecture_result.png

終わりに

今回は、KMPのcommonMainで定義した interfaceをiOS向けに拡張する方法についてご紹介しました。 iOS以外でもさまざまなプラットフォーム向けに使える手法だと思うので、ぜひ参考にしてみてください。

依存関係を図に起こしてみるとちょっと複雑になってしまいましたが、

interfaceへの依存を遵守することでモッククラスのDIが容易になり、テスタビリティ等の観点でメリットが上回ると感じています。 欲を言えば、複数の interfaceに準拠することを、Swiftのように簡潔に記述できるといいなあと思いました。

また、 byキーワードのような機能はSwiftにはないので、単一の言語ばかり書いていると得づらい知識が身につき楽しかったです。 今後も新しい知識を身につけつつ、それをiOSアプリ開発にも活かせるよう精進します。

参考文献

アバター画像

永田駿平

iOSアプリを作っています
音楽とガジェットが好きです

目次