2018. január 08.

Saját Android Lint szabály írása

10 perc olvasási idő

Saját Android Lint szabály írása

Amikor új kódot írsz, majd tesztelsz, természetesen előfordulnak hibák. Ugyebár egy alkalmazást több API verzión érdemes megnézni. Továbbá, amennyire csak lehetőséged van, különböző gyártók készülékeivel is érdemes tesztelni.

Ebben nekem az a legidegesítőbb, amikor ugyanazt a problémát kell javítanom már megint, mert a kód írásakor nem gondoltam rá vagy más nem számolt vele a csapatban.

Erre példa a beépített Android resource-ok megváltoztatása. Egyes készülékeken a beépített @android:color/transparent az fehér. Ekkor nyilván lehet átírni mindenhol, ahol ez használva van. Az ilyen jellegű szívást szerettem volna megspórolni saját Lint tesztek írásával.

A statikus analízisről írtam már, aminek fontos része volt a Lint. Ehhez egyedi szabályokat is lehet készíteni, hogy turkálj a res mappában lévő fáljaidban.

Emlékeztető

A Lint tulajdonképpen a kód futtatása nélkül keres hibákat, de Androidra hegyezve. Több mint 300 szabályt tartalmaz jelenleg. A legújabb bétájában már minden Java/Kotlin kódon futtatható.

Egyik legnagyobb előnye, hogy nem csak programkódot vizsgál, hanem többfajta fájlt, akár egyszerre is:

  • .xml, AndroidManifest
  • .jpg, .png, .webp
  • .class
  • .kt, .java
  • .cfg

Így például lehet ellenőrizni, hogy az XML-ben létrehozott felülethez megfelelő osztályokat használsz a forráskódban.

Meglepetés

Használatakor el kell fogadni, hogy nincs dokumentáció. Ez azért van, mert az API nem végleges és folyamatosan változik.

A legjobb információkat blog postokban, GitHub repoban, vagy előadásokban találni. Továbbá fejlesztés közben a forráskódban is érdemes turkálni, mert van komment a legtöbb helyen. Van egy aktív lint-dev fórum is, ahol tudsz segítséget kérni, ha elakadsz.

Ajánlott a meglévő szabályokra építkezni az Android forráskódból. Ezek példát is nyújtanak, ha esetleg nem megy valami.

Meggondolandó Kotlin nyelven írni a szabályokat. A Lint egy része is ezen a nyelven van írva és ki lehet használni például plusz annotációkat. A lenti kódok egyébként Javaban vannak.

Gyorstalpaló

Egy Lint szabály 4 nagyobb részre bontható fel:

  • Issue
    Leírás a hibáról és szabályról.
  • Detector
    Ez keresi a hibát. Hogy gyorsabb legyen egy detectorban több Issue-t is lehet keresni és specializált detectorok vannak. Például a ResourceXmlDetector Xml fájlokban keres.
  • Implementation
    Itt kell megadni, hol keresed a hibát és mivel, azaz a használt detectort.
  • Registry
    Egy nyilvántartás, lista az issue-król. Ez alapján fogja futtatni a szabályokat a Lint.

Ugrás a vízbe

Egy példán keresztül mutatom meg, hogy épül fel egy Lint szabály. Ez a „ne használj beépített Android színeket” kezdetleges változata. A githubon is eléred a lenti forráskódokat.

Egy külön modulba/projectbe kell pakolni a szabályokat, így könnyebben újrafelhasználhatóak. Először a gradle.build fájl felépítését vizsgáljuk meg:


apply plugin: 'java-library'

dependencies {
    compileOnly 'com.android.tools.lint:lint-api:26.0.1'
    testCompileOnly "com.android.tools.lint:lint:26.0.1"
    testCompileOnly "com.android.tools.lint:lint-tests:26.0.1"
    testCompileOnly "com.android.tools:testutils:26.0.1"
    testCompileOnly "junit:junit:4.12"
}

jar {
    manifest {
        attributes("Lint-Registry-v2": "hu.fps.lintrules.IssueRegistry")
    }
}

Elég a java-library plugint használni. Nem lesznek Androidos függőségek, fájlok és nem kell AndroidManifest.xml. Egy jar fájlt fog generálni a project, ami a Registry-re mutat. Ahogy említettem ott vannak felsorolva a szabályok, azaz issue-k.

A Lint 26.0.1-es verzióját érdemes használni. Ebben már lintChecks konfigurációval be lehet húzni a szábályt a project függőségei közé. A régebbi verzióknál lint.jar fájl kell másolgatni egy Gradle task segítségével.

Az IssueRegistry osztályra még később visszatérek. Először a szabályt kell létrehozni:


public class ColorDetector extends ResourceXmlDetector {

A szabályoknak ki kell terjeszteniük a Detector osztályt és implementálniuk a Scanner interfészt. Ez a szabály a Lintbe beépített ResourceXmlDetector osztályt használja, ami kiterjeszti a Detector-t és implementál egy Detector.XmlScanner interfészt. Ez a beépített osztály XML fájlokat vizsgál.


private static final Implementation IMPLEMENTATION = new Implementation(
            ColorDetector.class, //Ami keres
            Scope.MANIFEST_AND_RESOURCE_SCOPE); //Ahol keresünk

public static final Issue ISSUE = Issue.create(
            "AndroidColorDetector", //azonosító
            "Using Android color resources", //rövid leírás
            "Android color resources like android:color/white should not be used." +
                    " Manufacturers tend to overwrite them.", //leírás
            Category.CORRECTNESS, //kategória
            7, //prioritás
            Severity.ERROR, //súlyosság
            IMPLEMENTATION);
            

Létrehozod benne az issue-t és az implementációt. Az issue-ban lévő adatok megjelennek majd a riportban, ezért fontosak. A Severity (súlyosság) beállítására is oda kell figyelni. A Fatal, ami a legrosszabb, például megakadályozza az APK csomagolását.

Implementációnál megadod a Detector osztályt és azt, hogy hol folyik majd a hiba keresése. Az @android:color/ érték előfordulhat a manifestben és a resource mappában lévő xml fájlokban, ezért ott keresed majd.

Jöhet a vizsgálat, azaz mégse, mert tudsz teszteket írni és érdemes is:


public class ColorDetectorTest extends LintDetectorTest {

A ColorDetectorTest osztály kiterjeszti a beépített LintDetectorTest-et. Ez JUnit 3, tehát minden metódusnak „test”-el kell kezdődnie, nincsenek annotációk.


@Override
protected Detector getDetector() {
    return new ColorDetector();
}

@Override
protected List getIssues() {
    return new IssueRegistry().getIssues();
}

Ezek a metódusok kötelezőek. Egyik a Detectort másik az Issue-t adja vissza.


 public void testShouldDetectNoWarning() {
    lint().files(
            manifest("android:icon=\"@mipmap/ic_launcher\"")
    ).allowMissingSdk()
    .run()
    .expectClean();
}

public void testShouldDetectWarning() {
    lint().files(
            manifest("android:icon=\"@android:color/white\"")
    ).allowMissingSdk()
    .run()
    .expectErrorCount(1)
    .expect("");
}

DSL teszt API van Linthez, ami megkönnyíti a tesztelést, természetesen dokumentáció nélkül teszi mindezt. Egyik tesztben sikeres eredményt várunk el, míg a másikban sikertelent.

Az elvárt eredményt is meg kell adni hibás esetben, amit addig nem lehet tudni, míg nem írod meg. Ha ezt megírjuk, akkor a teszt futtatása után látni lehet majd és csak át kell csak másolni a tesztbe. Szóval ez nem gond.

A fenti kódban a manifest nagy része nincs benne, de githubon megtekintheted.

Ha megvannak a tesztek, akkor lehet írni a szabályt a ColorDetector osztályban:


@Override
public Collection getApplicableAttributes() {
    return ALL;
}

@Override
public void visitAttribute(XmlContext context, Attr attribute) {
    if (attribute.getValue().contains(SdkConstants.ANDROID_COLOR_RESOURCE_PREFIX)) {
        context.report(ISSUE, attribute, context.getLocation(attribute), "Using Android color resources are not recommended. Manufacturers are overriding them with other colors.");
    }
}

A vizsgálat 2 részből áll. A getApplicableAttributes() metódussal megmondod, melyik attribútumokat szeretnéd vizsgálni, majd a visitAttribute() metódusban megvizsgálod ezeket.

Mivel túl sok attribútum van, ahol szín szerepelhet, ezért nem lettek felsorolva. Az attrs.xml fájlba kell beleírni, hogy egy view attribútuma mit tartalmazhat. A beépített attrs.xml hatalmas. Bármelyik elem, amelyik color vagy reference lehet azt vizsgálni kéne. Ha nagyon fontos a sebesség, akkor még a leggyakrabban használtakat elég lenne ellenőrizni.

A visitAttribute() metódusban van a vizsgálat. Ha az attribútum értéke tartalmazza az @android:color/ Stringet, akkor jelentünk egy hibát. „Jelentünk” a megfelelő szó, mert ez lehet egy tooltip lesz az Android Studio-ban, vagy a generált HTML jelentésben lesz csak benne. A hívásnak átadjuk az Issue-t, a hiba hatáskörét jelző attribútumot, annak helyzetét és a hiba leírását.

Ahhoz, hogy megtalálja a Lint ezt a szabályt, kell egy Registry:


public class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {
    @Override
    public List getIssues() {
        return Arrays.asList(
                ColorDetector.ISSUE
        );
    }
}

A Lint IssueRegistry osztály getIssues() metódusát írjuk felül. Itt adod meg a szabályok listáját. Ennek az osztálynak kell szerepelnie a build.gradle fájlban is.

Használat

Ezután a lintChecks konfigurációval lehet berakni a függőségek közé. Én felraktam GitHubra majd JitPack segítségével hivatkoztam rá a projectünkben:


buildscript {
    repositories {
        maven { url "https://jitpack.io" }
    }
}

dependencies {
    lintChecks 'com.github.fpshu:android-lintrules:master-SNAPSHOT'
}

Így bármelyik projektbe be tudom könnyen illeszteni.

Zárás

Ez a kis szabály nem teljes, ugyanis kódban és styles.xml-ben nem keres. A fentiek alapján viszont könnyen kiegészíthető és építhetőek rá más szabályok.

Már írtam is hozzzá egy másik fájdalmasat, ami a drawable xml fájlokat vizsgál. Minden shape típusú drawable fájlnak kötelező hátteret adni <solid> taggel, kivéve, ha gradiens. Ha nem teszed, akkor a gyártó által megadott alapértelmezett hátteret fogja használni, ami lehet fekete, átlátszó vagy bármi.
Ez már segített is felfedezni egy hibát anélkül, hogy futtatni kellett volna az alkalmazást, így biztosan megérte.

Vaszil Ádám

Már sok-sok éve szeretne programozó lenni, de mivel ennyi nyelvet nehéz megtanulni, ezért inkább csak a Javara koncentrál.

Vaszil Ádám

Hozzászólások