Linting Android Projects

This page covers essential practices for maintaining and enforcing code quality in Android projects using the Mill build tool. Proper linting helps detect and resolve potential issues early, promoting better performance, security, and user experience.

Linting with Basic Config

This Mill build configuration includes a linting step, which is essential for ensuring code quality and adhering to best practices in Android development projects. Running the androidLintRun task produces a detailed HTML report by default, identifying potential issues in the code, such as performance, security, and usability warnings. This helps maintain the health and quality of the codebase.

In this example, we set a custom config to generate the report in both txt and HTML formats.

build.mill (download, browse)
package build

import mill._
import mill.javalib.android.{AndroidSdkModule, AndroidAppModule, AndroidLintReportFormat}

Create and configure an Android SDK module to manage Android SDK paths and tools.

object androidSdkModule0 extends AndroidSdkModule {
  def buildToolsVersion = "35.0.0"
  def bundleToolVersion = "1.17.2"
}

Actual android application with linting config

object app extends AndroidAppModule {
  def androidSdkModule = mill.define.ModuleRef(androidSdkModule0)
  override def releaseKeyPath = millSourcePath

  // Set path to the custom `lint.xml` config file. It is usually at the root of the project
  def androidLintConfigPath = Task { Some(PathRef(millSourcePath / "lint.xml")) }

  // Set path to the custom `baseline.xml` file. It is usually at the root of the project
  def androidLintBaselinePath = Task { Some(PathRef(millSourcePath / "lint-baseline.xml")) }

  // Set the linting report to be generated as both text and html files
  def androidLintReportFormat =
    Task { Seq(AndroidLintReportFormat.Txt, AndroidLintReportFormat.Html) }

  // Set to true (default), stops the build if errors are found
  def androidLintAbortOnError = false
}
app/src/main/AndroidManifest.xml (browse)
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.helloworld.app" android:versionCode="1" android:versionName="1.0">
    <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="35"/>
    <application android:label="@string/app_name" android:theme="@android:style/Theme.Light.NoTitleBar" android:debuggable="true">
        <activity android:name=".MainActivity"
                android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

The AndroidManifest.xml file shown above is flawed with hardcoded string linting problem so it can be spoted by running the androidLintRun task.

app/src/main/res/values/colors.xml (browse)
<resources>
    <color name="white">#FFFFFF</color>
    <color name="text_green">#34A853</color>
</resources>
app/src/main/res/values/strings.xml (browse)
<resources>
    <string name="app_name">HelloWorldApp</string>
    <string name="hello_world">Hello, World Java!</string>
</resources>
app/src/main/java/com/helloworld/app/MainActivity.java (browse)
package com.helloworld.app;

import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.ViewGroup.LayoutParams;
import android.widget.TextView;

public class MainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Create a new TextView
    TextView textView = new TextView(this);

    // Set the text to the string resource
    textView.setText(getString(R.string.hello_world));

    // Set text size
    textView.setTextSize(32);

    // Center the text within the view
    textView.setGravity(Gravity.CENTER);

    // Set the layout parameters (width and height)
    textView.setLayoutParams(
        new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

    // Set the text color using a resource
    textView.setTextColor(getResources().getColor(R.color.text_green));

    // Set the background color using a resource
    textView.setBackgroundColor(getResources().getColor(R.color.white));

    // Set the content view to display the TextView
    setContentView(textView);
  }
}
app/lint.xml (browse)
<?xml version="1.0" encoding="utf-8"?>
<lint>
    <issue id="MissingApplicationIcon" severity="ignore" />
</lint>

The lint.xml file shown above ignores the MissingApplicationIcon error in the code as the project is for demostration. There are no icons for the demo project.

> ./mill app.androidLintRun

> ls out/app/androidLintRun.dest  # Display path to the linting reports generated. A text and HTML reports
report.html
report.txt

> ls app/ # List out all files in the app directory to check lint-baseline file exists
lint-baseline.xml
lint.xml
src

> cat out/app/androidLintRun.dest/report.txt # Display content of the linting report
AndroidManifest.xml:3: Error: Avoid hardcoding the debug mode; leaving it out allows debug and release builds to automatically assign one [HardcodedDebugMode]

