Communitygithub.com

ImL1s/flutter-claude-skills

Claude Code 向けに厳選された 66 の Flutter/Dart 開発、テスト、リリース、AI 駆動の設計からコードへのパイプラインスキルです。

flutter-claude-skills とは?

このスキルセットは、Claude Code 上で動作する Flutter/Dart 開発者向けに設計された 66 の厳選されたスキルを提供します。プロジェクト構造、Gradle のセットアップ、マルチプラットフォームアーキテクチャ、テスト戦略、リリースパイプライン、AI を活用したデザインからコードへの変換ワークフローをカバーしています。各スキルは、実践的なコード例、ベストプラクティス、よくある落とし穴を含む詳細なマークダウンドキュメントとして提供され、Claude Code のコンテキスト内で直接利用できます。これにより、開発者は AI アシスタントを使いながら一貫性のある高品質な Flutter コードを迅速に生成し、プロジェクトのセットアップからデプロイまでを効率化できます。

対応✓Claude Code~Codex CLI~Cursor
npx skills add ImL1s/flutter-claude-skills

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Kotlin Multiplatform (KMP) の基礎

Kotlin Multiplatform は、Android、iOS、Desktop、Web(WASM)、Server 間でコードを共有することを可能にします。

プロジェクト構造

マルチモジュールアーキテクチャ(フィーチャーベース + api/impl)

your-project-admin/
├── build.gradle.kts              # ルートビルド設定
├── settings.gradle.kts           # モジュールインクルード
├── gradle/libs.versions.toml     # バージョンカタログ
│
├── core/
│   ├── common/                   # ユーティリティ、Result 型、拡張関数
│   │   └── src/commonMain/kotlin/
│   ├── data/                     # データ抽象化、DataStore
│   │   ├── src/commonMain/kotlin/
│   │   ├── src/androidMain/kotlin/
│   │   └── src/iosMain/kotlin/
│   ├── database/                 # Room(Android/iOS/JVM のみ)
│   │   └── src/commonMain/kotlin/
│   ├── network/                  # Ktor クライアント
│   │   └── src/commonMain/kotlin/
│   └── ui/                       # デザインシステム、テーマ
│       └── src/commonMain/kotlin/
│
├── feature/
│   ├── auth/
│   │   ├── api/                  # 公開インターフェース、モデル
│   │   │   └── src/commonMain/kotlin/
│   │   └── impl/                 # 実装、UI
│   │       └── src/commonMain/kotlin/
│   └── home/
│       ├── api/
│       └── impl/
│
├── composeApp/                   # プラットフォームエントリポイント
│   ├── src/commonMain/           # アプリ構成、DI グラフ
│   ├── src/androidMain/          # MainActivity
│   ├── src/iosMain/              # iOS エントリ
│   ├── src/jvmMain/              # Desktop main()
│   └── src/wasmJsMain/           # Web エントリ
│
└── iosApp/                       # Xcode プロジェクト

ソースセット階層

commonMain
├── androidMain
├── iosMain
│   ├── iosX64Main
│   ├── iosArm64Main
│   └── iosSimulatorArm64Main
├── jvmMain
├── wasmJsMain
└── jsMain (フォールバック)

Gradle のセットアップ

ルート build.gradle.kts

plugins {
    alias(libs.plugins.kotlinMultiplatform) apply false
    alias(libs.plugins.androidApplication) apply false
    alias(libs.plugins.androidLibrary) apply false
    alias(libs.plugins.composeMultiplatform) apply false
    alias(libs.plugins.composeCompiler) apply false
    alias(libs.plugins.kotlinSerialization) apply false
    alias(libs.plugins.ksp) apply false
    alias(libs.plugins.room) apply false
    alias(libs.plugins.metro) apply false
}

settings.gradle.kts

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "your-project-admin"

// コアモジュール
include(":core:common")
include(":core:data")
include(":core:database")
include(":core:network")
include(":core:ui")

// フィーチャーモジュール
include(":feature:auth:api")
include(":feature:auth:impl")
include(":feature:home:api")
include(":feature:home:impl")

// アプリエントリポイント
include(":composeApp")

gradle/libs.versions.toml

[versions]
kotlin = "2.1.0"
agp = "8.7.3"
compose-multiplatform = "1.7.3"
ktor = "3.1.1"
room = "2.8.4"
datastore = "1.2.0"
decompose = "3.5.0"
metro = "0.1.1"
essenty = "2.5.0"
coroutines = "1.10.1"
serialization = "1.7.3"
ksp = "2.1.0-1.0.29"

