コンテンツにスキップ

Spring Bootに頼り切ったKotlinエンジニアが、AndroidのGradle設定でつまずいた話

Spring BootでKotlinを書いてきたので、Android開発でも言語の部分ではある程度知識を使えるだろうと思っていました。

ところが、「別のモジュールにあるクラスを使いたい」「ライブラリを追加したい」という場面で、Gradleのどこに何を書けばよいか分からなくなってしまいました。

特に戸惑ったのが、クラスをimportしようとしても、そもそも認識してもらえないことでした。「importすれば使えるはず」と思っていたのに、そのimport先自体を解決できません。

SpringのBeanを使った開発に慣れていても、クラスを参照できるようにするためのビルド設定まで、十分に理解できていたわけではなかったのだと思います。

この記事では、Spring BootからAndroid開発へ移るときに迷いやすい、Gradleのモジュール依存とライブラリの追加方法を整理します。コードは仕組みを説明するためのサンプルです。各Androidモジュールのプラグインやandroid {}など、既に設定されている部分は省略しています。

importしようとしても、そもそも認識されないとき

Section titled “importしようとしても、そもそも認識されないとき”

まずは、どんな場面で困るのかを、説明用の小さな例に置き換えてみます。プロフィール画面を作る:feature:profileモジュールから、共通UI部品を持つ:core:uiモジュールのクラスを使いたいとします。

たとえば、共通UIモジュールに次のクラスがあるとします。

core/ui/src/main/kotlin/com/example/core/ui/UiLabel.kt
package com.example.core.ui
class UiLabel(val text: String)

プロフィールのモジュールから使いたくて、次のimportを書こうとします。

import com.example.core.ui.UiLabel

:feature:profileから:core:uiへの依存が設定されていないと、このimport先を解決できません。IDEで補完候補に出てこなかったり、手動で書いてもUnresolved referenceと表示されたりする、という状況です。

同じリポジトリにファイルが存在することと、そのモジュールのコンパイル時に参照できることは別です。importは、参照できる宣言をそのファイル内で使うための記述です。別モジュールやライブラリをビルドへ追加する操作はGradle側で行います。Kotlin:Packages and imports

この例では、プロフィール側のモジュールに共通UIへの依存を追加する必要があります。その書き方を見る前に、Springに任せていた処理と、Gradleで設定する処理を整理します。

SpringのBeanと、Gradleの依存関係を整理する

Section titled “SpringのBeanと、Gradleの依存関係を整理する”

Spring Bootでコンストラクタに必要なクラスを書き、Springにそのインスタンスを渡してもらう。こうした書き方に慣れると、「必要なものを使えるようにする」という話を、ひとまとめに考えやすくなります。

Springがインスタンスを用意できるのも、必要なクラスが実行時に利用できることが前提です。ソースコードからその型を参照するには、コンパイル時にも利用できなければなりません。

仕組み何を設定するか例
Gradleの依存関係そのモジュールのビルドで、どのライブラリや別モジュールを使うかimplementation(project(":core:ui"))
SpringのBean・DI必要なオブジェクトをどう生成し、他のオブジェクトへ渡すか@Serviceや@Bean、コンストラクタ注入
Kotlinのimportソースコードで型や関数をどの名前で参照するかimport com.example.core.ui.UiLabel

たとえば、importを書いても、Gradleがそのクラスを含むモジュールを参照していなければ利用できません。逆に、Gradleでライブラリを追加しても、コンストラクタに渡すインスタンスが自動的に生成されるわけではありません。

Spring Bootの公式ドキュメントでも、Beanの検出やコンストラクタ注入はSpringが担う仕組みとして説明されています。Spring Beans and Dependency Injection

Spring Bootの例で、クラスの参照とBeanの注入を見比べる

Section titled “Spring Bootの例で、クラスの参照とBeanの注入を見比べる”

違いを具体的にするため、まずは一つのGradleモジュール内にSpring Bootのアプリを作る場合を考えます。次のクラスが同じモジュールにあり、Springのコンポーネントスキャンの対象になっているとします。

Spring Bootでの例
package com.example.profile
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service
@Repository
class ProfileRepository {
fun findName(): String = "Kotlinエンジニア"
}
@Service
class ProfileService(
private val repository: ProfileRepository,
) {
fun displayName(): String = repository.findName()
}

