🍡
Gradle APIとMermaidを使ってmoduleの依存関係を図示する
はじめに
マルチモジュールのAndroidアプリにおいて、Gradle APIとMermaidを使って図1のようなモジュールの依存関係を図示する方法を紹介します。
図1 公式doc「What is modularization?」より引用
注意事項
- 本記事で使用するGradle APIは非推奨となっており、Gradle 9で削除されることがアナウンスされています。(詳しくは後述)
- 軽いお遊びのような記事なので正確性に欠けている可能性があります。
実装
基本的に Project.java や ProjectDependency.java のAPIを使って実装します。コードの全体を載せます。
root/build.gradle.kts
tasks.register("dependencyGraph") {
val dependencyGraph = getDependencyGraph()
val mermaid = createMermaidText(dependencyGraph.joinToString("\n"))
saveFile(mermaid)
}
fun getDependencyGraph(): List<String>{
val graph = mutableListOf<String>()
rootProject.subprojects.forEach { project ->
val parentModule = project.path
val moduleDependency = buildList {
project.configurations
.filter { it.name.lowercase().endsWith("implementation") || it.name.lowercase().endsWith("api") }
.forEach { config ->
config.dependencies
.withType(ProjectDependency::class.java)
.map { it.dependencyProject } ............ (※1)
.filter { it != project }
.forEach { add(it) }
}
}
val projectMermaidGraph = moduleDependency.map {
val childModule = it.path
"${parentModule}[$parentModule] --> ${childModule}[$childModule]"
}.joinToString("\n")
graph.add(projectMermaidGraph)
}
return graph.toList()
}
fun createMermaidText(graph: String) = """graph TB
$graph
"""
fun saveFile(graph: String) {
val outputFile = file("${rootDir.absolutePath}/dependencyGraph.txt")
outputFile.writeText(graph)
}
簡単に処理の流れを説明すると、
- モジュールの依存関係を洗い出す
- Mermaid形式のテキストに変換する
- テキストを書き出したファイルを保存する
になります。
Mermaidを使うとflowchartを書くことができます。このMermaidの形式に合わせて依存関係をテキストに出力します。Mermaidに関しては公式docのflowchartを参考にしてください。
実際に使ってみる
有名どこのアプリで上記のGradle Taskを実行しdependencyGraph.txt
の中身をMermaidの Live Editor にコピペして完成した図を見てみます。
1. DroidKaigi 2023
出力結果
graph TB
:app-android[:app-android] --> :feature:main[:feature:main]
:app-android[:app-android] --> :feature:contributors[:feature:contributors]
:app-android[:app-android] --> :feature:sessions[:feature:sessions]
:app-android[:app-android] --> :feature:about[:feature:about]
:app-android[:app-android] --> :feature:sponsors[:feature:sponsors]
:app-android[:app-android] --> :feature:floor-map[:feature:floor-map]
:app-android[:app-android] --> :feature:achievements[:feature:achievements]
:app-android[:app-android] --> :feature:staff[:feature:staff]
:app-android[:app-android] --> :core:model[:core:model]
:app-android[:app-android] --> :core:data[:core:data]
:app-android[:app-android] --> :core:designsystem[:core:designsystem]
:app-android[:app-android] --> :core:ui[:core:ui]
:app-android[:app-android] --> :core:testing[:core:testing]
:app-ios-shared[:app-ios-shared] --> :core:model[:core:model]
:app-ios-shared[:app-ios-shared] --> :core:data[:core:data]
:app-ios-shared[:app-ios-shared] --> :core:ui[:core:ui]
:app-ios-shared[:app-ios-shared] --> :feature:contributors[:feature:contributors]
:core:data[:core:data] --> :core:model[:core:model]
:core:data[:core:data] --> :core:common[:core:common]
:core:testing[:core:testing] --> :core:model[:core:model]
:core:testing[:core:testing] --> :core:designsystem[:core:designsystem]
:core:testing[:core:testing] --> :core:data[:core:data]
:core:testing[:core:testing] --> :core:ui[:core:ui]
:core:testing[:core:testing] --> :feature:main[:feature:main]
:core:testing[:core:testing] --> :feature:sessions[:feature:sessions]
:core:testing[:core:testing] --> :feature:about[:feature:about]
:core:testing[:core:testing] --> :feature:sponsors[:feature:sponsors]
:core:testing[:core:testing] --> :feature:floor-map[:feature:floor-map]
:core:testing[:core:testing] --> :feature:achievements[:feature:achievements]
:core:testing[:core:testing] --> :feature:staff[:feature:staff]
:core:ui[:core:ui] --> :core:common[:core:common]
:core:ui[:core:ui] --> :core:designsystem[:core:designsystem]
:core:ui[:core:ui] --> :core:data[:core:data]
:feature:about[:feature:about] --> :core:designsystem[:core:designsystem]
:feature:about[:feature:about] --> :core:ui[:core:ui]
:feature:about[:feature:about] --> :core:model[:core:model]
:feature:about[:feature:about] --> :core:testing[:core:testing]
:feature:achievements[:feature:achievements] --> :core:designsystem[:core:designsystem]
:feature:achievements[:feature:achievements] --> :core:ui[:core:ui]
:feature:achievements[:feature:achievements] --> :core:model[:core:model]
:feature:achievements[:feature:achievements] --> :core:testing[:core:testing]
:feature:contributors[:feature:contributors] --> :core:model[:core:model]
:feature:contributors[:feature:contributors] --> :core:ui[:core:ui]
:feature:contributors[:feature:contributors] --> :core:designsystem[:core:designsystem]
:feature:floor-map[:feature:floor-map] --> :core:designsystem[:core:designsystem]
:feature:floor-map[:feature:floor-map] --> :core:ui[:core:ui]
:feature:floor-map[:feature:floor-map] --> :core:model[:core:model]
:feature:floor-map[:feature:floor-map] --> :core:testing[:core:testing]
:feature:main[:feature:main] --> :core:designsystem[:core:designsystem]
:feature:main[:feature:main] --> :core:ui[:core:ui]
:feature:main[:feature:main] --> :core:model[:core:model]
:feature:main[:feature:main] --> :core:testing[:core:testing]
:feature:sessions[:feature:sessions] --> :core:designsystem[:core:designsystem]
:feature:sessions[:feature:sessions] --> :core:ui[:core:ui]
:feature:sessions[:feature:sessions] --> :core:model[:core:model]
:feature:sessions[:feature:sessions] --> :core:testing[:core:testing]
:feature:sponsors[:feature:sponsors] --> :core:designsystem[:core:designsystem]
:feature:sponsors[:feature:sponsors] --> :core:ui[:core:ui]
:feature:sponsors[:feature:sponsors] --> :core:model[:core:model]
:feature:sponsors[:feature:sponsors] --> :core:testing[:core:testing]
:feature:staff[:feature:staff] --> :core:designsystem[:core:designsystem]
:feature:staff[:feature:staff] --> :core:ui[:core:ui]
:feature:staff[:feature:staff] --> :core:model[:core:model]
:feature:staff[:feature:staff] --> :core:testing[:core:testing]
2. DroidKaigi 2024
出力結果
graph TB
:app-android[:app-android] --> :core:testing-manifest[:core:testing-manifest]
:app-android[:app-android] --> :feature:main[:feature:main]
:app-android[:app-android] --> :feature:contributors[:feature:contributors]
:app-android[:app-android] --> :feature:sessions[:feature:sessions]
:app-android[:app-android] --> :feature:eventmap[:feature:eventmap]
:app-android[:app-android] --> :feature:profilecard[:feature:profilecard]
:app-android[:app-android] --> :feature:about[:feature:about]
:app-android[:app-android] --> :feature:sponsors[:feature:sponsors]
:app-android[:app-android] --> :feature:staff[:feature:staff]
:app-android[:app-android] --> :feature:settings[:feature:settings]
:app-android[:app-android] --> :feature:favorites[:feature:favorites]
:app-android[:app-android] --> :core:model[:core:model]
:app-android[:app-android] --> :core:data[:core:data]
:app-android[:app-android] --> :core:designsystem[:core:designsystem]
:app-android[:app-android] --> :core:droidkaigiui[:core:droidkaigiui]
:app-android[:app-android] --> :core:testing[:core:testing]
:app-ios-shared[:app-ios-shared] --> :core:model[:core:model]
:app-ios-shared[:app-ios-shared] --> :core:data[:core:data]
:app-ios-shared[:app-ios-shared] --> :core:droidkaigiui[:core:droidkaigiui]
:app-ios-shared[:app-ios-shared] --> :feature:main[:feature:main]
:app-ios-shared[:app-ios-shared] --> :feature:sessions[:feature:sessions]
:app-ios-shared[:app-ios-shared] --> :feature:eventmap[:feature:eventmap]
:app-ios-shared[:app-ios-shared] --> :feature:sponsors[:feature:sponsors]
:app-ios-shared[:app-ios-shared] --> :feature:settings[:feature:settings]
:app-ios-shared[:app-ios-shared] --> :feature:contributors[:feature:contributors]
:app-ios-shared[:app-ios-shared] --> :feature:profilecard[:feature:profilecard]
:app-ios-shared[:app-ios-shared] --> :feature:about[:feature:about]
:app-ios-shared[:app-ios-shared] --> :feature:staff[:feature:staff]
:app-ios-shared[:app-ios-shared] --> :feature:favorites[:feature:favorites]
:core:data[:core:data] --> :core:model[:core:model]
:core:data[:core:data] --> :core:common[:core:common]
:core:data[:core:data] --> :core:model[:core:model]
:core:designsystem[:core:designsystem] --> :core:testing[:core:testing]
:core:droidkaigiui[:core:droidkaigiui] --> :core:common[:core:common]
:core:droidkaigiui[:core:droidkaigiui] --> :core:model[:core:model]
:core:droidkaigiui[:core:droidkaigiui] --> :core:designsystem[:core:designsystem]
:core:droidkaigiui[:core:droidkaigiui] --> :core:data[:core:data]
:core:testing[:core:testing] --> :core:testing-manifest[:core:testing-manifest]
:core:testing[:core:testing] --> :core:model[:core:model]
:core:testing[:core:testing] --> :core:designsystem[:core:designsystem]
:core:testing[:core:testing] --> :core:data[:core:data]
:core:testing[:core:testing] --> :core:droidkaigiui[:core:droidkaigiui]
:core:testing[:core:testing] --> :feature:main[:feature:main]
:core:testing[:core:testing] --> :feature:sessions[:feature:sessions]
:core:testing[:core:testing] --> :feature:profilecard[:feature:profilecard]
:core:testing[:core:testing] --> :feature:about[:feature:about]
:core:testing[:core:testing] --> :feature:staff[:feature:staff]
:core:testing[:core:testing] --> :feature:sponsors[:feature:sponsors]
:core:testing[:core:testing] --> :feature:settings[:feature:settings]
:core:testing[:core:testing] --> :feature:favorites[:feature:favorites]
:core:testing[:core:testing] --> :feature:eventmap[:feature:eventmap]
:core:testing[:core:testing] --> :feature:contributors[:feature:contributors]
:core:testing-manifest[:core:testing-manifest] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:about[:feature:about] --> :core:testing[:core:testing]
:feature:about[:feature:about] --> :core:testing[:core:testing]
:feature:about[:feature:about] --> :core:designsystem[:core:designsystem]
:feature:about[:feature:about] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:about[:feature:about] --> :core:model[:core:model]
:feature:contributors[:feature:contributors] --> :core:testing[:core:testing]
:feature:contributors[:feature:contributors] --> :core:testing[:core:testing]
:feature:contributors[:feature:contributors] --> :core:model[:core:model]
:feature:contributors[:feature:contributors] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:contributors[:feature:contributors] --> :core:designsystem[:core:designsystem]
:feature:eventmap[:feature:eventmap] --> :core:testing[:core:testing]
:feature:eventmap[:feature:eventmap] --> :core:testing[:core:testing]
:feature:eventmap[:feature:eventmap] --> :core:model[:core:model]
:feature:eventmap[:feature:eventmap] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:eventmap[:feature:eventmap] --> :core:designsystem[:core:designsystem]
:feature:favorites[:feature:favorites] --> :core:testing[:core:testing]
:feature:favorites[:feature:favorites] --> :core:testing[:core:testing]
:feature:favorites[:feature:favorites] --> :core:model[:core:model]
:feature:favorites[:feature:favorites] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:favorites[:feature:favorites] --> :core:designsystem[:core:designsystem]
:feature:main[:feature:main] --> :core:testing[:core:testing]
:feature:main[:feature:main] --> :core:model[:core:model]
:feature:main[:feature:main] --> :core:designsystem[:core:designsystem]
:feature:main[:feature:main] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:profilecard[:feature:profilecard] --> :core:testing[:core:testing]
:feature:profilecard[:feature:profilecard] --> :core:testing[:core:testing]
:feature:profilecard[:feature:profilecard] --> :core:designsystem[:core:designsystem]
:feature:profilecard[:feature:profilecard] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:profilecard[:feature:profilecard] --> :core:model[:core:model]
:feature:profilecard[:feature:profilecard] --> :core:model[:core:model]
:feature:sessions[:feature:sessions] --> :core:testing[:core:testing]
:feature:sessions[:feature:sessions] --> :core:model[:core:model]
:feature:sessions[:feature:sessions] --> :core:testing[:core:testing]
:feature:sessions[:feature:sessions] --> :core:designsystem[:core:designsystem]
:feature:sessions[:feature:sessions] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:sessions[:feature:sessions] --> :core:model[:core:model]
:feature:settings[:feature:settings] --> :core:testing[:core:testing]
:feature:settings[:feature:settings] --> :core:testing[:core:testing]
:feature:settings[:feature:settings] --> :core:model[:core:model]
:feature:settings[:feature:settings] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:settings[:feature:settings] --> :core:designsystem[:core:designsystem]
:feature:sponsors[:feature:sponsors] --> :core:testing[:core:testing]
:feature:sponsors[:feature:sponsors] --> :core:testing[:core:testing]
:feature:sponsors[:feature:sponsors] --> :core:model[:core:model]
:feature:sponsors[:feature:sponsors] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:sponsors[:feature:sponsors] --> :core:designsystem[:core:designsystem]
:feature:staff[:feature:staff] --> :core:testing[:core:testing]
:feature:staff[:feature:staff] --> :core:testing[:core:testing]
:feature:staff[:feature:staff] --> :core:model[:core:model]
:feature:staff[:feature:staff] --> :core:droidkaigiui[:core:droidkaigiui]
:feature:staff[:feature:staff] --> :core:designsystem[:core:designsystem]
3. Now in Android
出力結果
graph TB
:app[:app] --> :core:testing[:core:testing]
:app[:app] --> :core:data-test[:core:data-test]
:app[:app] --> :core:datastore-test[:core:datastore-test]
:app[:app] --> :ui-test-hilt-manifest[:ui-test-hilt-manifest]
:app[:app] --> :feature:interests[:feature:interests]
:app[:app] --> :feature:foryou[:feature:foryou]
:app[:app] --> :feature:bookmarks[:feature:bookmarks]
:app[:app] --> :feature:topic[:feature:topic]
:app[:app] --> :feature:search[:feature:search]
:app[:app] --> :feature:settings[:feature:settings]
:app[:app] --> :core:common[:core:common]
:app[:app] --> :core:ui[:core:ui]
:app[:app] --> :core:designsystem[:core:designsystem]
:app[:app] --> :core:data[:core:data]
:app[:app] --> :core:model[:core:model]
:app[:app] --> :core:analytics[:core:analytics]
:app[:app] --> :sync:work[:sync:work]
:app[:app] --> :core:screenshot-testing[:core:screenshot-testing]
:app[:app] --> :core:data-test[:core:data-test]
:app[:app] --> :core:datastore-test[:core:datastore-test]
:app[:app] --> :sync:sync-test[:sync:sync-test]
:app-nia-catalog[:app-nia-catalog] --> :core:designsystem[:core:designsystem]
:app-nia-catalog[:app-nia-catalog] --> :core:ui[:core:ui]
:core:data[:core:data] --> :core:common[:core:common]
:core:data[:core:data] --> :core:database[:core:database]
:core:data[:core:data] --> :core:datastore[:core:datastore]
:core:data[:core:data] --> :core:network[:core:network]
:core:data[:core:data] --> :core:analytics[:core:analytics]
:core:data[:core:data] --> :core:notifications[:core:notifications]
:core:data[:core:data] --> :core:datastore-test[:core:datastore-test]
:core:data[:core:data] --> :core:testing[:core:testing]
:core:data-test[:core:data-test] --> :core:data[:core:data]
:core:database[:core:database] --> :core:model[:core:model]
:core:datastore[:core:datastore] --> :core:datastore-proto[:core:datastore-proto]
:core:datastore[:core:datastore] --> :core:model[:core:model]
:core:datastore[:core:datastore] --> :core:common[:core:common]
:core:datastore[:core:datastore] --> :core:datastore-test[:core:datastore-test]
:core:datastore-test[:core:datastore-test] --> :core:common[:core:common]
:core:datastore-test[:core:datastore-test] --> :core:datastore[:core:datastore]
:core:designsystem[:core:designsystem] --> :core:screenshot-testing[:core:screenshot-testing]
:core:domain[:core:domain] --> :core:data[:core:data]
:core:domain[:core:domain] --> :core:model[:core:model]
:core:domain[:core:domain] --> :core:testing[:core:testing]
:core:network[:core:network] --> :core:common[:core:common]
:core:network[:core:network] --> :core:model[:core:model]
:core:notifications[:core:notifications] --> :core:model[:core:model]
:core:notifications[:core:notifications] --> :core:common[:core:common]
:core:screenshot-testing[:core:screenshot-testing] --> :core:designsystem[:core:designsystem]
:core:testing[:core:testing] --> :core:analytics[:core:analytics]
:core:testing[:core:testing] --> :core:common[:core:common]
:core:testing[:core:testing] --> :core:data[:core:data]
:core:testing[:core:testing] --> :core:model[:core:model]
:core:testing[:core:testing] --> :core:notifications[:core:notifications]
:core:ui[:core:ui] --> :core:testing[:core:testing]
:core:ui[:core:ui] --> :core:analytics[:core:analytics]
:core:ui[:core:ui] --> :core:designsystem[:core:designsystem]
:core:ui[:core:ui] --> :core:model[:core:model]
:feature:bookmarks[:feature:bookmarks] --> :core:testing[:core:testing]
:feature:bookmarks[:feature:bookmarks] --> :core:ui[:core:ui]
:feature:bookmarks[:feature:bookmarks] --> :core:designsystem[:core:designsystem]
:feature:bookmarks[:feature:bookmarks] --> :core:data[:core:data]
:feature:bookmarks[:feature:bookmarks] --> :core:testing[:core:testing]
:feature:foryou[:feature:foryou] --> :core:testing[:core:testing]
:feature:foryou[:feature:foryou] --> :core:ui[:core:ui]
:feature:foryou[:feature:foryou] --> :core:designsystem[:core:designsystem]
:feature:foryou[:feature:foryou] --> :core:data[:core:data]
:feature:foryou[:feature:foryou] --> :core:domain[:core:domain]
:feature:foryou[:feature:foryou] --> :core:notifications[:core:notifications]
:feature:foryou[:feature:foryou] --> :core:screenshot-testing[:core:screenshot-testing]
:feature:foryou[:feature:foryou] --> :core:testing[:core:testing]
:feature:interests[:feature:interests] --> :core:testing[:core:testing]
:feature:interests[:feature:interests] --> :core:ui[:core:ui]
:feature:interests[:feature:interests] --> :core:designsystem[:core:designsystem]
:feature:interests[:feature:interests] --> :core:data[:core:data]
:feature:interests[:feature:interests] --> :core:domain[:core:domain]
:feature:interests[:feature:interests] --> :core:testing[:core:testing]
:feature:search[:feature:search] --> :core:testing[:core:testing]
:feature:search[:feature:search] --> :core:ui[:core:ui]
:feature:search[:feature:search] --> :core:designsystem[:core:designsystem]
:feature:search[:feature:search] --> :core:data[:core:data]
:feature:search[:feature:search] --> :core:domain[:core:domain]
:feature:search[:feature:search] --> :core:testing[:core:testing]
:feature:settings[:feature:settings] --> :core:ui[:core:ui]
:feature:settings[:feature:settings] --> :core:designsystem[:core:designsystem]
:feature:settings[:feature:settings] --> :core:data[:core:data]
:feature:settings[:feature:settings] --> :core:testing[:core:testing]
:feature:topic[:feature:topic] --> :core:testing[:core:testing]
:feature:topic[:feature:topic] --> :core:ui[:core:ui]
:feature:topic[:feature:topic] --> :core:designsystem[:core:designsystem]
:feature:topic[:feature:topic] --> :core:data[:core:data]
:feature:topic[:feature:topic] --> :core:testing[:core:testing]
:sync:sync-test[:sync:sync-test] --> :core:data[:core:data]
:sync:sync-test[:sync:sync-test] --> :sync:work[:sync:work]
:sync:work[:sync:work] --> :core:testing[:core:testing]
:sync:work[:sync:work] --> :core:analytics[:core:analytics]
:sync:work[:sync:work] --> :core:data[:core:data]
:sync:work[:sync:work] --> :core:notifications[:core:notifications]
Discussion