はじめに
LIFULL Advent Calendar 2025 11日目の記事です。
普段はネイティブアプリエンジニアとしてAndroidアプリの開発を行っています。
1. 導入:今後のKMP/CMP開発を見据えたテンプレートアプリの構築
今後、KMP/CMPを活用したアプリケーションを個人で開発していくために、その基盤となるテンプレートアプリを作成しています。このテンプレートは、ネットワーク、ローカルDB、DI、バックエンド連携といった、実際のアプリケーション開発で必要となるコア機能を網羅し、初期セットアップの手間を大幅に削減することを目的としています。本記事では、このテンプレート構築の過程で採用したライブラリ群(Ktor, SQLDelight, Koin, Supabase)の技術的な役割と、マルチプラットフォーム開発特有の課題にどう対処したか(特にAPI Key管理)を解説します。
テンプレートの目的
- 初期設定コストの削減: 新規プロジェクト開始時に頻出するライブラリ導入や環境設定の手間を省き、簡単に機能開発できるようにする。
- 学習目的: 2025年のDroidKaigiに参加した際にCMP/KMPの話が多く、CMP/KMPのアプリ0から作成する場合やライブラリ選定の部分の学習をしたいと思ったため、今回のテンプレートアプリ作成は、KMP/CMPのライブラリ選定と実装パターンを学ぶための学習を兼ねています。
作成したアプリのデモ動画
採用技術スタック
今回作成したテンプレートでは、以下の技術スタックを採用しました。選定理由などについては追って記述します。
- UIフレームワーク: Compose Multiplatform (CMP)
- コアロジック: Kotlin Multiplatform (KMP)
- ネットワーク: Ktor (HTTPクライアント)
- ローカルデータベース: SQLDelight (型安全なローカルDB)
- 依存性注入 (DI): Koin
- バックエンドサービス (BaaS): Supabase (認証・DB)
2. KMP/CMPの基本解説
-
Kotlin Multiplatform (KMP):
ビジネスロジック、データ層、通信処理などの共通コードをKotlinで一度だけ記述し、JVM、Android、iOS、Webなど複数のプラットフォームで共有するための技術です。commonMainソースセットに記述されたコードが各プラットフォーム向けにコンパイルされます。 -
Compose Multiplatform (CMP):
Android向けUIツールキット「Jetpack Compose」を、Desktop (JVM) や iOS に展開するフレームワークです。KMPの仕組みを利用し、UI層においても高いコード共通化を実現します。
本プロジェクトでは、UIをCMPで、それ以外をKMPの共通ソースセット(commonMain)に記述することで、最大限のコード共通化を目指す戦略をとっています。そのため、プロダクトコードについてはKotlinのみを書くようにしています。
3. プロジェクトの作成
今回作成したCMP/KMPのプロジェクトは下記のブログを参考にさせて頂きながら、作成しました。
今回特に便利だと感じたのが、kdoctorによるインストール状況の確認とAndroid Studio側からiOSAppを選択するとiOSのエミューレータが立ち上がり動作確認を行える点です。毎回xCode自体を開いてBuildをしてという作業なしでコードを書いているAndroid Studioから直接起動できる点がとても便利だと感じました。
4. テンプレートのコア構成技術の役割と実装
全体の構成図はこのようになりました。
ネットワーク (Ktor)
-
役割と選定理由
マルチプラットフォーム対応のHTTPクライアントです。Ktorを選択した理由としてはJetBrains社が提供しているということもあり今回はKtorを採用しました。
データベース (SQLDelight)
-
役割
SQLクエリから型安全なKotlinコードを自動生成するマルチプラットフォーム対応のローカルDBライブラリです。 今回この部分ローカルDBについてはRoomが利用出来ているのですが、今回はSQLDelightを選択しました。 今後SQLを記述して実装するプロダクトを作成する際、SQLDelightの方が型安全にSQLを扱える点や、個人の学習という面で利点があると考え選定しました。ただ、RoomについてもAndroidエンジニアとしてはどのようにして、KMPと繋ぎ込みを行い利用していくのかなどについては気になっているので、今後対応できるのであればSQLDelightではなく、Roomのバージョンのアプリも作ってみたいと思っています。
依存性注入 (Koin)
-
役割
Repository・UseCase・ViewModelなどの依存管理を一元化し、疎結合とテスタビリティを向上を目的としてDIのライブラリ導入を行いました。現状テンプレートアプリなので、Testの作成等は行えていないのですが、今後プロダクト作成の際に必要になるため追加しておきました。
バックエンド連携 (Supabase)
-
役割
今回、Supabaseについては、データベースアクセスや認証、Storageのために導入を行いました。Firebaseとどちらにするかというところで悩んだのですが、Supabaseを今まで利用したことがありませんでした。また、SupabaseはKMPに対して公式のSDKを提供しているということから、今回はSupabaseを選択しました。
Supabase上にProjectを作成して、Databaseからデータを取得してくる部分までの作成を行なっています。今後認証部分についてもSupabaseを用いて実施する想定となっています。認証部分まで実装することが出来た際にテンプレートアプリとして公開しようと考えています。
SupabaseのPostgreSQL Databaseにcountriesというテーブルを作成して
{
id: 1,
created_at: 2024-11-22 10:47:03+00,
name: Japan
}
のような情報を追加して下記のように呼び出しを行なっています。(こちらに関してエラーハンドリング周りは追加が必要かと思います)今回Repository, Usecase, ViewModelと階層を分けて最終的にCMPのViewに値を反映するようにしています。
// RepositoryのImpl
class CountryRepositoryImpl(
private val supabaseClient: SupabaseClient
) : CountryRepository {
override suspend fun getCountries(): List<Country> {
return try {
supabaseClient.postgrest["countries"].select().decodeList<Country>()
} catch (e: Exception) {
println("Error getting countries: ${e.message}")
emptyList()
}
}
}
5. テンプレート構築時に直面した技術課題と解決策
課題: Supabase API Keyの安全な管理
問題
Supabaseとの接続の際に、APIキーをハードコーディングせず、かつ Git にコミットしない安全な管理が必要ありました。普段はAndroid/iOSそれぞれ固有の方法で対応しますが、KMP/CMPでは共通コードからプラットフォームごとに異なるAPIキーに安全にアクセスし、かつGitコミットから除外する仕組みが必要になりました。iOS側を対応するためにbuildKonfigを利用して対応を行いました。
解決策(Android):
Androidについてはlocal.propertiesにSupabaseのAPIキーを記載してそれを読み込むようにして、対応をまず行いました。local.propertiesについては.gitignoreを利用して対応を行なっています。しかしこの設定だけではiOS側の対応を行うことができなかったため、iOS側の対応を行うためにbuildKonfigを利用しました。
解決策(iOS)
buildKonfig を使用を利用するようにして、iOS側でもAPIキーにアクセスすることが出来るようにしました。
こちらについて、テンプレートアプリとして公開する際にはそれぞれのOSに合った本番公開などを行う際にも安全面を考慮したAPI Key管理が出来るようにしてからにしようと思っています。
buildKonfig 導入例
local.properties
SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_KEY=YOUR_SUPABASE_KEY
composeApp/build.gradle.kts
こちらの実装部分に関してはエラー処理の部分などにおいて改良できるかと思っています。
val localProperties = Properties()
val localPropertiesFile = project.rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { localProperties.load(it) }
}
/**
* 実際のプロダクトでは、キーが取得できない場合はビルドエラーにするなどをした方が良いかと思います。
* 今回は説明簡略化のため空文字を返していますが、テンプレート公開時は改善しようと思っています。
*/
fun getSupabaseUrl(): String {
return localProperties.getProperty("SUPABASE_URL") ?: ""
}
fun getSupabaseKey(): String {
return localProperties.getProperty("SUPABASE_KEY") ?: ""
}
buildkonfig {
packageName = "sample"
defaultConfigs {
buildConfigField(
STRING,
"SUPABASE_URL",
getSupabaseUrl()
)
buildConfigField(
STRING,
"SUPABASE_KEY",
getSupabaseKey()
)
}
targetConfigs {
create("ios")
}
}
利用側(共通コード)
val client = SupabaseClient(
supabaseUrl = BuildKonfig.SUPABASE_URL,
supabaseKey = BuildKonfig.SUPABASE_KEY
)
6. まとめと今後の展望
今回、自身の学習と、今後の趣味開発の基盤となるネイティブアプリのテンプレートを作成したので、その内容を記事にまとめました。普段Androidエンジニアとして働いていて、アプリを0から作ることが少なく、このアプリを作る中でも学びがとても多かったです。今後は、現状作れていない認証機能の追加などを進め、より汎用的なテンプレートとして GitHub 公開を目指します。読んでいただきありがとうございました。

