Как переименовать копию проекта в Android Studio?
11:40:58 Unsupported Modules Detected: Compilation is not supported for following modules: H0001 Practise. Unfortunately you can’t have non-Gradle Java modules and Android-Gradle modules in one project.
11:40:58 Gradle sync started
11:41:02 Gradle sync completed
11:41:02 Executing tasks: [:app:generateDebugSources, :app:generateDebugAndroidTestSources]
11:41:03 Gradle build finished in 1s 579ms
11:41:25 Throwable: Merged rootsChanged not allowed inside rootsChanged, rootsChanged level == 1
Как переименовать копию проекта в Android Studio?
На английском стэке задавали тот же вопрос, последовательность действий была примерно следующая:
1. Создать копию проекта используя файловый менеджер.
2. Refactor->Rename для проекта.
3. Изменить Gradle соответствующим образом.
Мои проблемы:
1. Как и что нужно сделать в Gradle?
2. На этапе переименования через Refactor выводит в EventLog следующее:
—
11:40:58 Unsupported Modules Detected: Compilation is not supported for following modules: H0001 Practise. Unfortunately you can't have non-Gradle Java modules and Android-Gradle modules in one project. 11:40:58 Gradle sync started 11:41:02 Gradle sync completed 11:41:02 Executing tasks: [:app:generateDebugSources, :app:generateDebugAndroidTestSources] 11:41:03 Gradle build finished in 1s 579ms 11:41:25 Throwable: Merged rootsChanged not allowed inside rootsChanged, rootsChanged level == 1
- Вопрос задан более трёх лет назад
- 624 просмотра
Глобальный ренейминг в Android-проекте
Привет-привет! Не будем мять бока и начнем максимально быстро.
Но для начала представлюсь. Меня зовут Таскаев Евгений — я Android-разработчик в фичевой команде hh.ru. Пилю всякие интересные фичи, которыми вы пользуетесь каждый день*.
В статье я расскажу, как у нас в Android-приложении прошел глобальный ренейминг фичей и пакетов и их структуризация. Что у нас получилось, а что нет. А стоит ли вам делать так же, вы решите уже сами.
* — если вы каждый день открываете приложение «hh работа»
Для тех, кто не любит читать
У меня для вас есть выход! Эту статью можно глянуть в формате видео на ютубе в одном из эпизодов «Охэхэнных историй» 😉
Представим, что сейчас 2018 год. Представили? А теперь перестаньте плакать. В 2018 тоже было полно проблем.
У нас есть шикарный проект, в котором, по законам жанра, огромный монолит с 1 app gradle модулем и весь код в активити и несколько флэйворов. Основные из них — applicant и employer — приложения соискателя и работодателя соответственно.
В этом нет ничего страшного ровно до тех пор
- пока команда не слишком разрослась и не стало трудно пилить фичи
- пока старые разработчики не ушли, а новые знают не всё
- пока не пришел хайптрейн с многомодульностью
- пока рак на горе не свистнул
В нашем случае произошло так: были разного рода лики и баги, фиксить которые было сложно, к тому же старые разработчики разбежались, а новым было трудно ориентироваться в том, что осталось. Из-за этого было трудно декомпозировать задачи и разработка стала “дорогой”.
А какие у вас были проблемы на проектах пишите в комментариях)
Структура проекта до ренейминга
Спустя некоторое время, когда многое было зарефачено и отлажено, мы пришли к новому виду структуры проекта. Это как раз та структура, которая была у нас до недавнего времени.
За всё это время в плане структуры произошло следующее:
- мы избавились от flavors и разделили приложения на два app gradle-модуля — модули которые зависят от плагина «com.android.application».
- разбили монолит на отдельные library gradle-модули (и в дальнейшем просто gradle-модули) — модули которые зависят от плагина «com.android.library».
Также прошло несколько этапов формирования структуры. И перед последним, о котором я расскажу в конце, структура была такая:
- Часть фичей лежали в папке feature в корне проекта, туда складывались gradle-модули фичей соискательского приложения с префиксом “feature-” в именовании.
Например, фича резюме называлась feature-resume. Это рудиментарное решение, которое появилось почти в самом начале рефакторинга. - Некоторые фичи состояли из нескольких сабфичей, по аналогии с 1 пунктом, но создавался не gradle-модуль, а папка также с префиксом, куда уже складывались нужные фичи. Слишком сложно? Сейчас приведу пример. Фича резюме, feature-resume, внутри нее лежали gradle-модули: feature-resume-profile,feature-resume-network и тд.
- Общие gradle-модули, которые использовались в обоих приложениях, лежали в папочке shared. Помимо этого, фичи внутри shared делились на core и feature. Соответственно, в какой папке лежал gradle-модуль, то имя папки добавлялось префиксом к названию модуля. Как можете заметить на картинке, с core что-то было не так. По правилам префикс у названия фичей должен быть “shared-core-”.
- Основная логика приложений лежала в папках соответствующих названиям приложений applicant (соискательское) и employer (работодательское).
Структура была аналогична папке shared и фичи в них именовались так:
Проблемы с именованием
- Расположение модулей не мэтчилось с их названием, что затрудняло подключение модулей, а иногда и путало, особенно новых разработчиков
- Бардак в названии пакетов, конечно были какие то правила, но за 3 года, они тоже менялись, а пакеты могли называться по-разному, иногда даже совпадали у некоторых разных фич
И выход из всего этого был — РЕНЕЙМИНГ!
Ретроспектива ренейминга
А теперь самое интересное. Расскажу про новую структуру проекта.
Начнем с небольшой ретроспективы и разберемся, с какими проблемами мы столкнулись.
До начала рефакторинга мы определились какой хотим видеть новую структуру.
Во время праздничных выходных в феврале мы начали переносить файлы по новым папкам и менять имена пакетов с помощью разного рода скриптов.
Время было выбрано специально, чтобы никто параллельно ничего не делал.Но работа затянулась. Тогда мы плохо понимали масштаб возложенной на нас задачи.
И поэтому решили для начала вручную составить гигантский чеклист того, что предстоит сделать, формата «как модуль назывался -> как будет называться». А также за одно переименовать пакеты в соответсвии с названием модулей
Он выглядел примерно так
#### feature-chat --> chat #### feature-chat/core - [x] applicant-feature-chat-core-data — [x] rename module: applicant-feature-chat-core-data -> :applicant:feature:chat:core:data — [x] rename package: ru.hh.applicant.feature.chat.screen_core_data -> ru.hh.applicant.feature.chat.core.data - [x] applicant-feature-chat-core-network — [x] rename module: applicant-feature-chat-core-network -> :applicant:feature:chat:core:network — [x] rename package: ru.hh.feature_chat_network -> ru.hh.applicant.feature.chat.core.network #### feature-chat/feature - [x] applicant-feature-chat-list — [x] rename module: applicant-feature-chat-list -> :applicant:feature:chat:chat-list — [x] rename package: ru.hh.feature_chat_list -> ru.hh.applicant.feature.chat.list и так далее
И даже после этого мы не до конца осознали, что нора, в которую мы залезли — не кроличья.
Поэтому первые модули было решено перенести просто руками, в Android Studio. После 10 модулей стало понятно, что git-история этих файлов исчезла. Терять историю нам однозначно не хотелось, поэтому мы вспомнили про такую команду как git mv, которая позволяет без потери истории перенести файлы из одной папки в другую.
Попытавшись перенести несколько модулей с использованием git mv, мы сильно взгрустнули. Потому что писать руками эти команды было очень долго и муторно.
Нужно было искать какое-то автоматическое решение. Поэтому мы написали простой bash-скрипт, который для двух заданных папок генерил тонну команд git mv, которые можно было скопировать и сразу запустить.
А вот и сам скриптик на генерацию git mv команд
#!/bin/bash readonly LOCAL_PATH=$1 readonly START_FOLDER=$2 readonly START_PACKAGE=$3 readonly TARGET_FOLDER=$4 readonly TARGET_PACKAGE_NAME="$5" echo "===== MV COMMANDS GENERATOR =====" echo "LOCAL_PATH: $" echo "START_FOLDER: $" echo "START_PACKAGE: $" echo "TARGET_FOLDER: $" echo "TARGET_PACKAGE_NAME: $" readonly SPLITTED_TARGET_PACKAGE=($) TARGET_PATH_PARTS="" for package_part in "$"; do TARGET_PATH_PARTS="$/$" done readonly SOURCE_SET_FOLDER="/src/main/java" readonly SOURCE_KOTLIN_SET_FOLDER="/src/main/kotlin" readonly DESIRED_CODE_ROOT_FOLDER="$$$" echo "DESIRED_CODE_ROOT_FOLDER: $" readonly SPLITTED_START_PACKAGE=($) START_PATH_PARTS="" for package_part in "$"; do START_PATH_PARTS="$/$" done START_CODE_ROOT_FOLDER="$$$" if [ ! -d "$" ]; then echo ". there is no /java source set --> /kotlin source set exists" START_CODE_ROOT_FOLDER="$$$" fi readonly FULL_START_PATH="$(cd "$(dirname "$")"; pwd)/$(basename "$")" echo "START_CODE_ROOT_FOLDER: $" echo "FULL_START_PATH: $" echo "" echo "======= Generation result ======" echo "" # Первая команда — создаём директорию для переноса кода echo "mkdir -p $ && \\" # Перечисляем команды для аккуратного переноса кода for code_directory in $/* ; do NAME="$" echo "git mv $ $ && \\" done # Последняя команда — перенос кода в target_folder echo "git mv $ $" echo "" echo "=================================" echo ""
Дело пошло чуть веселее. Но перенос папок — это всего лишь одна часть истории. Вторая часть заключалась в том, что мы, помимо простого переноса папок, захотели ещё и ПЕРЕИМЕНОВАТЬ некоторые модули, о чем я писал выше, добавив структуры не только в иерархию папок, но и в иерархию package-ей.
Чтобы было вот так:
ru.hh.feature_chat_list -> ru.hh.applicant.feature.chat.list ru.hh.feature_chat_network -> ru.hh.applicant.feature.chat.core.network ru.hh.applicant.feature.chat.screen_core_data -> ru.hh.applicant.feature.chat.core.data
Поэтому помимо генерации команд git mv, нужно было ещё сгенерить команды для переименования одних package-ей в другие. Для этого мы тоже написали дополнительный скрипт, который генерил команды для вызова скрипта переименования.
Скрип запуска скрипта переименования
readonly START_FOLDER=$1 readonly START_PACKAGE=$2 readonly REPLACE_PACKAGE=$3 echo "START_FOLDER = $" echo "START_PACKAGE = $" echo "REPLACE_PACKAGE = $" echo "" echo "=========" echo "" for filename in $/*; do withoutPath=$(basename -- "$filename") fff="$" echo "sh global_rename.sh $.$ $.$ && \\" done echo "" echo "======== spoiler">Сам скрипт переименования пакетов #!/bin/bash readonly OLD_PACKAGE=$1 readonly NEW_PACKAGE=$2 find . -type d \( \ -name 'firebase' -o \ -name 'gradle' -o \ -name 'hooks' -o \ -name 'lint' -o \ -name 'profiling' -o \ -name 'scripts' -o \ -name 'detekt' -o \ -name '.git' -o \ -name '.gradle' -o \ -name '.mainframer' -o \ -name 'build' -o \ -name '.idea' -o \ -name 'android-style-guide' -o \ -name 'ci' \ \) -prune -o \ -type f \( \ -name '*.java' -o \ -name '*.kt' -o \ -name '*.gradle' -o \ -name '*.xml' -o \ -name '*.txt' \ \) \ -print0 | xargs -0 sed -i '' "s/$/$/g" echo "done rename for ( $ / $ )"
В итоге мы просидели все выходные, по очереди генерируя команды для каждого модуля и проверяя, собирается ли приложение и запускается ли оно вообще.
Так прошло 3 дня и две ночи.
Ответственные за это люди ушли отдыхать, а остальные разработчики продолжили, но у нас ничего не вышло.
А не получилось потому, что:
- в больших фиче ветках разработчиков было много изменений и таких веток было несколько, если бы каждый мержил себе сам, то он с большей вероятностью кто-то мог ошибиться где-то
- также некоторые ветки пересекались по изменениям и было трудно потом все это смержить еще и с ренеймингом
Подумав, мы решили что было бы хорошо, если кто-то один замержит ренейминг везде!
Поэтому решили фиче ветки смержить сразу в develop, а те кто не хочет мержить сейчас, а хотят еще поработать, будут потом сами разруливать конфликты с новым develop.
И мы не до конца понимали, чем нам это грозит.
Смержив все, что можно было в develop, в пятницу мы объявили кодфриз и избранный занялся мержем ренейминга в develop.
За неделю работы команд накопилось кучу изменений, и при мерже вылезло много конфликтов.
После мержа ветки с ренеймингом в старый develop (недельной давности), старые фичи сменили имя и пакет, по факту это означало, что они отправились в другую папку.
Обратите внимание на файлы, все хорошо, они лежат в новом месте.
Но при мерже ренейминга в новый develop после недельной работы, появились такие фантомные структуры:
По старым путям пакетов лежали файлы, которые были изменены разработчиками, но изменены они были в старой структуре, и таких мест было много. голова шла кругом.
Естественно, все это править руками будет долго. Чтобы ускорить процесс, в качестве вспомогательного инструмента, была выбрана утилита rsync, потому что она умеет рекурсивно мержить папки друг с другом, и ей можно указать стратегию разрешения конфликтов (перезапись, оставить новое, оставить старое, etc.).
В консоли с ее помощью рекурсивно переносили папки фичей. Из папки со старым названием в папку с новым.
Затем с помощью волшебных настроек Android Studio — Optimize imports on fly и Add unambiguous imports on the fly, были поправлены проблемы с импортами. Да, мы заходили ручками в каждый файл.
По-хорошему, надо было идти сначала от корневых модулей (shared/core) и делать регулярный синк проекта в IDE. Так пришлось бы гораздо меньше страдать потом с импортами при переносах файлов, ибо Android Studio автоматически бы их сразу переименовывала во всех местах, куда дотянется.
Но эта мысль пришла в наши светлые головы уже после проделанной работы и полученного опыта.
Спустя пару дней мучений мержа, develop был актуализирован и содержал новую структуру папок и новые имена пакетов.
А ребятам, которые не стали мержить свои ветки в develop, была предоставлена инструкция, как безболезненно влить в себе новый develop.
Но все было не так просто, как хотелось бы.
Приведу список основных пунктов, если вдруг вам понадобится:
- Вмержить к себе в ветку develop и сохранить лог конфликтов,
на память чтобы понимать поле работы
- Конфликты типа modified — modified разрешить самим, руками, но таких конфликтов было минимум
- Остальные конфликты нужно разрешить в свою пользу
- Нужно перевести все созданные вами папки на новую структуру
- Каждый перенос лучше делать отдельным коммитом, чтобы ничего не потерять и чтобы лучше контролировать процесс
- Разрулить силами Android Studio все неправильные импорты
- Удалить все неиспользуемые папки
В теории звучит просто, но в реальных условиях:
- если переносить модуль/модули у себя в ветке, то рефакторинг будет применен к модулю, который располагался в старом месте. Приходилось повторить все, что уже сделал, но для перенесенного модуля (получилась двойная работа) + удалить старый модуль, причем сделать аккуратно, чтобы осталась история гита
- если переносить файлы у себя в ветке, то рефакторинг будет применен к старым файлам, естественно появлялась неактуальная фантомная структура файлов, которые были уже перенесены, поэтому приходилось аккуратно их объединить с новыми
Всего файлов с конфликтами было ~500 в ~50 модулях.
Итоги ренейминга
- Названия всех модулей соответствуют структуре папок.
И теперь включать gradle-модуль в settings.gradle можно через
include(':shared:feature:location'), поскольку путь совпадает с неймингом.
Для примера, раньше это делалось вот так:
Все модули из папки feature из корня проекта (о чем я писал вначале), переместились на свои законные места в папку applicant/feature.
Имена пакетов стали соотноситься с расположением фичи.
Например, фича геолокации :shared:feature:location получила пакет ru.hh.shared.feature.location.
Gradle-модули избавились от префиксов feature, shared, core и т.д.
Но префиксы сабфичей решено было оставить. А потом и их решили не писать 🙂
Появилась возможность статической валидация подключения модулей.
В будущем будем проверять нейминг модулей и пакетов, а также корректность связей между модулями разных типов.
Наши рекомендации
Прежде чем идти в эту историю, надо написать скрипты, которые автоматизируют большую часть работы. Можно воспользоваться нашими наработками, но сначала нужно проверить их на валидность вашему проекту.
И основной совет — не делайте руками, делайте сразу через скрипты. Это сэкономит кучу нервов и времени.
Также нужно составить чеклист переноса модулей/файлов. И после каждого этапа переноса по чеклисту нужно проверять: собирается ли проект. Да, это долго и замедляет процесс, но сильно упрощает жизнь в дальнейшем. По крайней мере будет понимание, что “вот из-за этого у меня проект развалился“.
Для подобных глобальных изменений нужно обязательно уведомлять всю команду и заранее договариваться, как будет идти разработка в это время, чтобы минимизировать конфликты. Самый радикальный инструмент для этого — фичефриз/кодфриз etc. Если вы проводите такой рефакторинг, поддерживайте регулярную связь с командой, сразу же сообщайте о проблемах и потенциально сложных для мерджа местах.
И не стоит недооценивать эту задачу, если у вас большая кодовая база. Она стопудово займёт больше времени, чем вам кажется.
На этом все, если у вас остались какие-либо вопросы или вы можете поделиться собственным опытом, пишите в комментариях.
Спасибо за внимание 😉
Маленький опрос для большого исследования
Мы проводим ежегодное исследование технобренда крупнейших IT-компаний России. Нам очень нужно знать, что вы думаете про популярных (и не очень) работодателей. Опрос займет всего 10 минут.
Пройти опрос можно здесь.
Как переименовать проект в Android Studio
Чтобы переименовать название проекта в Android Studio открываем проект и нажимаем на Show Options Menu (шестеренка) и снимаем галочку с Compact Middle Packages. Таким образом разбиваем названия через точку на отдельные каталоги. Например, com.domain.application разбивается на каталог com в котором каталог domain и далее каталог application. По сути получается наименование сайта, только наоборот.
Затем выбираем каталог для переименования и правой клавишей мыши открываем Refactor->Rename
Появится окно предупреждения, жмем Rename Package
Указываем новое название каталога
Далее снизу жмем Do Refactor
После того, как переименовали файлы, возможно где-то в файлах остались старые названия приложения, например, com.domain.application. В Android Studio нажимаем комбинаций клавиш для поиска ctrl+shift+F. Отобразятся файлы, например, BuildConfig.java, Activity.java, Build.Gradle и другие, в которых есть старые названия, заменяем их на новые.
3335