🍡

Gradle APIとMermaidを使ってmoduleの依存関係を図示する

2024/12/04に公開

はじめに

マルチモジュールのAndroidアプリにおいて、Gradle APIとMermaidを使って図1のようなモジュールの依存関係を図示する方法を紹介します。


図1 公式doc「What is modularization?」より引用

注意事項

  1. 本記事で使用するGradle APIは非推奨となっており、Gradle 9で削除されることがアナウンスされています。(詳しくは後述)
  2. 軽いお遊びのような記事なので正確性に欠けている可能性があります。

実装

基本的に Project.javaProjectDependency.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)
}

簡単に処理の流れを説明すると、

  1. モジュールの依存関係を洗い出す
  2. Mermaid形式のテキストに変換する
  3. テキストを書き出したファイルを保存する

になります。

Mermaidを使うとflowchartを書くことができます。このMermaidの形式に合わせて依存関係をテキストに出力します。Mermaidに関しては公式docのflowchartを参考にしてください。

実際に使ってみる

有名どこのアプリで上記のGradle Taskを実行しdependencyGraph.txtの中身をMermaidの Live Editor にコピペして完成した図を見てみます。

1. DroidKaigi 2023

https://github.com/DroidKaigi/conference-app-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

https://github.com/DroidKaigi/conference-app-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

https://github.com/android/nowinandroid

出力結果
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