TL;DR: эта статья начинается с истории — если у вас мало времени, переходите сразу к следующим разделам, чтобы получить инструкции по использованию dependencyInsight.

Недавно мне понадобилось обновить зависимость до бета-версии (androidx.navigation:navigation-compose, версия 2.8.0-beta02) в Android-приложении. Как это обычно бывает, для данной версии зависимости требовались другие (транзитивные) зависимости Jetpack Compose, некоторые из которых были определены в их альфа- или бета-версиях. Чаще всего это нормальная ситуация: мы допускаем, что альфа- или бета-версия зависимости могут иметь проблемы, обнаруживая которые, мы дожидаемся стабильных версий и при необходимости поднимаем вопрос об ошибке.

Но я не могла ждать: мне надо было срочно внедрить в приложение несколько функций из новой библиотеки навигации (Type-Safe Navigation). Проблема заключалась в том, что новые альфа-версии транзитивных зависимостей вызвали сбой, который я не хотела допустить в своем приложении (в данном случае это была ошибка NoSuchMethodError при работе с HorizontalPager).

Я обнаружила, что конструктор HorizontalPager изменил сигнатуру в версии 1.7.0-beta02 зависимости androidx.compose.foundation (HorizontalPager помечен как ExperimentalAPI, так что это не удивительно), которая была включена в качестве транзитивной зависимости в androidx.navigation:navigation. Библиотека навигации указывала версию 1.7.0-beta02, но я знала, что в предыдущей стабильной версии, 1.6.7, моя реализация HorizontalPager работала.

Так что же делать с этим? Ждать, пока androidx.navigation:navigation или androidx.compose.foundation:foundation станут стабильными, а затем интегрировать их в приложение (несмотря на новую функцию, которую мне нужно было добавить), попробовать какой-нибудь хак с новой зависимостью или убедиться, что используется стабильная версия зависимости?

На тот момент я даже не знала, какая зависимость вызывала обновление версии транзитивной зависимости. Я обновила несколько других зависимостей в той же части, так что это могло быть что угодно.

Просмотр дерева зависимостей Gradle

Первое, что нужно сделать в такой ситуации — просмотреть все дерево зависимостей. Самый простой способ сделать это — использовать обертку gradle с помощью команды dependencies:

./gradlew :app:dependencies

Я указала модуль. Примечательно, что, исключив модуль, вы не получите полный вывод проекта — только детали верхнего уровня (а это обычно не то, что нужно).

Указав модуль, получите список зависимостей, требуемых этим модулем, включая транзитивные зависимости.

Если хотите пойти дальше, можете добавить --scan к команде dependencies, чтобы создать веб-отчет с возможностью поиска. Это требует верификации вашей почты с помощью gradle. По моим наблюдениям, быстрее будет просто просмотреть текстовую версию. Вообще, мне удобнее выводить результаты команды в файл, чтобы потом выполнить diff (сравнение) результатов до и после изменений (в большом проекте результат может получиться довольно длинным).

./gradlew :app:dependencies > dependencyTree.txt

Понимание вывода дерева зависимостей

Результирующий файл слишком велик (для простого тестового приложения Hello World его длина составит 7435 строк). Придется сжать его, указав необходимую конфигурацию. В большинстве случаев можно использовать compileClasspathruntimeClasspath, testCompileClasspath и testRuntimeClasspath. Мне понадобился runtimeClasspath, и я добавила тип сборки debug:

./gradlew :app:dependencies --configuration debugRuntimeClasspath 
> dependencyTree.txt

После этого длина файла составила всего 692 строки — он стал гораздо проще в использовании!

Теперь можно увидеть все зависимости и то, какие транзитивные зависимости они включают. Например:

+--- androidx.navigation:navigation-compose:2.8.0-beta02
| +--- androidx.activity:activity-compose:1.8.0 -> 1.9.0 (*)
| +--- androidx.compose.animation:animation:1.7.0-beta02 (*)
| +--- androidx.compose.foundation:foundation-layout:1.7.0-beta02 (*)
| +--- androidx.compose.runtime:runtime:1.7.0-beta02 (*)
| +--- androidx.compose.runtime:runtime-saveable:1.7.0-beta02 (*)
| +--- androidx.compose.ui:ui:1.7.0-beta02 (*)
| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -> 2.8.1
| | \--- androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.1
| | +--- androidx.annotation:annotation:1.8.0 (*)
| | +--- androidx.compose.runtime:runtime:1.6.0 -> 1.7.0-beta02 (*)
| | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-beta02 (*)
| | +--- androidx.lifecycle:lifecycle-common:2.8.1 (*)
| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.1 (*)
...

