Implementing and automating code formatting and static code analysis in your Android projects

In the first part of our series on how to play your code the right way on your Kotlin projects, we will focus on integrating ktfmt for code formatting.

Reading Time10 minutes

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 blog 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 reformatting code was a manual step, it was easy to forget after each change. If not for our diligence, 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 effortlessly enforcing consistent code formatting and static analysis practices.

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

Blog series overview

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

Integrating code formatting into your Android projects

In 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.

Enhancing code quality with detekt for Static Analysis

A deep dive into using detekt for static code analysis in Android projects: Here, we will explore how detekt helps identify code smells, enforce coding standards, and improve overall code quality. Detekt will be added to our pre-commit hook to automate the static analysis project.

Developing a custom gradle plugin for formatting and static analysis

This is a 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 run 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.

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. With that said, this comment on Reddit highlighted the reliability of ktfmt, and we found the code 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
}

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

Configuring ktfmt

Since ktfmt isn’t especially 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 allows you 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 other 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
    }
}

In the code above, we:

  • 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.
  • Applied the Kotlin Lang style, but you can 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 by:

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 implemented a pre-commit hook for our project.

Creating a pre-commit hook

We’ve decided to implement a pre-commit hook script to avoid manually running the ktfmtFormat Gradle task before each commit.

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

  • Navigate to your project’s root directory, e.g., ~/projects/SampleProject/.
  • Toggle hidden files and folder visibility (Windows, MacOS, Linux).
  • Navigate to rootProjectDir/.git/hooks and create a new pre-commit file without an extension.
  • 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 
  • Save the file and close the editor.
  • 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/*

This runs the code from the pre-commit script before each commit. If any of the commands fail, the commit is 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 format all .kt and .kts project files. You can run the formatting on specific files only, so here’s an example of how you can run formatting on staged files exclusively.

Applying formatting only on specific project files

If you want to run the script on only some of the files, you can use 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("ktfmtPrecommitFormat") {
    group = "formatting"
    description = "Runs ktfmt on kotlin files in the project"
    source = project.fileTree(rootDir).apply { include("**/*.kt", "**/*.kts") }
}

The code above registers a new Gradle task named ktfmPrecommitFormat. We defined the task group, added a description, and 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:

  • Reading staged files and processing them - ktfmt takes in a --include-only argument, which is a string of joint file names that 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[*]}")
  • 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
  • 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

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") }
    }
}

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")
        }
    }
}

As you can see, the formatted code is much cleaner and easier to read, which helps maintain the codebase and avoid 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:

Last, but not least

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 and how to set up and configure the plugin and the pre-commit hook, the long-term benefits outweigh them by far.

Stay tuned for the next part, where we delve into enhancing code quality with detekt for static analysis. 

In the meantime, check out our blog section for more interesting topics!

Hey, you! What do you think?

They say knowledge has power only if you pass it on - we hope our blog post gave you valuable insight.

If you want to share your opinion or you need help with your own projects with codes that need polishing, feel free to contact us.

We'd love to hear what you have to say!