プラットフォーム固有のAPIの使用
この記事では、マルチプラットフォームアプリケーションとライブラリを開発する際に、プラットフォーム固有のAPIを使用する方法を学びます。
Kotlinマルチプラットフォームライブラリ
プラットフォーム固有のAPIを使用するコードを記述する前に、代わりにマルチプラットフォームライブラリを使用できるかどうかを確認してください。 この種のライブラリは、異なるプラットフォーム向けに異なる実装を持つ共通Kotlin APIを提供します。
ネットワーキング、ロギング、アナリティクスを実装したり、デバイス機能にアクセスしたりするために使用できる多くのライブラリがすでに利用可能です。 詳細については、このキュレーションされたリストを参照してください。
expected および actual 関数とプロパティ
Kotlinは、共通ロジックを開発する際にプラットフォーム固有のAPIにアクセスするための言語メカニズムを提供します。expected および actual 宣言です。
このメカニズムにより、マルチプラットフォームモジュールの共通ソースセットはexpected
宣言を定義し、各プラットフォームソースセットはexpected
宣言に対応するactual
宣言を提供する必要があります。コンパイラは、共通ソースセットでexpect
キーワードでマークされたすべての宣言が、対象となるすべてのプラットフォームソースセットでactual
キーワードでマークされた対応する宣言を持つことを保証します。
これは、関数、クラス、インターフェース、列挙型、プロパティ、アノテーションなど、ほとんどのKotlin宣言に適用されます。このセクションでは、expected
および actual
関数とプロパティの使用に焦点を当てます。
この例では、共通ソースセットでexpected
なplatform()
関数を定義し、プラットフォームソースセットでactual
な実装を提供します。特定のプラットフォームのコードを生成する際、Kotlinコンパイラはexpected
および actual
宣言をマージします。そのactual
な実装を持つ1つのplatform()
関数が生成されます。expected
および actual
宣言は同じパッケージで定義され、結果として生成されるプラットフォームコードでは_1つの宣言_にマージされる必要があります。生成されたプラットフォームコードにおけるexpected
なplatform()
関数の任意の呼び出しは、正しいactual
な実装を呼び出します。
例: UUIDの生成
Kotlin Multiplatformを使用してiOSおよびAndroidアプリケーションを開発しており、universally unique identifier (UUID) を生成したいと仮定しましょう。
そのためには、Kotlin Multiplatformモジュールの共通ソースセットで、expect
キーワードを使用してrandomUUID()
関数をexpected
として宣言します。実装コードを含めないでください。
// 共通ソースセット内:
expect fun randomUUID(): String
各プラットフォーム固有のソースセット(iOSとAndroid)で、共通モジュールでexpected
とされるrandomUUID()
関数のactual
な実装を提供します。これらのactual
な実装をマークするためにactual
キーワードを使用します。
次のスニペットは、AndroidとiOSの実装を示しています。プラットフォーム固有のコードはactual
キーワードを使用し、関数に同じ名前を使用します。
// Androidソースセット内:
import java.util.*
actual fun randomUUID() = UUID.randomUUID().toString()
// iOSソースセット内:
import platform.Foundation.NSUUID
actual fun randomUUID(): String = NSUUID().UUIDString()
Androidの実装はAndroidで利用可能なAPIを使用し、iOSの実装はiOSで利用可能なAPIを使用します。 Kotlin/NativeコードからiOS APIにアクセスできます。
Android向けの最終的なプラットフォームコードを生成する際、Kotlinコンパイラはexpected
および actual
宣言を自動的にマージし、actual
なAndroid固有の実装を持つ単一のrandomUUID()
関数を生成します。同じプロセスがiOSでも繰り返されます。
簡潔にするため、この例および以下の例では、「common」、「ios」、「android」という簡略化されたソースセット名を使用します。 通常、これはcommonMain
、iosMain
、androidMain
を意味し、同様のロジックはテストソースセットであるcommonTest
、iosTest
、androidTest
でも定義できます。
expected
および actual
関数と同様に、expected
および actual
プロパティを使用すると、異なるプラットフォームで異なる値を使用できます。expected
および actual
関数とプロパティは、シンプルなケースで最も役立ちます。
共通コード内のインターフェース
プラットフォーム固有のロジックが大きすぎて複雑な場合、共通コードでそれを表すインターフェースを定義し、その後プラットフォームソースセットで異なる実装を提供することで、コードを簡素化できます。
プラットフォームソースセットの実装は、それぞれの対応する依存関係を使用します。
// commonMain ソースセット内:
interface Platform {
val name: String
}
// androidMain ソースセット内:
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
// iosMain ソースセット内:
import platform.UIKit.UIDevice
class IOSPlatform : Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
共通インターフェースが必要な場合に適切なプラットフォーム実装を注入するには、以下のいずれかのオプションを選択できます。それぞれのオプションは、以下でさらに詳しく説明します。
expected および actual 関数
このインターフェースの値を返すexpected
関数を定義し、そのサブクラスを返すactual
関数を定義します。
// commonMain ソースセット内:
interface Platform
expect fun platform(): Platform
// androidMain ソースセット内:
class AndroidPlatform : Platform
actual fun platform() = AndroidPlatform()
// iosMain ソースセット内:
class IOSPlatform : Platform
actual fun platform() = IOSPlatform()
共通コードでplatform()
関数を呼び出すと、Platform
型のオブジェクトを扱えます。 この共通コードをAndroidで実行すると、platform()
の呼び出しはAndroidPlatform
クラスのインスタンスを返します。 iOSで実行すると、platform()
はIOSPlatform
クラスのインスタンスを返します。
異なるエントリポイント
エントリポイントを制御する場合、expected
および actual
宣言を使用せずに各プラットフォーム成果物の実装を構築できます。 そのためには、プラットフォーム実装を共有Kotlin Multiplatformモジュールで定義しますが、それらをプラットフォームモジュールでインスタンス化します。
// 共有Kotlin Multiplatformモジュール
// commonMain ソースセット内:
interface Platform
fun application(p: Platform) {
// application logic
}
// androidMain ソースセット内:
class AndroidPlatform : Platform
// iosMain ソースセット内:
class IOSPlatform : Platform
// androidApp プラットフォームモジュール内:
import android.app.Application
import mysharedpackage.*
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
application(AndroidPlatform())
}
}
// iosApp プラットフォームモジュール内 (Swift):
import shared
@main
struct iOSApp : App {
init() {
application(IOSPlatform())
}
}
Androidでは、AndroidPlatform
のインスタンスを作成してapplication()
関数に渡す必要があります。一方iOSでは、同様にIOSPlatform
のインスタンスを作成して渡す必要があります。これらのエントリポイントはアプリケーションのエントリポイントである必要はありませんが、ここで共有モジュールの特定の機能を呼び出すことができます。
expected
および actual
関数を使用するか、またはエントリポイントを直接介して適切な実装を提供する方法は、シンプルなシナリオにはうまく機能します。 しかし、プロジェクトで依存性注入フレームワークを使用している場合、一貫性を確保するためにシンプルなケースでもそれを使用することをお勧めします。
依存性注入フレームワーク
最新のアプリケーションは通常、疎結合アーキテクチャを構築するために依存性注入 (DI) フレームワークを使用します。 DIフレームワークは、現在の環境に基づいてコンポーネントに依存関係を注入できます。
Kotlin Multiplatformをサポートする任意のDIフレームワークは、異なるプラットフォーム向けに異なる依存関係を注入するのに役立ちます。
例えば、KoinはKotlin Multiplatformをサポートする依存性注入フレームワークです。
// 共通ソースセット内:
import org.koin.dsl.module
interface Platform
expect val platformModule: Module
// androidMain ソースセット内:
class AndroidPlatform : Platform
actual val platformModule: Module = module {
single<Platform> {
AndroidPlatform()
}
}
// iosMain ソースセット内:
class IOSPlatform : Platform
actual val platformModule = module {
single<Platform> { IOSPlatform() }
}
ここでは、Koin DSLが注入するコンポーネントを定義するモジュールを作成します。共通コードでexpect
キーワードを使用してモジュールを宣言し、その後actual
キーワードを使用して各プラットフォーム向けにプラットフォーム固有の実装を提供します。フレームワークが実行時に正しい実装を選択する役割を担います。
DIフレームワークを使用する場合、すべての依存関係をこのフレームワークを介して注入します。同じロジックがプラットフォーム依存関係の処理にも適用されます。プロジェクトにDIをすでに導入している場合は、expected
および actual
関数を手動で使用するよりも、DIを継続して使用することをお勧めします。この方法により、依存関係を注入する2つの異なる方法を混在させることを避けられます。
また、共通インターフェースを常にKotlinで実装する必要はありません。Swiftのような別の言語で、異なる_プラットフォームモジュール_で行うこともできます。このアプローチを選択する場合、DIフレームワークを使用してiOSプラットフォームモジュールから実装を提供する必要があります。
このアプローチは、実装をプラットフォームモジュールに配置した場合にのみ機能します。Kotlin Multiplatformモジュールが自己完結型にならないため、あまりスケーラブルではありません。共通インターフェースを別のモジュールで実装する必要があるためです。
次のステップ
- KMPアプリでのプラットフォーム固有APIの使用のビデオチュートリアルをご覧ください。
expect
/actual
メカニズムに関するさらに多くの例と情報については、expected および actual 宣言を参照してください。