この例では、ProfileRepositoryとProfileServiceは同じモジュールのソースコードなので、クラスを一つ増やすたびにモジュール間の依存を宣言する必要はありません。別パッケージに分けても、同じモジュール内で参照可能なら、必要なimportを追加して利用できます。

そのうえで、Springがスキャン対象のクラスをBeanとして管理し、ProfileServiceを生成するときにProfileRepositoryをコンストラクタへ渡します。アプリケーションクラスを上位のパッケージに置く一般的な構成では、@SpringBootApplicationに含まれるコンポーネントスキャンが配下のクラスを検出します。Spring Boot:Structuring Your Code

Androidで機能ごとにGradleモジュールを分けると、別モジュールのクラスを使うための依存宣言も確認する場面が増えます。インスタンスの生成・注入には、手動で渡す方法や、そのプロジェクトで採用しているDIの仕組みを使います。

モジュールをまたぐ参照にGradleの設定が必要なのは、Spring Bootでも同じです。比較するときは、SpringのBean管理と、プロジェクトのモジュール構成を分けて考える必要があります。

たとえば、Spring Bootで「型は参照できるが、注入したいBeanが見つからない」場合は、Beanの登録やスキャン範囲などを確認します。一方、import先の型を認識できない場合は、Gradleの依存宣言や、型の名前・可視性などを先に確認します。どの段階でつまずいているかによって、調べる場所が変わります。

まず、設定ファイルの役割を見分ける

Section titled “まず、設定ファイルの役割を見分ける”

Androidのマルチモジュール構成では、複数の場所にGradle関連のファイルがあります。最初に把握したいのは、次の役割です。

ファイル主な役割
settings.gradle.ktsビルドに含めるモジュールや、依存解決先などを設定する
ルートのbuild.gradle.kts共通のビルド設定やプラグイン宣言などを置く。内容はプロジェクトごとに異なる
各モジュールのbuild.gradle.ktsそのモジュールに適用するプラグインや、利用する依存関係を設定する
gradle/libs.versions.tomlライブラリ・プラグインの名前とバージョンをVersion Catalogとして管理する

依存先を追加する際に特に大切なのは、そのコードを直接書いているモジュールのbuild.gradle.ktsを確認することです。ルートやappに書けば、すべてのモジュールから使えるという仕組みではありません。

ここからは、次の構成を例にします。

project/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/
│ └── libs.versions.toml
├── app/
│ └── build.gradle.kts
├── feature/
│ └── profile/
│ └── build.gradle.kts
└── core/
└── ui/
└── build.gradle.kts

:appがアプリ本体、:feature:profileがプロフィール画面、:core:uiが共通UI部品を持つモジュールです。プロフィール画面から共通UI部品を使えるようにしてみます。

別のモジュールを使うには、登録と依存宣言が必要

Section titled “別のモジュールを使うには、登録と依存宣言が必要”

settings.gradle.ktsでモジュールを登録する

Section titled “settings.gradle.ktsでモジュールを登録する”

まず、ビルドに含めるモジュールをsettings.gradle.ktsに登録します。

settings.gradle.kts
include(":app", ":feature:profile", ":core:ui")

標準的な対応では、:feature:profileはfeature/profile/、:core:uiはcore/ui/を指します。プロジェクトによってはprojectDirを設定したり、独自の処理でモジュールを自動登録したりする場合もあります。既存の登録方法を確認してから追加します。

ここで行っているのは、「このビルドにはこれらのモジュールがある」とGradleに伝えることです。includeしただけでは、すべてのモジュール同士が自由に参照できる状態にはなりません。Gradle:Multi-Project Builds

利用する側のbuild.gradle.ktsに書く

Section titled “利用する側のbuild.gradle.ktsに書く”

プロフィール画面のコードから共通UI部品を使うので、変更するのは:feature:profile側です。

feature/profile/build.gradle.kts
dependencies {
implementation(project(":core:ui"))
}

この宣言を追加し、Gradle Syncで設定を読み込むと、:feature:profileから:core:uiの公開されたクラスや関数を参照できるようになります。冒頭の例では、UiLabelのimport先が解決できるか、プロフィールのモジュールがビルドできるかを確認します。

Kotlinの可視性指定も関係するため、依存を追加すればprivateやinternalの宣言まで自由に使えるわけではありません。

さらに、:appからプロフィール機能を利用するなら、アプリ側にも依存を追加します。

app/build.gradle.kts
dependencies {
implementation(project(":feature:profile"))
}

