Integrating Code Formatting into Your Android Projects

pavikaa

Marko Pavičić

Posted on July 1, 2024

Integrating Code Formatting into Your Android Projects

This is the first blog in the series where we will share our journey of implementing and automating code formatting and static code analysis in Android projects.

Motivation for the Series

In our projects, we initially relied on IntelliJ's Reformat Code feature with the default code style settings. This helped maintain the readability and consistency of our codebase.

Since it was a manual step it was easy to forget after every change. And if we weren't diligent, we'd end up with a messy project with code all over the place, remembering to reformat code added extra overhead to the development process.

To address these issues, we sought automated solutions to ensure consistent formatting. We implemented a pre-commit hook script that runs ktfmt for Kotlin code formatting.

When our team began migrating to Jetpack Compose, we wanted to avoid common mistakes due to our limited expertise. We discovered detekt and compose-rules for static code analysis which was also added to our pre-commit hook.

Integrating this pre-commit hook maintained a high code quality and consistency across the team, streamlined our workflow, and reduced manual overhead. Overall, it significantly improved our development process by enforcing consistent code formatting and static analysis practices effortlessly.

Since this proved incredibly useful for us, we decided to share our experience in the following article series.

Series Overview

This series is divided into the following parts, each focusing on a specific aspect of our process:

  1. Integrating Code Formatting into Your Android Projects

    • An introduction to the importance of code formatting and how to integrate tools like ktfmt. We’ll cover the benefits of code formatting and provide a step-by-step guide to setting up ktfmt in your project which will automatically run before each commit using a pre-commit hook.
  2. Enhancing Code Quality with detekt for Static Analysis

    • A deep dive into using detekt for static code analysis in Android projects. This article will explore how detekt helps in identifying code smells, enforcing coding standards, and improving overall code quality. To automate the static analysis project detekt will be added to our pre-commit hook
  3. Developing a Custom Gradle Plugin for Formatting and Static Analysis

    • Step-by-step guide to creating and publishing a custom Gradle plugin that integrates both ktfmt and detekt and applies our pre-commit hook. This plugin will ensure that code formatting and static analysis are consistently applied and automatically ran on each commit across all your projects, simplifying the setup process for new projects.

By the end of this series, you'll have the tools and knowledge to maintain a clean, consistent, and high-quality codebase in your Android projects.

Let's look into integrating ktfmt first.

Integrating Code Formatting into Your Android Projects

This blog will take you through the implementation of code formatting with ktfmt. We will explain its purpose and present the benefits we discovered after the integration.

Firstly, a brief introduction to ktfmt.

Introduction to ktfmt

ktfmt is a tool developed by Facebook that pretty-prints (formats) Kotlin code based on google-java-format. It always produces the same result, regardless of how the code looks initially, allowing developers to focus on the essence of their code.

One of the key features of ktfmt is its non-customizability, which is designed to promote consistency. Initially, we used ktlint, but after research and testing, we found ktfmt to be more consistent and simpler to set up. This comment on Reddit also highlights the reliability of ktfmt: Reddit Comment. You can also find the differences in the official ktfmt readme.

Let's set it up in our project.

Setting Up ktfmt

To integrate ktfmt, you can use the command-line interface (CLI) tool, but for this guide, we will focus on using the ktfmt Gradle plugin.

Simply apply the latest plugin version inside your Top-level build.gradle or build.gradle.kts file, sync your project, and you're good to go.

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

Now that you have the plugin integrated, let's look into configuring it for your specific use case.

Configuring ktfmt

Since ktfmt isn't very customizable, it means it's easy to adhere to a strict formatting system. With complex customization options, you can fall into the trap of having too many custom rules. This can hurt your codebase in the long run, as more and more rules mean there is less consistency overall.

The plugin gives you the option to choose which code style you want to use. The main difference between the code styles is the number of spaces used for block indentations.

For our purposes, we decided to use the Kotlin Lang code style (4 space block indentation), but feel free to try to use any one you prefer.

To configure the ktfmt Gradle plugin to use the Kotlin Lang code style, add the following code to your Top-level build.gradle or build.gradle.kts file:

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

In the snippet above, we did the following:

  1. Opened the configuration block of ktfmt. All of the configuration goes within this block. Some configuration examples are line break width, various indents, import handling, and similar. The complete list of configurable fields can be found here.
  2. Applied the Kotlin Lang style, feel free to apply any code style you like.