Здесь видно, что androidx.navigation:navigation-compose включает androidx.compose.foundation:foundation-layout:1.7.0-beta02.

В документации Gradle довольно четко объясняется, что означают символы аннотации для каждой из перечисленных зависимостей.

(*): указывает на повторные вхождения поддерева транзитивных зависимостей (Gradle раскрывает поддеревья транзитивных зависимостей только один раз в проекте; при повторных вхождениях отображается только корень поддерева, а затем эта аннотация).

(c): этот элемент является ограничением зависимости, а не зависимостью (надо искать соответствующую зависимость в другом месте дерева).

(n): зависимость или конфигурация зависимости, которая не может быть разрешена.

Но из этого невозможно понять, приводит ли androidx.navigation:navigation-compose:2.8.0-beta02 к тому, что androidx.compose.foundation:foundation-layout использует версию 1.7.0-beta02. К тому же при выполнении большого проекта будет довольно утомительно находить все следы проблематичной библиотеки и проводить сравнения.

Определение зависимости с помощью dependencyInsight

Использование dependencyInsight позволяет получить конкретную информацию о разрешении зависимости.

./gradlew :app:dependencyInsight — configuration debugRuntimeClasspath — dependency androidx.compose.foundation > dependencyInsight.txt

./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath 
--dependency androidx.compose.foundation > dependencyInsight.txt

Здесь снова передается configuration и добавляется необходимая зависимость в качестве аргумента. Результаты отправляются в файл, чтобы потом можно было выполнить сравнение результатов до и после внесения изменений. Существует также веб-версия этого метода с использованием флага --scan.

В начале файла получаем метаданные о зависимости, а также информацию о том, какие версии запрашиваются и какая версия была разрешена:

> Task :app:dependencyInsight
androidx.compose.foundation:foundation:1.7.0-beta02
Variant releaseRuntimeElements-published:
| Attribute Name | Provided | Requested |
|-------------------------------------------------|--------------|---------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.usage | java-runtime | java-runtime |
| org.jetbrains.kotlin.platform.type | androidJvm | androidJvm |
| com.android.build.api.attributes.AgpVersionAttr | | 8.6.0-alpha03 |
| com.android.build.api.attributes.BuildTypeAttr | | debug |
| org.gradle.jvm.environment | | android |
Selection reasons:
- By constraint: foundation-layout is in atomic group androidx.compose.foundation
- By constraint
- By constraint: prevents a critical bug in Text
- By conflict resolution: between versions 1.7.0-beta02, 1.6.7, 1.4.0 and 1.6.0

После этого можно будет увидеть разрешенную версию и список зависимостей, которые ее запрашивали:

androidx.compose.foundation:foundation:1.7.0-beta02
+--- debugRuntimeClasspath
\--- androidx.compose.foundation:foundation-layout-android:1.7.0-beta02
+--- androidx.compose:compose-bom:2024.05.00 (requested androidx.compose.foundation:foundation-layout-android:1.6.7)
| \--- debugRuntimeClasspath
\--- androidx.compose.foundation:foundation-layout:1.7.0-beta02
+--- androidx.compose:compose-bom:2024.05.00 (requested androidx.compose.foundation:foundation-layout:1.6.7) (*)
+--- androidx.navigation:navigation-compose:2.8.0-beta02
...

Кроме того, будет предоставлена информация о причинах выбора зависимости для каждого запроса. Вы сможете найти причину, по которой была выбрана проблемная версия.

Если дерево большое и вы догадываетесь, какая библиотека является причиной, можете выполнить исключение, как описано ниже, а затем повторно выполнить команду и сравнить результаты, чтобы посмотреть, что изменилось.

Принудительное исключение определенной версии зависимости

Теперь, когда вы знаете, какая зависимость или зависимости включают ненужную транзитивную версию, можете исключить ее с помощью exclude. В моем случае:

implementation(libs.compose.navigation) {
exclude(group = "androidx.compose.foundation", module = "foundation")
exclude(group = "androidx.compose.foundation", module = "foundation-android")
exclude(group = "androidx.compose.foundation", module = "foundation-layout-android")
}

Где:

compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "2.8.0-beta02" 

И не забудьте указать желаемую версию:

implementation(libs.compose.foundation)
implementation(libs.compose.foundation.layout)

Где:

compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "1.6.7"}
compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "1.6.7"}

Таким образом я решила проблему с зависимостями. Очевидно, что переопределение транзитивных версий зависимостей не стоит делать часто (это может привести к неожиданным ошибкам сборки или выполнения), но при необходимости этот способ можно попробовать.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Katie Barnett: Debugging dependencies in Gradle

Предыдущая статья7 типичных ошибок в Go-интерфейсах