参照の向きは、次のようになります。

:app → :feature:profile → :core:ui

:appに:core:uiの依存を追加しても、それだけで:feature:profileから使えるようにはなりません。依存は宣言した向きに働きます。プロフィール画面が共通UI部品を直接使うなら、プロフィールのモジュール自身にその依存を宣言します。

この「利用する側に書く」という判断ができると、複数あるbuild.gradle.ktsのうち、どれを開くべきかが分かりやすくなります。Gradle:Declaring dependencies

projectsでモジュールを参照する書き方もある

Section titled “projectsでモジュールを参照する書き方もある”

既存のコードを読むと、次のような記述が出てくることもあります。

dependencies {
implementation(projects.core.ui)
}

これは、Gradleの型安全なプロジェクトアクセサーを使った書き方です。上の構成なら、project(":core:ui")に対応します。たとえばモジュール名が:core-uiの場合は、projects.coreUiという名前になります。

この仕組みを使う構成では、settings.gradle.ktsで次の機能を有効にします。

enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

projectsで始まる記述を見たら、どのモジュールパスから生成された名前かを確認すると読み解けます。機能の扱いは使用するGradleのバージョンに依存するため、既存プロジェクトの書き方に合わせます。Gradle:Type-safe project dependencies

外部ライブラリを追加するときは、座標と利用箇所を確認する

Section titled “外部ライブラリを追加するときは、座標と利用箇所を確認する”

プロジェクト内のモジュールではなく、外部のライブラリを使う場合は、取得するライブラリを指定します。

feature/profile/build.gradle.kts
dependencies {
implementation("androidx.core:core:1.19.1")
}

文字列はグループ名:ライブラリ名:バージョンという形です。この例は、AndroidX Coreの依存をプロフィールのモジュールへ追加しています。AndroidX Coreの公式リリース情報

バージョン番号は記法を説明するための例です。実際に追加するときは、既存プロジェクトが採用しているバージョンや、AGP・compileSdkなどとの互換性を確認します。

取得先のリポジトリも必要です。settings.gradle.ktsで依存解決先をまとめている構成なら、たとえば次のようになっています。

settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}

既に設定されていれば、同じブロックを追加する必要はありません。pluginManagementにもrepositoriesが出てきますが、こちらはプラグインの取得先です。ライブラリを取得するための設定とは役割が異なります。

libs.versions.tomlに書くだけでは、依存は追加されない

Section titled “libs.versions.tomlに書くだけでは、依存は追加されない”

Androidの既存プロジェクトでは、バージョン付きの文字列より、次のような記述を見かけることがあります。

dependencies {
implementation(libs.androidx.core)
}

libs.androidx.coreは、標準的な構成ではgradle/libs.versions.tomlに定義したライブラリの別名です。先ほどの依存をVersion Catalogで管理するなら、たとえば次のように定義します。

gradle/libs.versions.toml
[versions]
androidx-core = "1.19.1"
[libraries]
androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }

[versions]がバージョン、[libraries]がライブラリの定義です。version.refは、[versions]にあるどの値を使うかを示しています。androidx-coreという別名に対応して、ビルドスクリプトではlibs.androidx.coreを参照できます。

既存のファイルに同じ名前や同じライブラリの定義があれば、その定義を利用します。上の例をファイル末尾へそのまま追記して、[versions]などのセクションを重複させないようにします。

そして、利用するモジュールのbuild.gradle.ktsに書きます。

feature/profile/build.gradle.kts
dependencies {
implementation(libs.androidx.core)
}

直接指定する方法とVersion Catalogを使う方法は、同じ依存を宣言する別々の書き方なので、両方を重ねて追加する必要はありません。

ここでも、カタログへの定義と、モジュールへの依存追加は別の操作です。libs.versions.tomlへ名前を登録しただけでは、そのライブラリを使うモジュールは決まりません。Android Developers:Version Catalogへの移行

ここまでの書き方を並べると、それぞれ何を参照しているかが見えてきます。

記述参照しているもの
project(":core:ui")同じビルド内のモジュール
projects.core.uiモジュールを表す、生成されたプロジェクトアクセサー
"androidx.core:core:1.19.1"外部ライブラリの座標
libs.androidx.coreVersion Catalogに定義したライブラリの別名

プラグインの追加と、ライブラリの追加も分けて考える

Section titled “プラグインの追加と、ライブラリの追加も分けて考える”

