kotlin について
型付き状態モデル、明確な非同期処理の境界、Androidに即した連携を重視するKotlin開発向けスキルです。AndroidコードやKotlin製Nitro Moduleを扱う開発者を対象としています。
主な機能
- 01型安全な状態モデリング
- 02非同期処理とスレッドの指針
- 03ノンブロッキングなプロパティ設計
- 04Androidヘルパーとファイルの整理
- 05Nitro Moduleの実装指針
活用シーン
- 01Kotlin APIの状態モデリング
- 02コルーチンとディスパッチャー境界の選定
- 03Android実装ファイルの整理
- 04Kotlin Nitro Moduleの実装
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ margelo/ react-native-skills --skill kotlinインストール後、Claude Code で/kotlinを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
Kotlin
強力な型モデリング、明示的な非同期境界、慣用的な Android 連携が必要な Kotlin コードには、このスキルを使用してください。コードが Nitro Module の一部である場合は、生成される仕様、Promise のマッピング、アノテーション、HybridObject の制約に対応するため、build-nitro-modules と組み合わせてください。
ワークフロー
- 編集する前に、ローカルの Kotlin コード、生成された抽象クラス、公開 API の形状を確認します。
- 動作を実装する前に、Kotlin の型を使って有効な状態をモデル化します。
- 同期処理、コルーチンベースの処理、明示的にディスパッチする処理のいずれにするかを選択します。
- プロパティは低コストで、ブロックしないようにします。
- スレッドの切り替え、Android サービスへのアクセス、I/O、失敗する可能性のある処理を API 上で明示します。
型安全な API 設計
- 状態のバリエーションは、sealed インターフェース/クラス、通常のインターフェース、または具象クラスで表現します。多数の nullable フィールドを持つ単一のデータクラスでバリエーションを表現しないでください。
- 関連する値は、それらが有効なバリエーション上で非 null にします。
barcodeとbarcodeTypeが常に一緒に存在する必要があるなら、それらをScannedBarcodeに含めます。 - nullable 型は、1 つの状態内で値が実際に存在しない場合に使用し、「このオブジェクトが別の種類かもしれない」ことを表すためには使用しません。
- 呼び出し側で null を確認するより、コンパイル時の型の絞り込みを優先します。関連フィールドに対する null チェックが繰り返し必要になる場合は、通常、モデルが緩すぎることを示しています。
// Avoid: related fields are nullable and the valid combinations are implicit.
data class ScannedData(
val position: Point,
val text: String?,
val barcode: String?,
val barcodeType: BarcodeType?,
val face: Rect?,
)
// Prefer: each variant exposes only the fields that are valid for it.
sealed interface ScannedData {
val position: Point
}
data class ScannedText(
override val position: Point,
val text: String,
) : ScannedData
data class ScannedBarcode(
override val position: Point,
val barcode: String,
val barcodeType: BarcodeType,
) : ScannedData
data class ScannedFace(
override val position: Point,
val face: Rect,
) : ScannedData
非同期処理とスレッド処理
- 自然に中断する API や、すでにコルーチンをサポートしている Android の I/O にはコルーチンを使用してください。
- 高速で決定論的なローカル処理は同期的に保ってください。単純な値の構築、キャッシュされたメタデータ、純粋な変換のために、コルーチン、ディスパッチャー、Promise の配線を導入しないでください。
- 呼び出し元のスレッドで実行すべきでない、CPU 負荷の高い同期処理には、明示的に所有するディスパッチャー/スコープ、または Nitro の
Promise.parallelを使用してください。 - Android API が必要とする場合(ビュー/UI の変更やライフサイクル API など)を除き、メインディスパッチャーは避けてください。メインスレッド上の処理は小さく保ち、解析、変換、I/O、セッションのネゴシエーション、CPU 処理は、所有するディスパッチャーまたは非同期 API に移してください。
- ライブラリコード、プロパティのゲッターやセッター、JS 向けエントリーポイントで
runBlockingを使用しないでください。 - スレッド切り替え、サービスの検索、ブロッキング I/O、権限の処理、失敗する可能性のあるネイティブ処理を、プロパティの背後に隠さないでください。
- 値が特定の Android コールバック/スレッドから通知される場合は、リスナー、Flow、イベント API を優先してください。Nitro の仕様では、リスナーまたは
Promise<T>メソッドを公開してください。 launch、withContext、ディスパッチャー、ハンドラー、エグゼキューターを繰り返し経由することは、アーキテクチャ上の問題の兆候として捉えてください。コンポーネントは、使用するコルーチンスコープ/ディスパッチャー/ライフサイクルを自ら所有するか、公開する非同期境界またはネイティブコールバック境界で一度だけ所有者に処理を渡すようにしてください。- ワークフローが、ネストされた複数の箇所でメイン、IO、デフォルト、ネイティブ、JS/Nitro のコンテキスト間を行き来する場合は、いったん止めて、オブジェクト/ライフサイクル/API を再設計してください。過剰な切り替えはレイテンシを隠し、処理順序の把握を難しくし、将来のパフォーマンス問題につながります。
- 競合状態や準備完了状態の不具合を、
delay、Thread.sleep、Handler.postDelayed、タイマー、追加のディスパッチャー切り替え、同じメソッドの二度呼び出しで修正しないでください。代わりに、所有者のディスパッチャー/ライフサイクル、状態マシン、コールバック/イベント、Flow、または非同期 API の境界を修正してください。ハードウェア、OS サービス、リモートサービス、ネットワークなど、外部要因による非決定性がある場合に限り再試行し、再試行の回数を制限し、キャンセル可能かつ冪等にしてください。 - 変更可能な共有状態は、単一のコルーチンスコープ、ディスパッチャー、ロック、または Android コンポーネントのライフサイクルが所有するようにしてください。明確な境界なく、異なる所有モデルを混在させないでください。
Mutex、synchronized、ReentrantLockは、リスナーやコールバックの処理でデフォルトとして使うものではなく、最後の手段としての同期手段と捉えてください。追加する前に、共有される変更可能な値、並行して呼び出す側、単一の所有者ディスパッチャー/ライフサイクル、チャネル/Flow、または不変のスナップショットでは不十分な理由を明確にしてください。- コールバックの呼び出し中、JS/Nitro の呼び出し中、または未知のユーザーコードの呼び出し中に、ロックを保持しないでください。必要に応じてロック中にリスナーのスナップショットを取得し、ロックを解除してから呼び出してください。処理中のイベント発行と同時に削除されたリスナーにも、そのイベントは届くことがあります。
プロパティ
- コストの低いメモリ内の状態や派生値には、
valとvarを使用してください。 - 副作用、I/O、Android サービスの呼び出し、権限の確認、割り当て、ブロッキング処理、または失敗する可能性のある操作には、メソッドを使用してください。
- プロパティの取得に別のスレッドを待つ必要がある場合は、非同期メソッドまたはイベントとして再設計してください。
// Avoid: blocking work hidden in a getter.
val status: SessionStatus
get() = runBlocking { session.status() }
// Prefer: make the async boundary explicit.
fun getStatus(): Promise<SessionStatus> {
return Promise.async {
session.status()
}
}
Kotlin スタイル
- デフォルトではクラスを final にします。Kotlin のクラスは
openとして宣言しない限りすでに final なので、継承を意図している場合を除き、そのままにします。 - 単純な値には
data classを、所有するリソースやライフサイクルを持つものには通常のクラスを優先します。 - 閉じた結果の種類には
sealed interfaceまたはsealed classを優先します。 - 無効な入力や無効な状態には、
require、check、または専用の例外を使用します。ユーザーが到達できる失敗を暗黙的に何もしないままにしないでください。 - Java との相互運用は明示的に行います。実用上可能な場合は、プラットフォーム型を Kotlin の型に変換してからパブリック API で公開します。
- 事前のチェックによって不変条件が明らかな狭い境界を除き、
!!は避けます。早期リターン、requireNotNull、または型付きの状態を優先します。 - 複数行の制御フロー内では、明示的な
return文を優先します。式のように見せるためだけに、複数行のtry/catch、if、when、ラムダの外へreturnを移動しないでください。try { return value } catch (...) { return fallback }のようにします。 - 複数行のラムダでは、結果を返す際に
return@map valueのようなラベル付きリターンを使用します。items.map { it.toString() }のような真の単一式ラムダに限りラベルを省略します。items.map { return@map it.toString() }のようには書かないでください。 - 曖昧さのない単一式ラムダには、簡潔なインライン記法を優先します。複数行の
map { format -> format.toMLKitFormat() }ではなく、formats.map { it.toMLKitFormat() }とします。外側またはネストしたラムダですでにitが使われている場合はitを使用せず、ネストしたラムダではパラメーターに名前を付けるか、明確さが必要な場合に名前を付けます。 - ファイル名はスコープの契約として扱います。
HybridDataScanner.ktはHybridDataScannerを実装するべきであり、Android ヘルパー、ジオメトリ変換、リスナー、拡張ユーティリティを併せて含めるべきではありません。 - ファイルごとにトップレベルの実装型を 1 つだけにします。プライマリ型の下に、補助クラス、インターフェース、列挙型、オプションアダプター、コーディネーター、デリゲート、ヘルパー型を置かないでください。ヘルパーに型が必要なら、専用のファイルを用意します。
- ファイルごとに、目的を絞った拡張/変換を 1 つだけにします。すべてのヘルパーが拡張関数だからといって、何でも入れる拡張ファイルを作らないでください。
Barcode+toScannedCode.kt、TargetBarcodeFormat+toMLKitFormat.kt、BarcodeFormat+fromMLKitBarcodeFormat.ktのように名前を付けたファイルに変換を分け、適切な場合はinternal可視性にします。 - 拡張
fun、val、varの宣言は、たとえprivateで小さく、そのファイルでしか使われなくても、Hybrid*実装ファイルやその他のプライマリ実装ファイルに置かないでください。コードの分割、保守性、将来の差分を整理された状態に保つため、すべての拡張はType+operation.kt形式の専用の名前付き拡張/変換ファイルに置きます。 - Kotlin で許可されているからといって、プライマリクラスの下にプライベートなトップレベル拡張プロパティ/関数を置かないでください。コンパニオン拡張、列挙型/プラットフォームのマッピング、バーコード形式の変換、ML Kit/CameraX アダプターは、それぞれ専用の名前付きファイルに置きます。
- 同じファイル内のプライベートなトップレベルヘルパーは、小さな非拡張関数/プロパティであり、そのファイルのプライマリ型をサポートすることだけを目的とする場合に限り使用します。
- Android の事前チェックとプラットフォームの定型処理は、
Hybrid*ファクトリーや実装メソッドから切り出します。権限チェック、マニフェストの機能/権限の検証、PackageManagerの機能チェック、サービスの利用可否チェック、および同様のセットアップガードは、目的を絞ったヘルパーファイルに置きます。HybridObject の呼び出し箇所は 1 行か 2 行に保ちます。 - これらのヘルパー用に、広範な
Utils.ktファイルを作らないでください。プラットフォーム型またはドメインチェックに基づいてファイル名を付け、各ヘルパーの動作を 1 つに絞ります。 - 行数はレビュー時の指標として使用します。1 型 1 ファイルのルールと、ファクトリーにヘルパーを置かないルールを満たした後であれば、約 300 行未満のファイルは通常問題ありません。ヘルパー、変換、Android 関連の接着コードによるサイズ増加は、設計上の問題です。
- 変換が単一要素を読み取るだけの場合は、
Listや配列型ではなく要素型に変換を持たせます。Array<TargetBarcodeFormat>.toMLKitFormats()よりも、TargetBarcodeFormat.toMLKitFormat()を用意して呼び出し箇所でformats.map { it.toMLKitFormat() }とする方法を優先します。 - ドメイン変換を
Int、String、Double、Anyのような広範で汎用的なレシーバーに、たとえprivateであっても持たせないでください。Int.toBarcodeFormat()よりも、BarcodeFormat.Companion.fromFormat(format: Int)のようにドメイン型のコンパニオン/ファクトリー側に持たせます。 - コレクション拡張関数は、要素をまたいだ検証、重複排除、並べ替え、バッチ処理、キャッシュ、空でないことのチェック、エラー集約など、コレクションに実質的なドメイン動作がある場合に限り追加します。レシーバーが
Array<SomeDomainType>またはList<SomeDomainType>で、本体の大部分がmap { ... }である場合は、呼び出し元のコードに置きます。 - Kotlin のコンバーターが Android プラットフォームの
Int定数を返す、または受け取る場合、API がサポートしていれば、関数、戻り値、またはパラメーターに CameraX の@ImageCapture.FlashModeのような、対応する AndroidX/Java/Kotlin アノテーションを適用します。アノテーションを付ける前に、ターゲットと保持期間を確認してください。プラットフォーム API が通常のIntしか公開していない場合に、やみくもにアノテーションを付けないでください。 - アノテーション付きのプラットフォーム
Intから変換する場合、アノテーションのターゲットがパラメーターに対応していれば、format: Intパラメーターにアノテーションを付けます。たとえば、型付けされていないプリミティブ入力よりも、Int.toBarcodeFormat()よりも、BarcodeFormat.Companion.fromFormat(@SomeBarcodeFormat format: Int)を優先します。
Nitro の注意事項
- Kotlin の HybridObject 実装は、生成された
Hybrid*Specクラスを継承し、@Keepと@DoNotStripを付与する必要があります。 - サスペンド処理や I/O 処理には
Promise.asyncを、CPU バウンドの同期処理にはPromise.parallelを使用します。 Promise<T>インスタンスを手動で作成したり、受け渡したりすることは避けてください。Promise.async、Promise.parallel、Promise.resolved、Promise.rejectedを優先して使用します。手動で Promise を使うのは、サスペンド API としてラップできない、ネイティブの完了通知、リスナー、コールバックとのブリッジが実際に必要な場合に限ります。- Google の
Task<T>などの一般的なコールバック API では、まずTask+await.ktのような専用の拡張ファイルに汎用のサスペンドアダプターを記述し、Promise.async { task.await() }から呼び出します。公開 HybridObject メソッド内でaddOnSuccessListener/addOnFailureListener/addOnCanceledListenerを直接つなぎ合わせないでください。 Promise<void>にはPromise<Unit>を使用します。NitroModules.applicationContextには遅延アクセスし、利用できない場合は明示的に失敗させます。- 生成されるプロパティは、同期的な JS エントリーポイントです。Android のスレッド制約への対応や非同期処理が必要な場合は、メソッドまたはリスナーとして設計し直してください。
コメント
Markdown 対応