0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】Kotlin × Compose × Domain DSL の統合設計

Posted at

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 の真価です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?