Now that you've applied the plugin, you can finally run the formatting!

Running ktfmtFormat

After you've applied the plugin and synced your project with Gradle plugins you can run the ktfmtFormat Gradle task.
That can be done from:

  • The Gradle tool window:
    • Open the Gradle tool window
    • Navigate to Tasks/formatting
    • Double click on the ktfmtFormat task (or right-click and click on Run)

gradle-tool-window

  • The terminal:
    • Open the Terminal window inside Android Studio
    • Run the following command: ./gradlew ktfmtFormat

terminal

So far, we have implemented a consistent code formatting style, but we haven't fixed the main problem, having to manually reformat the code.

To fix this issue we will implement a pre-commit hook for our project.

Creating a pre-commit hook

To avoid having to manually run the ktfmtFormat Gradle task before each commit, we've decided to implement a pre-commit hook script.

Firstly, make sure Git is set up for your project before you continue with the next steps.

  1. Navigate to your project's root directory, e.g., ~/projects/SampleProject/
  2. Toggle hidden files and folders visibility [Windows, MacOS, Linux]
  3. Navigate to rootProjectDir/.git/hooks and create a new file named pre-commit without an extension
  4. Open the file with a text editor and paste the following code:

    #!/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 
    
  5. Save the file and close the editor

  6. Add the execution permission for your pre-commit hook script, on Mac, open the Terminal, navigate to the rootProjectDir directory and run the following command: chmod +x .git/hooks/*

What this does is run the code from the pre-commit script before each commit. If any of the commands failed, the commit would be aborted. Currently, the pre-commit hook we created runs the ktfmtFormat Gradle task, but it will be expanded in the following parts of this series.

Try initiating a commit; your project's files should be automatically formatted.

pre-commit

By default, this script will run the formatting on all .kt and .kts project files. You can run the formatting on specific files only, here's an example of how you can run formatting on staged files only.

Applying Formatting only on specific project files

If you want to run the script on only some of the files, you can take advantage of the ktfmt --include-only parameter.

To do that, go to your Top-level build.gradle or build.gradle.kts file and paste the following code:

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

The code snippet above registers a new Gradle task named ktfmPrecommitFormat, we defined the tasks group and added a description. We've set the source to include all of the .kt and .kts files inside the project

Here's how you can implement a pre-commit hook script that runs ktfmt only on staged git files:

  1. Reading staged files and processing them

    ktfmt takes in a --include-only argument which is a String of joint file names which we want to format separated by a semicolon (;)

    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. Formatting specified files after processing

    # 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. Re-add the modified files to Git

    When a pre-commit hook modifies files (e.g., formats them), the modified versions of these files need to be staged again for commit. This is because the files are initially staged (added to the index) before the hook runs. If the hook changes these files (e.g., through formatting), the changes are made in the working directory but not in the index.

    # 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
    

The complete script file should look like this:

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
Enter fullscreen mode Exit fullscreen mode

Comparing Non-Formatted and Formatted Code

Here is an example of how ktfmt can transform your code.

Non-Formatted Code:

@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") }
    }
}
Enter fullscreen mode Exit fullscreen mode

Formatted Code:

@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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the formatted code is much cleaner and easier to read, which helps in maintaining the codebase and avoiding errors.

Benefits of Integrating ktfmt

Integrating ktfmt into our projects has provided several benefits:

  • Consistency: The codebase is consistently formatted, which enhances readability and maintainability.

  • Reduced Human Error: Automated formatting reduces the likelihood of human error, ensuring that the code adheres to a standard style.

  • Focus on Code Essence: Developers can focus more on the logic and functionality of the code rather than worrying about formatting.

Resources

To dive deeper into ktfmt, check out the following resources:

Conclusion

Implementing code formatting tools like ktfmt proved essential for maintaining a high-quality codebase. Implementing ktfmt improved code readability, reduced human errors, and facilitated better collaboration among team members. While there may be some initial challenges as to which formatting tool to use, how to set up and configure the plugin and the pre-commit hook, the long-term benefits far outweigh them.

Stay tuned for the next article, where we will delve into enhancing code quality with Detekt for static analysis. In the meantime check out our Barrage blog for more interesting topics.

💖 💪 🙅 🚩
pavikaa
Marko Pavičić

Posted on July 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related