What is Android Lint and how it helps write maintainable code

  —  

When developers are not careful enough, things can go south. Classic developer oversights include the usage of an API added in the newer version that is not supported in the old versions, actions that require specific permissions, missing translations, just to name a few.

On top of that, Java, Kotlin, as well as any other programming languages have their own set of programming constructs that might result in a poor performance.

Hello, Lint

We use a tool called Lint (or Linter) to prevent such issues. Lint is a tool for static code analysis that helps developers catch potential issues before the code even compiles. It runs multiple checks on the source code that can detect issues like unused variables or function arguments, condition simplification, wrong scopes, undefined variables or functions, poorly optimized code etc. When we talk about Android development, there are hundreds of existing Lint checks available out-of-the-box.

But sometimes, we need to detect specific issues in our codebase that are not covered by those existing checks.

Hello, custom Lint check

Before we start coding, let's define our goal and see how to implement each part of that goal in terms of a Lint API. The goal is to create a check to detect wrong method call on an object. The idea of this check is to detect if the method for setting click listener on an Android View component is the one that will throttle multiple consecutive clicks so we can avoid opening same activity, or calling an API multiple times.

Custom Lint checks are written as a part of standard Java (or Kotlin) module. The easiest way to start is to create a simple Gradle-based project (it doesn't have to be an Android project).

Next, you'd want to add Lint dependencies. In your module's build.gradle file add:

compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"

Now, there's a trick I've learned by researching this topic. lintVersion should be gradlePluginVersion + 23.0.0. gradlePluginVersion is variable defined in root build.gradle project when you create an Android project using Android Studio and at this moment, the latest stable version is 3.3.0. That means that lintVersion should be 26.3.0.

Each Lint check consists of 4 parts:

  • Issue - the problem in our code we are trying to prevent from happening. When Lint check fails, this is what is reported to the user
  • Detector - a tool for finding a problem which exposes parts of the source code to us in terms of a Lint API classes
  • Implementation - scope in which a problem might happen (source file, XML file, compiled code, ...)
  • Registry - custom Lint check registry which will be used alongside existing registry that contains predefined checks

Implementation

Let's start by creating an implementation for our custom check. Each implementation consists of the class that implements a detector and a scope.

val correctClickListenerImplementation = Implementation(CorrectClickListenerDetector::class.java, Scope.JAVA_FILE_SCOPE)

Have in mind that Scope.JAVA_FILE_SCOPE will also work for Kotlin classes.

Issue

Next step is to use this implementation for defining an issue. Each issue consists of several parts:

  • ID - unique identifier
  • Description - short (5-6 words) summary of an issue
  • Explanation - full issue explanation with a suggestion on how to fix it
  • Category - category of an issue (performance, translation, security etc.)
  • Priority - the importance of the issue, in a range from 1 to 10 with 10 being the highest. This will be used to sort issues in a report generated by running Lint task
  • Severity - a severity of an issue (fatal, error, warning, info or ignore)
  • Implementation - implementation that will be used to detect this issue
val ISSUE_CLICK_LISTENER = Issue.create(
    id = "UnsafeClickListener",
    briefDescription = "Unsafe click listener", 
    explanation = """"
        This check ensures you call click listener that is throttled 
        instead of a normal one which does not prevent double clicks.
        """.trimIndent(),
    category = Category.CORRECTNESS,
    priority = 6,
    severity = Severity.WARNING,
    implementation = correctClickListenerImplementation
)

Detector

Lint API offers interfaces for each scope you can define in the implementation. Each of these interfaces exposes methods that you can override and access parts of the code you are interested in.

  • UastScanner - Java or Kotlin files (UAST - Unified Abstract Syntax Tree)
  • ClassScanner - compiled files (bytecode)
  • BinaryResourceScanner - binary resources like bitmaps or res/raw files
  • ResourceFolderScanner - resources folders (not specific files in them)
  • XmlScanner - XML files
  • GradleScanner - Gradle files
  • OtherFileScanner - everything else

Also, Detector class is a base class that has dummy implementations of all the methods each of the above interfaces exposes so you are not forced to implement a complete interface in case you need only one method.

Now, we are ready to implement a detector which will check the correct method call on an object.


private const val REPORT_MESSAGE = "Use setThrottlingClickListener"

/**
 * Custom detector class that extends base Detector class and specific
 * interface depending on which part of the code we want to analyze.
 */
class CorrectClickListenerDetector : Detector(), Detector.UastScanner {

/**
* Method that defines which elements of the code we want to analyze.
* There are many similar methods for different elements in the code,
* but for our use-case, we want to analyze method calls so we return
* just one element representing method calls.
*/
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
    return listOf<Class<out UElement>>(UCallExpression::class.java)
}

/**
    * Since we've defined applicable UAST types, we have to override the
    * method that will create UAST handler for those types.
    * Handler requires implementation of an UElementHandler which is a
    * class that defines a number of different methods that handle
    * element like annotations, breaks, loops, imports, etc. In our case,
    * we've defined only call expressions so we override just this one method.
    * Method implementation is pretty straight-forward - it checks if a method
    * that is called has the name we want to avoid and it reports an issue otherwise.
    */
    override fun createUastHandler(context: JavaContext): UElementHandler? {
        return object: UElementHandler() {

            override fun visitCallExpression(node: UCallExpression) {
                if (node.methodName != null && node.methodName?.equals("setOnClickListener", ignoreCase = true) == true) {
                    context.report(ISSUE_CLICK_LISTENER, node, context.getLocation(node), REPORT_MESSAGE, createFix())
                }
            }
        }
    }

    /**
     * Method will create a fix which can be trigger within IDE and
     * it will replace incorrect method with a correct one.
     */
    private fun createFix(): LintFix {
        return fix().replace().text("setOnClickListener").with("setThrottlingClickListener").build()
    }
}

The last thing we need to do is add issues to our registry and tell Lint there is a custom registry of issues that it should use alongside a default one.

class MyIssueRegistry : IssueRegistry() {
    override val issues: List<Issue> = listOf(ISSUE_CLICK_LISTENER)
}

In module's build.gradle:

jar {
    manifest {
        attributes("Lint-Registry-v2": "co.infinum.lint.MyIssueRegistry")
    }
}

where co.infinum.lint is package of MyIssueRegistry class. Now, you can run jar task using gradlew script and library should appear in build/libs directory.

There is another example of a custom Lint check here where you can see how to process XML files. The general idea is the same for this as for any other check - extend, specify and override functionality you need for the parts of the code that should be analyzed.

Usage

Your brand new Lint check is ready to be used on a project. If this check can be applied to all projects, you can put it in ~/.android/lint folder (you can create it if it doesn't exist).

Also, you can develop your check as a module in your project and include that module as any other dependency using lintChecks method. This can also be used if you upload jar file to Bintray.

Is it worth the trouble?

Lint is a really nice tool every developer should use. The ability to detect potential issues with your code upfront comes in handy. While custom checks are not the easiest to write, mostly due to the complexity of the API, they’re definitely worth it and can save a lot of time and effort down the road.

48346133-B7BD-45CD-9E24-6CC0D844E9FF
Greetings from our lovely team!
1/4
Achievement unlocked
Resize Master