Site icon AppTractor

Интеграция форматирования кода в Android-проекты

Это первая статья из серии, в которой мы расскажем о нашем пути внедрения и автоматизации форматирования и статического анализа кода в Android-проектах.

Мотивация

В наших проектах мы изначально полагались на функцию IntelliJ Reformat Code с настройками стиля кода по умолчанию. Это помогало поддерживать читабельность и согласованность нашей кодовой базы.

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

Чтобы решить эти проблемы, мы искали автоматизированные решения для обеспечения последовательного форматирования. Мы внедрили pre-commit hook скрипт, который запускает ktfmt для форматирования кода Kotlin.

Когда наша команда начала переходить на Jetpack Compose, мы хотели избежать распространенных ошибок из-за недостатка опыта. Мы открыли для себя detekt и compose-rules для статического анализа кода, которые также были добавлены в наш pre-commit хук.

Интеграция этого в pre-commit хук обеспечила высокое качество и согласованность кода в команде, оптимизировала наш рабочий процесс и сократила ручную работу. В целом, это значительно улучшило наш процесс разработки, обеспечив последовательное форматирование кода и статический анализ без особых усилий.

Поскольку это оказалось невероятно полезным для нас, мы решили поделиться своим опытом в следующем цикле статей.

Обзор цикла

Этот цикл разделен на следующие части, каждая из которых посвящена определенному аспекту нашего процесса:

  1. Интеграция форматирования кода в проекты Android. Вводная статья о важности форматирования кода и о том, как интегрировать такие инструменты, как ktfmt. Мы расскажем о преимуществах форматирования кода и предоставим пошаговое руководство по настройке ktfmt в вашем проекте, который будет автоматически запускаться перед каждым коммитом с помощью pre-commit хука.
  2.  Повышение качества кода с помощью detekt для статического анализа. Глубокое погружение в использование detekt для статического анализа кода в Android-проектах. В этой статье мы рассмотрим, как detekt помогает выявлять «плохие запахи» кода, внедрять стандарты кодирования и повышать общее качество кода. Для автоматизации проекта статический анализ detekt будет добавлен в наш pre-commit hook.
  3. Разработка пользовательского Gradle-плагина для форматирования и статического анализа. Пошаговое руководство по созданию и публикации пользовательского плагина Gradle, который интегрирует ktfmt и detekt и применяет наш pre-commit hook. Этот плагин обеспечит последовательное применение форматирования кода и статического анализа и их автоматическое выполнение при каждом коммите во всех ваших проектах, что упростит процесс настройки новых проектов.

К концу этой серии у вас будут инструменты и знания для поддержания чистой, последовательной и качественной кодовой базы в ваших Android-проектах.

Сначала давайте рассмотрим интеграцию ktfmt.

Интеграция форматирования кода в ваши Android-проекты

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

Для начала — краткое введение в ktfmt.

Введение в ktfmt

ktfmt — это инструмент, разработанный Facebook*, который красиво форматирует код Kotlin на основе google-java-формата. Он всегда выдает один и тот же результат, независимо от того, как код выглядит изначально, позволяя разработчикам сосредоточиться на сути своего кода.

Одной из ключевых особенностей ktfmt является его ненастраиваемость, которая призвана способствовать согласованности. Изначально мы использовали ktlint, но после изучения и тестирования пришли к выводу, что ktfmt более последователен и прост в настройке. Этот комментарий на Reddit также подчеркивает надежность ktfmt. Вы также можете найти различия в официальном readme ktfmt.

Давайте настроим его в нашем проекте.

Настройка ktfmt

Для интеграции ktfmt можно использовать инструмент интерфейса командной строки (CLI), но в этом руководстве мы сосредоточимся на использовании плагина ktfmt для Gradle.

Просто примените последнюю версию плагина в файле верхнего уровня build.gradle или build.gradle.kts, синхронизируйте проект, и все готово.

plugins {
    ...
    id("com.ncorti.ktfmt.gradle") version("0.18.0") // Replace with latest version
}

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

Настройка ktfmt

Поскольку ktfmt не очень настраивается, это означает, что легко придерживаться строгой системы форматирования. При сложных настройках вы можете попасть в ловушку слишком большого количества пользовательских правил. Это может навредить вашей кодовой базе в долгосрочной перспективе, поскольку все больше и больше правил означают меньшую согласованность в целом.

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

Для наших целей мы решили использовать стиль кода Kotlin Lang (отступы в блоках по 4 пробела), но вы можете попробовать использовать любой из них, который вам больше нравится.

Чтобы настроить плагин ktfmt Gradle на использование стиля кода Kotlin Lang, добавьте следующий код в файл верхнего уровня build.gradle или build.gradle.kts:

allprojects {
    ...
    apply(plugin = "com.ncorti.ktfmt.gradle")
    ktfmt { // 1
        kotlinLangStyle() // 2
    }
}