> sed -i.bak 's/ android:debuggable="true"//g' app/src/main/AndroidManifest.xml # Fix the HardcodedDebugMode warning issue from `AndroidManifest.xml`

> ./mill app.androidLintRun # Rerun it for new changes to reflect

> cat out/app/androidLintRun.dest/report.txt # Check the content of report again
../../lint-baseline.xml: Information: 1 errors/warnings were listed in the baseline file (../../lint-baseline.xml) but not found in the project; perhaps they have been fixed? Unmatched issue types: HardcodedDebugMode [LintBaseline]

Linting with Custom Configurations

The commands below are demos on using androidLintRun task with some custom configuratins on a sample project. The demos are adjusting severity level of linting issue, enable and disable linting issue and lastly suppressing linting issue in both XML and Java files. Some output of the changes are shown below.

> sed -i.bak 's/severity="ignore"/severity="warning"/g' app/lint.xml # Set `MissingApplicationIcon` severity level to warning

> ./mill app.androidLintRun # Rerun it for new changes to reflect

> cat out/app/androidLintRun.dest/report.txt # Output the changes in the report
AndroidManifest.xml:3: Warning: Should explicitly set android:icon, there is no default [MissingApplicationIcon]

> sed -i.bak 's/severity="warning"/severity="ignore"/g' app/lint.xml # Revert the severity level of `MissingApplicationIcon`

> sed -i '$s|.*|<!-- Unused resource -->\n<string name="unused_string">This string is unused</string>\n</resources>|' app/src/main/res/values/strings.xml # Add UnusedResources issue

> ./mill app.androidLintRun # Rerun it for the linting tool to detect the UnusedResources

> cat out/app/androidLintRun.dest/report.txt # Output the changes in the report
res/values/strings.xml:5: Warning: The resource R.string.unused_string appears to be unused [UnusedResources]

> sed -i 's|<resources|<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"|' app/src/main/res/values/strings.xml # Suppress linting check for UnusedResources issue

> ./mill app.androidLintRun # Rerun it for new changes to reflect

> cat out/app/androidLintRun.dest/report.txt # Output the report content
0 errors, 0 warnings

> sed -i.bak '/<lint>/a \    <issue id="SyntheticAccessor" severity="error" />' app/lint.xml # Enable SyntheticAccessor issue cause it's disabled by default

> cat app/lint.xml
<?xml version="1.0" encoding="utf-8"?>
<lint>
    <issue id="SyntheticAccessor" severity="error" />
    <issue id="MissingApplicationIcon" severity="ignore" />
</lint>

> sed -i.bak '$a\\nclass SyntheticAccessorViolation {\n  private String privateField = "access me, if you dare";\n\n  static class Inner {\n      static void usePrivateField(SyntheticAccessorViolation instance) {\n          System.out.println(instance.privateField);\n      }\n  }\n}' app/src/main/java/com/helloworld/app/MainActivity.java # Add SyntheticAccessor issue to code

> ./mill app.androidLintRun # Rerun it for new changes to reflect

> cat out/app/androidLintRun.dest/report.txt # Output the report content
java/com/helloworld/app/MainActivity.java:46: Error: Access to private field privateField of class SyntheticAccessorViolation requires synthetic accessor [SyntheticAccessor]

> sed -i.bak -e '/import android.annotation.SuppressLint;/d' -e '/class SyntheticAccessorViolation {/,/^}/d' app/src/main/java/com/helloworld/app/MainActivity.java # Add SuppressLint import

> sed -i.bak '/class SyntheticAccessorViolation {/i @SuppressLint("SyntheticAccessor")' app/src/main/java/com/helloworld/app/MainActivity.java # Add the SuppressLint annotation

> ./mill app.androidLintRun # Rerun it for new changes to reflect

> cat out/app/androidLintRun.dest/report.txt # Output the report content
0 errors, 0 warnings

> sed -i.bak -e '/import android.annotation.SuppressLint;/d' -e '/class SyntheticAccessorViolation {/,/^}/d' app/src/main/java/com/helloworld/app/MainActivity.java # Remove the SyntheticAccessor issue from code

> ./mill app.androidLintRun # Rerun it for new changes to reflect

> cat out/app/androidLintRun.dest/report.txt # Output the report content
0 errors, 0 warnings