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 строк). Придется сжать его, указав необходимую конфигурацию. В большинстве случаев можно использовать compileClasspath
, runtimeClasspath
, 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"}
Таким образом я решила проблему с зависимостями. Очевидно, что переопределение транзитивных версий зависимостей не стоит делать часто (это может привести к неожиданным ошибкам сборки или выполнения), но при необходимости этот способ можно попробовать.
Читайте также:
- Как использовать управляемые Gradle устройства с собственными девайсами
- Динамическое извлечение видеокадров в Android
- Секреты в Android. Часть 1
Читайте нас в Telegram, VK и Дзен
Перевод статьи Katie Barnett: Debugging dependencies in Gradle