Version Catalogには、[libraries]だけでなく[plugins]が置かれていることもあります。

たとえば、Androidライブラリ用プラグインの別名が既に定義されている場合、モジュール側では次のように適用します。

core/ui/build.gradle.kts
plugins {
alias(libs.plugins.android.library)
}

plugins {}は、そのモジュールをどうビルドするかという機能を追加する場所です。dependencies {}は、そこで利用するライブラリやモジュールを宣言する場所です。

ルートのbuild.gradle.ktsに、同じプラグインが次のように宣言されていることもあります。

build.gradle.kts
plugins {
alias(libs.plugins.android.library) apply false
}

apply falseの宣言は、ルートにそのプラグインを適用するものではありません。各モジュールに自動適用する指定でもないため、利用するモジュール側の設定を確認します。Android Developers:プラグインのVersion Catalogへの移行

プロジェクトによっては、独自のconvention pluginに共通設定をまとめています。新しい機能モジュールを追加するときは、役割が近い既存のライブラリモジュールを参照すると、適用すべきプラグインも把握しやすくなります。

implementationとapiで見える範囲が変わる

Section titled “implementationとapiで見える範囲が変わる”

モジュールをいくつかつなぐと、「依存先が使っているクラスを、さらにその利用側からも使えるのか」が気になります。

たとえば、:feature:profileが:core:uiをimplementationで参照していても、:appのコンパイル時に:core:uiの型がそのまま公開されるわけではありません。

:app自身も:core:uiのクラスを直接使うなら、:appに直接依存を宣言する方法があります。一方、ライブラリの公開APIとして依存先の型を利用側へ公開する設計なら、ライブラリ側でapiを検討します。

feature/profile/build.gradle.kts
dependencies {
// 公開する関数の引数や戻り値などに、core:uiの型が現れる場合に検討する
api(project(":core:ui"))
}

これは先ほどのimplementationを置き換える場合の例です。参照エラーが出るたびにapiへ変更すると、依存を公開する範囲が広がってしまいます。内部の実装だけで使うならimplementation、公開APIの一部として必要ならapi、と目的を確認します。Gradle:APIと実装の分離

分からなくなったときの確認順序

Section titled “分からなくなったときの確認順序”

設定を変更したらGradle Syncを行い、その後に対象モジュールのビルド結果を確認します。Syncで構成を読み込めても、ソースコードのコンパイルが成功するとは限らないので、両方を確認します。

参照エラーはGradleだけが原因とは限りません。確認する場所を絞るため、次のように切り分けます。

症状最初に確認すること
import候補に出ない・import文の参照先を解決できない利用側のモジュール依存、クラス名・パッケージ・可視性を確認する。src/test/のクラスをsrc/main/から使うなど、ソースセットの違いにも注意する
Project with path ... could not be foundsettings.gradle.ktsの登録、モジュールパス、ディレクトリとの対応
Could not find ...など、ライブラリの解決に失敗するライブラリの座標とバージョン、取得先、ネットワークや認証。エラーの詳細も確認する
libs以下の名前を解決できないカタログの別名、TOMLの記述、カタログの読み込み、Sync結果
implementationを解決できない記述場所と、その設定を提供するプラグインがモジュールに適用されているか

コマンドラインではビルドできるのにIDE上だけ認識されない場合は、Gradleの同期状態や、IDEがコードの検索・補完用データを作るインデックス処理の状況も確認します。

今後、同じ場所で迷ったときは、次の順序で追っていこうと思います。

  1. 参照したいクラスが、どのモジュールやライブラリにあるか調べる。
  2. そのクラスを使うコードが、どのモジュールにあるか調べる。
  3. プロジェクト内のモジュールなら、登録と依存の向きを確認する。
  4. 外部ライブラリなら、カタログの定義と利用側の依存宣言を確認する。
  5. Gradle Syncとビルドの結果を見て、残ったエラーを切り分ける。

Spring Bootで書いてきたKotlinの知識は、Androidでも役に立ちます。ただ、「Kotlinを書けること」と「そのコードを動かすためのビルド構成を理解していること」は、それぞれ学ぶ必要があるのだと感じました。

import先を認識してもらえず困ったときも、使いたいクラスがどこにあり、利用側からどう参照するのかを追うと、確認すべき設定が見えてきます。次に同じ場所で迷ったときは、まずそのクラスが属するモジュールと、利用する側のモジュールを確かめるところから始めたいと思います。