現在のアーキテクチャは、「Google公式のAndroidアーキテクチャガイドライン」と「クリーンアーキテクチャ(軽量版)」を組み合わせた MVVM + UDF(単方向データフロー) を採用している。
graph TB
subgraph "composeApp (Presentation)"
App["App.kt"]
HS["HomeScreen"]
MS["MatchScreen(STANDALONE)"]
subgraph "UI Components"
SBD["ScoreBoardDisplay\n(表示部)"]
SC["ScoreController\n(操作部)"]
TC["TimerControls"]
EM["EndMatchDialog"]
end
VM["MatchViewModel"]
end
subgraph "shared (Domain)"
MM["MatchManager"]
MT["MatchTimer"]
IR["IbjjfRule"]
end
App --> HS
App --> MS
MS --> SBD
MS --> SC
SC --> TC
SC --> EM
MS --> VM
VM --> MM
MM --> MT
MM --> IR
style SBD fill:#FF9800,color:#fff
style SC fill:#2196F3,color:#fff
style MM fill:#607D8B,color:#fff
composeAppsharedユーザーの操作が下へ流れ、状態が上へ流れる構造。
// ドメインレイヤー (MatchManager.kt)
class MatchManager {
// 状態のストリーム(川)
private val _state = MutableStateFlow(MatchState())
val state: StateFlow<MatchState> = _state.asStateFlow()
// イベントを受け取って状態を更新
fun addPoint() {
_state.value = _state.value.copy(points = _state.value.points + 2)
}
}
// UIレイヤー (MatchScreen.kt)
@Composable
fun MatchScreen(viewModel: MatchViewModel) {
// 状態を購読して画面を自動更新
val state by viewModel.state.collectAsState()
Button(onClick = { viewModel.onAddPoint() }) {
Text("Points: ${state.points}")
}
}
1. Room Multiplatform (コードファースト) Kotlinのコードにアノテーションを付けてSQLを生成させる。Android開発者にお馴染み。
// Roomのコード例
@Entity(tableName = "match_history")
data class MatchHistory(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val winnerName: String
)
@Dao
interface MatchHistoryDao {
@Query("SELECT * FROM match_history")
fun getAll(): Flow<List<MatchHistory>>
}
2. SQLDelight (SQLファースト)
.sq ファイルに生のSQLを書き、そこからKotlinコードを自動生成させる。KMPでの圧倒的な実績がある。
-- SQLDelightのコード例 (MatchHistory.sq)
CREATE TABLE match_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
winnerName TEXT NOT NULL
);
selectAll:
SELECT * FROM match_history;
Room も SQLDelight も、裏側で動いているのは全く同じ SQLite(.db ファイル)である。
RealmのようなNoSQLからRoom(SQL)への移行は「地獄の移行作業」になるが、Room ⇔ SQLDelight 間の移行はデータベースファイルの引っ越しが不要なため、比較的低コストで乗り換え可能。
現在はライブラリを使わず、コンストラクタで依存を注入している。
// 手動DIの例
class MatchViewModel(
private val rule: MatchRule = IbjjfRule() // デフォルト引数で注入
) : ViewModel() { ... }
初期フェーズは手動DIで十分だが、将来的に「別団体ルールの追加」などで依存関係が増えたタイミングでライブラリを導入すべき。 KMPでは Koin がデファクトスタンダードとなっている(HiltはKMPでは動かせないため)。
// Koinを使ったDIの例
val appModule = module {
// IbjjfRuleをシングルトンとして登録
single<MatchRule> { IbjjfRule() }
factory { MatchViewModel(get()) }
}
// 使う側
val viewModel: MatchViewModel by inject()
KMPの commonMain ではRetrofitは利用できないため、JetBrains公式の Ktor Client を利用する。エンジンを各OSで切り替えることができる。
// Ktor Clientのコード例
val client = HttpClient {
install(ContentNegotiation) {
json() // JSONパースを自動化
}
}
// 実際の通信(suspend関数内)
suspend fun fetchTournamentInfo(): TournamentInfo {
return client.get("https://api.example.com/tournament").body()
}
FireStoreのようなリアクティブな仕組みや非同期処理は、KMPでは Kotlin Coroutines / Flow が標準。
コールバックを使わず、上から下へ直感的に書ける。
// Coroutinesの例
viewModelScope.launch {
// ネットワーク通信などの重い処理
val data = fetchTournamentInfo()
updateUi(data) // 通信完了後に実行される
}
時間経過とともに流れてくるデータのストリーム。本アプリのスコア管理 (MatchManager の StateFlow) にも使われている仕組み。
// Flowの例
fun observeTimer(): Flow<Int> = flow {
var time = 0
while (true) {
emit(time) // 1秒ごとに時間を流す
delay(1000)
time++
}
}
// 受け取る側
observeTimer().collect { currentTime ->
println("現在の時間: $currentTime")
}
本記事で解説した各技術・アーキテクチャの公式ドキュメント(一次情報)は以下の通りです。