[libraries]
# Kotlin
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }

# Ktor
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

# Room(Android、iOS、JVM のみ)
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version = "2.6.2" }

# DataStore
datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }

# Decompose
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
decompose-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" }

[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "room" }
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }

モジュール build.gradle.kts(KMP ライブラリ)

// core/common/build.gradle.kts
plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "17"
            }
        }
    }

    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "CoreCommon"
            isStatic = true
        }
    }

    jvm("desktop")

    @OptIn(ExperimentalWasmDsl::class)
    wasmJs {
        browser()
    }

    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.coroutines.core)
        }

        androidMain.dependencies {
            // Android 固有
        }

        iosMain.dependencies {
            // iOS 固有
        }

        val desktopMain by getting {
            dependencies {
                // Desktop 固有
            }
        }
    }
}

android {
    namespace = "com.your-project.admin.core.common"
    compileSdk = 35

    defaultConfig {
        minSdk = 24
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

expect/actual パターン

宣言(commonMain)

// commonMain/kotlin/Platform.kt
expect class PlatformContext

expect fun getPlatformName(): String

expect fun createDataStorePath(context: PlatformContext): String

Android 実装

// androidMain/kotlin/Platform.android.kt
actual typealias PlatformContext = android.content.Context

actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"

actual fun createDataStorePath(context: PlatformContext): String {
    return context.filesDir.resolve("datastore").absolutePath
}

iOS 実装

// iosMain/kotlin/Platform.ios.kt
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask

actual class PlatformContext

actual fun getPlatformName(): String = "iOS"

actual fun createDataStorePath(context: PlatformContext): String {
    val documentDir = NSFileManager.defaultManager.URLForDirectory(
        NSDocumentDirectory,
        NSUserDomainMask,
        null,
        false,
        null
    )
    return "${documentDir?.path}/datastore"
}

Desktop 実装

// desktopMain/kotlin/Platform.jvm.kt
import java.io.File

actual class PlatformContext

actual fun getPlatformName(): String =
    "${System.getProperty("os.name")} ${System.getProperty("os.version")}"

actual fun createDataStorePath(context: PlatformContext): String {
    val home = System.getProperty("user.home")
    return File(home, ".your-project-admin/datastore").absolutePath
}

WASM 実装

// wasmJsMain/kotlin/Platform.wasmJs.kt
actual class PlatformContext

actual fun getPlatformName(): String = "Web (WASM)"

actual fun createDataStorePath(context: PlatformContext): String {
    return "your-project-admin-datastore"  // localStorage を使用
}

モジュール依存関係

依存関係ルール

composeApp
├── feature:auth:impl
│   ├── feature:auth:api
│   ├── core:ui
│   └── core:network
├── feature:home:impl
│   ├── feature:home:api
│   ├── core:ui
│   └── core:database
├── core:ui
│   └── core:common
├── core:network
│   └── core:common
├── core:database
│   └── core:common
└── core:data
    └── core:common

api/impl パターン

// feature/auth/api/build.gradle.kts
plugins {
    alias(libs.plugins.kotlinMultiplatform)
}

kotlin {
    // ターゲット...

    sourceSets {
        commonMain.dependencies {
            // モデルとインターフェースのみ - 実装は含めない
            api(projects.core.common)
        }
    }
}

// feature/auth/impl/build.gradle.kts
plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.composeMultiplatform)
    alias(libs.plugins.composeCompiler)
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(projects.feature.auth.api)
            implementation(projects.core.ui)
            implementation(projects.core.network)
            implementation(libs.decompose)
            implementation(libs.decompose.compose)
        }
    }
}

一般的なパターン

Result 型

// core/common/src/commonMain/kotlin/Result.kt
sealed class AppResult<out T> {
    data class Success<T>(val data: T) : AppResult<T>()
    data class Error(val message: String, val cause: Throwable? = null) : AppResult<Nothing>()
    data object Loading : AppResult<Nothing>()
}

inline fun <T, R> AppResult<T>.map(transform: (T) -> R): AppResult<R> = when (this) {
    is AppResult.Success -> AppResult.Success(transform(data))
    is AppResult.Error -> this
    is AppResult.Loading -> this
}

inline fun <T> AppResult<T>.onSuccess(action: (T) -> Unit): AppResult<T> {
    if (this is AppResult.Success) action(data)
    return this
}

inline fun <T> AppResult<T>.onError(action: (String, Throwable?) -> Unit): AppResult<T> {
    if (this is AppResult.Error) action(message, cause)
    return this
}

