Skip to content

iFanie/Intervention

Repository files navigation

Intervention

License Bintray Android Arsenal

Annotation based Android lint check generation

Generate custom Android lint checks and have lint warn you about code you may be dealing with using Kotlin extensions or your own coding conventions.

How is this useful?

We create and annotate the following simple extension functions:

@Intervene(name = "ContentView", warnAgainst = "setContentView")
fun Activity.layout(@LayoutRes layoutRes: Int) {
    this.setContentView(layoutRes)
}

@Intervene(name = "ToastIntervention", warnAgainst = "Toast.makeText")
fun Activity.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, length).show()
}

Having the following sample Activity:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        Toast.makeText(this, "Awesome", Toast.LENGTH_SHORT).show()

        toast("Even more Awesome")
    }
}

Running lint would generate the following report: Intervention report printscreen

The report is detailed and also includes the annotated code that would fix the issue.

How to use?

1. Preparation

a. Lint Module

Custom lint checks in Android need to be created in a Java Module, so go ahead and create one.

File -> New -> New Module... -> Java Library

Intervention generates kotlin code, so you need to apply the Kotlin plugin in the newly created module and instruct the module to use Kotlin generated files. Also, the module must declate the Registry class for the lint checks it contains; the Registry is generated by Intervention. Finally it should have a dependency on Android Lint and the main Intervention project. All in all, the Java module's gradle script should look like this:

apply plugin: 'java-library'
apply plugin: 'kotlin'

/* Sets the JAVA compatibility */
sourceCompatibility = '1.8'
targetCompatibility = '1.8'

/* Uses kotlin generated code in the project */
sourceSets {
    main {
        java {
            srcDir "${buildDir.absolutePath}/generated/source/kaptKotlin/"
        }
    }
}

/* Declares the lint Registry class */
jar {
    manifest {
        attributes("Lint-Registry-v2": "com.izikode.izilib.interventions.InterventionRegistry")
    }
}

/* All dependencies must be compileOnly as per lint regulations */
dependencies {
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.11"
    compileOnly "com.android.tools.lint:lint-api:26.2.1"
    compileOnly "com.izikode.izilib:intervention:0.4"
}

b. Android module

Intervention uses Kotlin code generation, so you must apply the Kapt plugin on your Android module. Also, you must provide the annotation processor with the path to your Java lint module created above. To do this modify your Android gradle script and add the following:

apply plugin: 'kotlin-kapt'

kapt {
    arguments {
        arg('interventionsModuleDir', project(':appinterventions').projectDir.absolutePath)
    }
}
Replace appinterventions with the name of your Java lint module.

You must also add the Intervention dependencies in your Android module, as well as the Java lint module:

dependencies {
    ...
    implementation "com.izikode.izilib:intervention:0.4"
    kapt "com.izikode.izilib:interventioncompiler:0.4"

    lintChecks project(":appinterventions")
}
Again replace appinterventions with the name of your Java lint module.

2. Usage

a. Annotate

Use the Intervene annotation to decorate extension or any other functions for which you want to have lint checks. A simple example is the ToastIntervention from the sample project.

@Intervene(name = "ToastIntervention", warnAgainst = "Toast.makeText")
fun Activity.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, length).show()
}

Another example is the Fragment factory function from the sample project.

class MainFragment : Fragment() {
    companion object {
        @Intervene(name = "MainFragmentInitialization", warnAgainst = "MainFragment()", useInstead = "MainFragment.newInstance()")
        fun newInstance() = MainFragment()
    }
}
  • name is the lint check's name and ID.
  • warnAgainst is the piece of code that when matched will trigger the warning.
  • useInstead is the code that replaces the above. When left empty (default) the compiler will provide the function signature in the report.
  • priority can be either Priority.LOW, Priority.NORMAL (default) or Priority.HIGH.
  • type can be either Type.WARNING (default) or Type.ERROR. Error stops the build.

b. Build and run lint

For a full example, see the sample app.

Licence

Copyright 2018 Fanis Veizis

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.