Android

Android Custom Lint

2020. 7. 28. 15:42

 

이 글은 "AndroIdiots Podcast E18: Custom Lints with Hitanshu Dhawan"(https://medium.com/androidiots/androidiots-podcast-e18-custom-lints-349f0651d458) 을 읽고 그대로 따라해 보면서 몇 가지 보충설명을 담아보았습니다. 이 글의 내용은 이 팀에서 UI 정책 같은 것들 때문에 RadioButton 을 Customize 한 IdiotRadioButton 사용하는데, 만약, RadioButton 을 그대로 사용할 경우, 경고 메시지를 주고, IdiotRadioButton 으로 수정/테스트하는 과정을 담은 것입니다.

 

Lint(린트) : 린트는 소스 코드를 분석하여 프로그래밍 오류, 버그, 문체 오류 및 의심스러운 구성을 표시하는 도구입니다.

 

Step 1. Java/Kotlin 라이브러리 모듈 생성

 

Step 2. Dependency 추가

:LintLibraryModule/build.gradle

dependencies {
    def lintVersion = '26.5.3'
    // Lint
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
}

:app/build.gradle

dependencies {
	lintChecks project(path: ':LintLibraryModule')
}

 

Step 3. IssueRegistry 생성

CustomCodeIssueDetector.ISSUE 는 Step 6 에서 만듭니다.

class CustomIssueRegistry : IssueRegistry() {
    override val issues: List<Issue>
        get() = listOf(
            CustomCodeIssueDetector.ISSUE
        )
}

 

Step 4. IssueRegistry 선언

안드로이드 스튜디오에서도 아래 그림과 같이 경고 팝업으로 알려주게 하려면 (1) gradle 코드를 추가하거나, (2) resources 에 텍스트 파일을 추가해 주어야 합니다.

 

1. gradle 에 추가하는 방법 : package name = com.example 일 경우,

dependencies {
    def lintVersion = '26.5.3'
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    ...
}

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.example.CustomIssueRegistry")
    }
}

2. Resource File 추가 하는 방법 : package name = com.example 일 경우,

 

Step 5. Custom Detector 클래스 만들기

class CustomCodeIssueDetector : Detector()

 

Step 6. Custom Detector Issue 만들기

Issue 를 생성할 때 필요한 값들에 대한 설명은 굳이 하지 않아도 대략 알 수 있도록 네이밍되어 있어 생략합니다. ^^

class CustomCodeIssueDetector : Detector() {
    companion object {
        val ISSUE = Issue.create(
            id = "IdiotRadioButtonUsageWarning",
            briefDescription = "Android's RadioButton should not be used",
            explanation = "Don't use Android Radio button, be an idiot and use IdiotRadioButton instead",
            category = Category.CORRECTNESS,
            priority = 3,
            severity = Severity.WARNING,
            implementation = Implementation(
                CustomCodeIssueDetector::class.java,
                Scope.RESOURCE_FILE_SCOPE
            )
        )
    }
}

 

Step 7.  Scanner 추가하기

class CustomCodeIssueDetector : Detector(), XmlScanner {
    override fun getApplicableElements(): Collection<String> {
        return listOf(
            "RadioButton"
        ) // will look for Radio Button //Text in all xml
    }

    override fun visitElement(context: XmlContext, element: Element) {
        context.report(
            issue = ISSUE,
            location = context.getNameLocation(element),
            message = "Usage of RadioButton is prohibited" // Error message
        )
    }

    companion object {
        val ISSUE = Issue.create(...) // Step 6 참고
    }
}

 

Step 8. Lint (Quick) Fix 추가

 

class CustomCodeIssueDetector : Detector(), XmlScanner {
    override fun getApplicableElements(): Collection<String> { ... } // Step 7 참고

    override fun visitElement(context: XmlContext, element: Element) {
        val idiotRadioButtonFix = LintFix.create()
            .name("Use IdiotRadioButton")
            .replace()
            .text("RadioButton")
            .with("com.androidiots.playground.IdiotRadioButton")
            .robot(true)
            .independent(true)
            .build()

        context.report(...) // Step 7 참고
    }

    companion object {
        val ISSUE = Issue.create(...) // Step 6 참고
    }
}

 

여기까지가 "AndroIdiots Podcast E18: Custom Lints with Hitanshu Dhawan"(https://medium.com/androidiots/androidiots-podcast-e18-custom-lints-349f0651d458) 에서 RadioButton 을 추가할 경우, Lint 를 이용해 경고메시지를 주고, RadioButton IdiotRadioButton 으로 Quick Fix 하는 코드입니다.

 

테스트 코드 작성

1. 테스트를 위한 dependency 추가

:LintLibraryModule/build.gradle

dependencies {
    def lintVersion = '26.5.3'
    ...

    testImplementation "com.android.tools.lint:lint:$lintVersion"
    testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
    testImplementation "junit:junit:4.12"
}

2. 테스트 코드 작성

:LintLibraryModule/src/test/

layout.xml 에서 RadioButton 을 사용하면 Warning 을 표시하는지 확인할 수 있는 코드

class CustomCodeIssueDetectorTest {
    val testInputString = """
            <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
                <RadioButton android:id="@+id/someidlowercase" />
            </LinearLayout>
              """
    @Test
    fun testRadioButton() {
        lint().files(
            xml(
                "res/layout/layout.xml", testInputString
            ).indented())
            .issues(CustomCodeIssueDetector.ISSUE)
            .allowMissingSdk(true)
            .run()
            .expectWarningCount(1)
    }
}

 

 

Lint 관련된 여러 글들을 보면, 안드로이드 그 자체의 Lint 기능이 강력해서 이딴 기능을 굳이 쓸필요가 있느냐하는 식의 멘트들이 많이 있습니다. 이 글의 사례, RadioButton -> IdiotRadioButton 으로 사용하는 것도 그닷 좋은 방법은 아니라 ,예제만 보아도 실제 이런 Lint 가 필요하다고 보기에는 조금 설득력이 떨어집니다. 개발에 참여하는 인원이 적을 수록 별로 의미가 없는 작업이지만, 안드로이드 앱 하나에 4~50명의 개발자가 동시에 개발할 경우, 코드 컨벤션처럼 강력하게 정착시켜야 할 것은 아니지만, 개발자들의 성향(?)에 따라 느슨한 룰같은 것들이 생기기 때문에, 큰 조직이라면 어쩌면 필요할 수도 있겠다는 생각이 듭니다. "Writing a Custom Android UI Inheritance Lint Rule"(https://medium.com/@roderiklagerweij/writing-a-custom-android-ui-inheritance-lint-rule-9af254480399) 에서 나온 것들이 아마도 대표적인 사례(i.e. IncorrectViewId)가 아닐까 싶습니다. 조금 익숙해지신 분들이라면, 글보다는 github(https://github.com/roderiklagerweij/AndroidCustomLint) 코드를 보시는 것이 훨씬 빠를 것입니다.