Kotlin × Compose × Domain DSL の統合設計
「UIを宣言しながらドメインを語る」──Kotlin DSLが導く宣言的アーキテクチャ
1. 背景:宣言的UIとドメインの断絶
Jetpack Compose に代表される宣言的UIでは、
UIは「状態」を描画するだけの関数的表現になりました。
しかし多くの現場では:
- UI DSL(Compose)と
- ドメインモデル(UseCase / Entity)が
別世界で定義され、状態とロジックの橋渡しが複雑化しています。
本記事ではこの断絶を埋め、
Composeの中でドメイン語彙をDSL的に語れる設計を目指します。
2. DSL統合の発想 — Composeとドメインの橋渡し
Compose は関数そのものが DSL です。
つまり、
「Compose DSL × Domain DSL」=宣言的な業務UI
を構築できます。
設計イメージ(概念図):
Compose の UI DSL が Domain DSL を型安全に操作する構造を取ることで、
UI層がドメインを直接「読む・書く・反応する」世界が実現します。
3. Domain DSL の基本形
まず、業務ロジックを DSL 的に表す TaskDomain を設計します。
@DslMarker
annotation class DomainDsl
@DomainDsl
class TaskDomain {
private val tasks = mutableListOf<Task>()
data class Task(val title: String, val done: Boolean = false)
fun task(title: String, done: Boolean = false) {
tasks += Task(title, done)
}
fun all(): List<Task> = tasks
}
これで次のように「宣言的なドメイン記述」が可能です:
val project = TaskDomain().apply {
task("Write Article")
task("Refactor DSL Engine", done = true)
}
4. UI層(Compose)へのDSL統合
Compose は受容者付き関数と相性抜群。
TaskDomain を直接受け取って DSL 的に描画します。
@Composable
fun TaskScreen(domain: TaskDomain) {
Column {
domain.all().forEach { task ->
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = task.done, onCheckedChange = { })
Text(task.title)
}
}
}
}
利用:
@Composable
fun MainScreen() {
val domain = remember {
TaskDomain().apply {
task("Design DSL")
task("Implement Compose Integration")
}
}
TaskScreen(domain)
}
これで UI は「ドメインを語る」DSL的存在になります。
5. 双方向連携:ViewModel × DSL
次に、TaskDomain を ViewModel に保持し、
UI からの変更を双方向に伝達できるようにします。
class TaskViewModel : ViewModel() {
private val _domain = MutableStateFlow(TaskDomain())
val domain = _domain.asStateFlow()
fun addTask(title: String) {
_domain.update { old ->
old.apply { task(title) }
}
}
}
Compose 側では StateFlow を collectAsState:
@Composable
fun TaskApp(viewModel: TaskViewModel = viewModel()) {
val domain by viewModel.domain.collectAsState()
Column {
TaskScreen(domain)
Button(onClick = { viewModel.addTask("New Task") }) {
Text("Add")
}
}
}
6. Clean Architecture 構造での配置
domain/
├── TaskDomain.kt ← Domain DSL
├── model/Task.kt ← Entity
└── usecase/TaskUseCase.kt
presentation/
├── viewmodel/TaskViewModel.kt
├── ui/TaskScreen.kt ← Compose DSL
└── ui/TaskApp.kt
- Domain DSL は Entity + BusinessRule に近い層
- Compose DSL は UI宣言層
- 双方を ViewModel が橋渡し
7. 実装例:Task管理DSL × Compose
統合例コード:
@DomainDsl
class TaskDomain {
data class Task(val title: String, val done: Boolean = false)
private val _tasks = mutableStateListOf<Task>()
val tasks: List<Task> get() = _tasks
fun task(title: String, done: Boolean = false) {
_tasks += Task(title, done)
}
fun toggle(index: Int) {
_tasks[index] = _tasks[index].copy(done = !_tasks[index].done)
}
}
@Composable
fun TaskScreen(domain: TaskDomain) {
Column(Modifier.padding(16.dp)) {
domain.tasks.forEachIndexed { index, task ->
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = task.done,
onCheckedChange = { domain.toggle(index) }
)
Text(task.title)
}
}
}
}
@Composable
fun TaskApp() {
val domain = remember { TaskDomain() }.apply {
task("Compose Integration")
task("Write Qiita Article")
}
TaskScreen(domain)
}
Compose の remember と Domain DSL の mutableStateListOf が連携することで、
UIがドメイン変更を自動追従する完全リアクティブ構造を実現。
8. 拡張設計:Validation・StateFlow・Intent化
1️⃣ Validation DSL
fun TaskDomain.validate() = tasks.all { it.title.isNotBlank() }
2️⃣ StateFlow + Intent DSL
sealed interface TaskIntent {
data class Add(val title: String): TaskIntent
data class Toggle(val index: Int): TaskIntent
}
class TaskViewModel : ViewModel() {
private val _domain = MutableStateFlow(TaskDomain())
val domain = _domain.asStateFlow()
fun onIntent(intent: TaskIntent) {
_domain.update {
when (intent) {
is TaskIntent.Add -> it.apply { task(intent.title) }
is TaskIntent.Toggle -> it.apply { toggle(intent.index) }
}
}
}
}
3️⃣ Compose 側で Intent DSL
Button(onClick = { viewModel.onIntent(TaskIntent.Add("New")) }) {
Text("Add Task")
}
→ ドメイン・UI・Intentが同一DSLパターンで統一される。
9. まとめとベストプラクティス
| レイヤ | DSL役割 | 実装技術 |
|---|---|---|
| Domain | 業務DSL | @DslMarker + mutableStateListOf |
| ViewModel | 状態DSL | StateFlow + Intent |
| UI(Compose) | 宣言DSL | Column / Row / Composable関数 |
| 全体統合 | 型安全な受容者DSL | remember + inline + sealed |
統合設計のポイント
- **「DomainをDSLとして定義」**し、ビジネス語彙をそのままコード化
- Compose DSLがDomainを描画し、状態変更も直結
- ViewModelは橋渡しとして最小限の責務に抑える
- 再利用可能・型安全・リアクティブな構造を維持
まとめ
Kotlin × Compose × Domain DSL は、
「UI」「状態」「ビジネスロジック」を
一貫したDSL設計哲学で統合する最もモダンなアプローチです。
ドメインが UI の中で語られ、
UI が ドメインの構文になる。
それが Kotlin DSL の真価です。