В приведенном выше фрагменте мы сделали следующее:

  1. Открыли блок конфигурации ktfmt. Вся конфигурация находится в этом блоке. Примерами конфигурации могут служить ширина перевода строки, различные отступы, обработка импорта и т.д. Полный список настраиваемых полей можно найти здесь.
  2. Применили стиль Kotlin Lang. Не стесняйтесь применять любой стиль кода, который вам нравится.

Теперь, когда вы применили плагин, вы можете запустить форматирование!

Запуск ktfmtFormat

После применения плагина и синхронизации проекта с плагинами Gradle вы можете запустить Gradle-задачу ktfmtFormat.

Это можно сделать из окна Gradle или из терминала.

Окно Gradle:

Терминал:

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

Чтобы решить эту проблему, мы внедрим хук перед коммитом для нашего проекта.

Создание pre-commit хука

Чтобы избежать необходимости вручную запускать Gradle-задачу ktfmtFormat перед каждым коммитом, мы решили реализовать скрипт в pre-commit hook.

Прежде всего, убедитесь, что Git настроен для вашего проекта, прежде чем приступать к следующим шагам.

  1. Перейдите в корневой каталог вашего проекта, например ~/projects/SampleProject/
  2. Включите видимость скрытых файлов и папок [Windows, MacOS, Linux]
  3. Перейдите в rootProjectDir/.git/hooks и создайте новый файл с именем pre-commit без расширения.
  4. Откройте файл в текстовом редакторе и вставьте следующий код:
#!/bin/bash

# Exit immediately if a command exits with a non-zero status
set -e

echo "
===================================
|  Formatting code with ktfmt...  |
==================================="

# Run ktfmt formatter on the specified files using Gradle
if ! ./gradlew --quiet --no-daemon ktfmtFormat; then
    echo "Ktfmt failed"
    exit 1
fi

# Check if git is available
if ! command -v git &> /dev/null; then
    echo "git could not be found"
    exit 1
fi

# Add all updated files to the git staging area
git add -u