リポジトリインターフェース(api モジュール)

// feature/auth/api/src/commonMain/kotlin/AuthRepository.kt
interface AuthRepository {
    suspend fun login(email: String, password: String): AppResult<User>
    suspend fun logout(): AppResult<Unit>
    fun observeAuthState(): Flow<AuthState>
}

data class User(
    val id: String,
    val email: String,
    val name: String
)

sealed class AuthState {
    data object Unauthenticated : AuthState()
    data class Authenticated(val user: User) : AuthState()
}

リポジトリ実装(impl モジュール)

// feature/auth/impl/src/commonMain/kotlin/AuthRepositoryImpl.kt
class AuthRepositoryImpl(
    private val apiService: AuthApiService,
    private val tokenStorage: TokenStorage
) : AuthRepository {

    private val _authState = MutableStateFlow<AuthState>(AuthState.Unauthenticated)

    override suspend fun login(email: String, password: String): AppResult<User> {
        return try {
            val response = apiService.login(LoginRequest(email, password))
            tokenStorage.saveToken(response.token)
            _authState.value = AuthState.Authenticated(response.user)
            AppResult.Success(response.user)
        } catch (e: Exception) {
            AppResult.Error("Login failed: ${e.message}", e)
        }
    }

    override suspend fun logout(): AppResult<Unit> {
        tokenStorage.clearToken()
        _authState.value = AuthState.Unauthenticated
        return AppResult.Success(Unit)
    }

    override fun observeAuthState(): Flow<AuthState> = _authState.asStateFlow()
}

ランタイムでのプラットフォームチェック

// expect/actual が過剰な場合は、ランタイムチェックを使用
enum class Platform {
    Android, iOS, Desktop, Web
}

expect val currentPlatform: Platform

// androidMain
actual val currentPlatform: Platform = Platform.Android

// iosMain
actual val currentPlatform: Platform = Platform.iOS

// desktopMain
actual val currentPlatform: Platform = Platform.Desktop

// wasmJsMain
actual val currentPlatform: Platform = Platform.Web

// 使用例
@Composable
fun AdaptiveComponent() {
    when (currentPlatform) {
        Platform.Android -> AndroidSpecificUI()
        Platform.iOS -> IOSSpecificUI()
        Platform.Desktop -> DesktopSpecificUI()
        Platform.Web -> WebSpecificUI()
    }
}

テスト

共有テスト(commonTest)

// core/common/src/commonTest/kotlin/ResultTest.kt
class ResultTest {
    @Test
    fun `map transforms success value`() {
        val result: AppResult<Int> = AppResult.Success(5)
        val mapped = result.map { it * 2 }

        assertTrue(mapped is AppResult.Success)
        assertEquals(10, (mapped as AppResult.Success).data)
    }

    @Test
    fun `map preserves error`() {
        val result: AppResult<Int> = AppResult.Error("test error")
        val mapped = result.map { it * 2 }

        assertTrue(mapped is AppResult.Error)
    }
}

プラットフォーム固有のテスト

// androidTest - Robolectric またはインストゥルメントテストを使用
// iosTest - シミュレーターで実行
// jvmTest - 標準 JUnit

ベストプラクティス

推奨事項

  • 可能な限り多くのコードを commonMain に配置する
  • プラットフォーム API にのみ expect/actual を使用する
  • プラットフォーム固有の実装は最小限に保つ
  • プラットフォーム依存関係には依存性注入を使用する
  • モジュール間では api よりも implementation を使用する
  • データ転送には不変データクラスを使用する
  • テスト可能性のためにインターフェースに依存する

注意事項

  • プラットフォームに依存するコードパスでは、expect/actual を乱用しない
  • プラットフォーム固有の依存関係を共通コードに漏らさない
  • 複数プラットフォームにわたる状態管理のためにシングルトンを使用しない
  • KMP ターゲットを避けて iOS 開発者をブロックしない
  • Module ではなく Framework を version カタログに入れない

よくある落とし穴

  • Kotlin バージョンと互換性のある KMP ライブラリバージョンを使用していない
  • プラットフォーム固有のテストを、共有テストコードではなくテストランナーに委譲するのを忘れている
  • 異なるプラットフォームで異なるモジュール解決戦略(Gradle モジュールメタデータなど)を考慮していない

重要: KMP セットアップを 1 から始めるときは、最初に KMP 対応のプロジェクトウィザード(Android Studio など)を常に使用してください。手動セットアップはエラーが発生しやすく、無視するのが難しい微妙なバグにつながる可能性があります。

関連スキル