# Exit the script successfully
exit 0
  1. Сохраните файл и закройте редактор.
  2. Добавьте разрешение на выполнение для вашего скрипта. На Mac откройте терминал, перейдите в каталог rootProjectDir и выполните следующую команду: chmod +x .git/hooks/*.

При этом код из скрипта перед коммитом будет выполняться перед каждой поставкой кода. Если какая-либо из команд не выполнится, коммит будет прерван. В настоящее время созданный нами pre-commit хук запускает задачу ktfmtFormat Gradle, но он будет расширен в следующих частях этой серии.

Попробуйте инициировать коммит. Файлы вашего проекта должны быть автоматически отформатированы.

По умолчанию этот скрипт запускает форматирование для всех файлов проектов .kt и .kts. Вы можете запустить форматирование только для определенных файлов, вот пример того, как можно запустить форматирование только для избранных файлов.

Применение форматирования только к определенным файлам проекта

Если вы хотите запустить скрипт только для некоторых файлов, вы можете воспользоваться параметром include-only.

Для этого перейдите в файл верхнего уровня build.gradle или build.gradle.kts и вставьте следующий код:

tasks.register<KtfmtFormatTask>("ktfmtPrecommitFormat") {
    group = "formatting"
    description = "Runs ktfmt on kotlin files in the project"
    source = project.fileTree(rootDir).apply { include("**/*.kt", "**/*.kts") }
}

В приведенном выше фрагменте кода регистрируется новая задача Gradle с именем ktfmPrecommitFormat, мы определили группу задач и добавили описание. Мы установили источник, включающий все файлы .kt и .kts внутри проекта.

Вот как можно реализовать скрипт перед коммитом, который будет запускать ktfmt только на staged git-файлах.

1. Чтение и обработка измененных файлов

ktfmt принимает аргумент — include-only, который представляет собой строку имен файлов, которые мы хотим отформатировать, разделенных точкой с запятой (;).

  echo "
   ===================================
   |  Formatting code with ktfmt...  |
   ==================================="

   # Get the list of staged files from git, filtering by their status and paths
   git_output=$(git --no-pager diff --name-status --no-color --cached)
   file_array=()
   file_names=()

   # Process each line of git output
   while IFS= read -r line; do
       status=$(echo "$line" | awk '{print $1}')
       file=$(echo "$line" | awk '{print $2}')
       # Include only .kt and .kts files that are not marked for deletion
       if [[ "$status" != "D" && "$file" =~ \.kts$|\.kt$ ]]; then
           # Extract relative paths starting from 'src/'
           relative_path=$(echo "$file" | sed 's/.*\(src\/.*\)/\1/')
           file_array+=("$relative_path")
           file_names+=("$file")
       fi
   done <<< "$git_output"

   # Join file array into a semicolon-separated string
   files_string=$(IFS=";"; echo "${file_array[*]}")

2. Форматирование заданных файлов после обработки

# Run ktfmt formatter on the specified files
./gradlew --quiet --no-daemon ktfmtPrecommitFormat --include-only="$files_string"
ktfmtStatus=$?

# If ktfmt fails, print a message and exit with the failure code
if [ "$ktfmtStatus" -ne 0 ]; then
    echo "Ktfmt failed with exit code $ktfmtStatus"
    exit 1
fi

3. Повторное добавление измененных файлов в Git

Когда хук перед коммитом изменяет файлы (например, форматирует их), модифицированные версии этих файлов нужно снова поместить в коммит. Это происходит потому, что файлы первоначально добавляются в индекс еще перед запуском хука. Если хук изменяет эти файлы (например, через форматирование), изменения вносятся в рабочий каталог, но не в индекс.

# Check if git is available
if ! command -v git &> /dev/null; then
    echo "git could not be found"
    exit 1
fi

# Re-add the formatted files to the git index with the original paths
for i in "${!file_array[@]}"; do
    file=${file_names[$i]}
    if [ -f "$file" ]; then
        git add "$file"
    fi
done

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

echo "
===================================
|  Formatting code with ktfmt...  |
==================================="

# Get the list of staged files from git, filtering by their status and paths
git_output=$(git --no-pager diff --name-status --no-color --cached)
file_array=()
file_names=()

# Process each line of git output
while IFS= read -r line; do
    status=$(echo "$line" | awk '{print $1}')
    file=$(echo "$line" | awk '{print $2}')
    # Include only .kt and .kts files that are not marked for deletion
    if [[ "$status" != "D" && "$file" =~ \.kts$|\.kt$ ]]; then
        # Extract relative paths starting from 'src/'
        relative_path=$(echo "$file" | sed 's/.*\(src\/.*\)/\1/')
        file_array+=("$relative_path")
        file_names+=("$file")
    fi
done <<< "$git_output"

# Join file array into a semicolon-separated string
files_string=$(IFS=";"; echo "${file_array[*]}")

# Run ktfmt formatter on the specified files
./gradlew --quiet --no-daemon ktfmtPrecommitFormat --include-only="$files_string"
ktfmtStatus=$?

# If ktfmt fails, print a message and exit with the failure code
if [ "$ktfmtStatus" -ne 0 ]; then
    echo "Ktfmt failed with exit code $ktfmtStatus"
    exit 1
fi

# Check if git is available
if ! command -v git &> /dev/null; then
    echo "git could not be found"
    exit 1
fi

# Re-add the formatted files to the git index with the original paths
for i in "${!file_array[@]}"; do
    file=${file_names[$i]}
    if [ -f "$file" ]; then
        git add "$file"
    fi
done

Сравнение неформатированного и форматированного кода

Вот пример того, как ktfmt может преобразовать ваш код.

Неформатированный код:

@Composable
fun ScreenContent(
    modifier:Modifier=Modifier) {
    var showGreetingText by remember {mutableStateOf(true)}
    Column(
        modifier =modifier.fillMaxSize().padding(20.dp),verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        if(showGreetingText) Greeting(name="Android", modifier = Modifier.align(Alignment.CenterHorizontally))
        Button(
            onClick={showGreetingText=!showGreetingText }, modifier = Modifier.align(Alignment.CenterHorizontally)
        ) { Text(text=if (showGreetingText) "Hide greeting text" else "Show greeting text") }
    }
}

Форматированный:

@Composable
fun ScreenContent(modifier: Modifier = Modifier) {
    var showGreetingText by remember { mutableStateOf(true) }
    Column(
        modifier = modifier.fillMaxSize().padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        if (showGreetingText)
            Greeting(name = "Android", modifier = Modifier.align(Alignment.CenterHorizontally))
        Button(
            onClick = { showGreetingText = !showGreetingText },
            modifier = Modifier.align(Alignment.CenterHorizontally)
        ) {
            Text(text = if (showGreetingText) "Hide greeting text" else "Show greeting text")
        }
    }
}

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

Преимущества интеграции ktfmt

Интеграция ktfmt в наши проекты дала несколько преимуществ:

Ресурсы

Чтобы глубже изучить ktfmt, ознакомьтесь со следующими ресурсами:

Заключение

Внедрение таких инструментов форматирования кода, как ktfmt, оказалось важным для поддержания качественной кодовой базы. Внедрение ktfmt улучшило читаемость кода, сократило количество человеческих ошибок и способствовало лучшей совместной работе членов команды. Хотя на начальном этапе могут возникнуть некоторые сложности с выбором инструмента форматирования, установкой и настройкой плагина и хука, который форматирует код перед коммитом, долгосрочные преимущества значительно перевешивают их.

Источник

Exit mobile version