[Android]ConstLayntLayout内でmatchParentを使用した場合に警告するカスタムLintを作成する
はじめに
DroidKaigi 2021にて行われた下記の発表に触発されて
DroidKaigi 2021 – 25分で作るAndroid Lint / Nozomi Takuma [JA] – YouTube
25分で作るAndroid Lint / Android Lint made in 25 minutes – Speaker Deck
PRレビューでよく指摘していたことをLintにできたらいいなと思い、上記を参考に自分もAndroidのカスタムLintを作成してみました。
Lint化しておけばローカルの開発環境や、CI上でDangerを使用してPR作成後に自動でLintチェックを行い、レビュアーが指摘せずとも間違いに気づける環境が構築できるという狙いがあります。
カスタムLint作成の準備
まずは発表内容を参考にカスタムLint作成時に必要なものが揃っているので下記のリポジトリをベースに作成していきます。
今回はConstraintLayout内の子Viewに
android:layout_width="match_parent"
もしくは android:layout_height="match_parent"
が設定されている場合に警告するLintを作成してみます。
ConstraintLayout内の子Viewにmatch_parentを使っては行けない理由は公式ドキュメントにかかれているのでそちらを参考にしてください。
ConstraintLayout | Android Developers
layout.xmlに対するLintの作成方法は発表内容やドキュメントだけだとわかりにくいので
標準のConstraintLayoutのLintのコードを参考にしながら作成しました。
ConstraintLayoutDetector.kt – Android Code Search
layout.xmlに対するカスタムLintの作成
まずはカスタムLintの本体となるLayoutDetectorを実装していきます。
appliesToやgetApplicableElementsでLintを適用する対象を絞っています。
visitElementで対象を実際にLintチェックするコードを実装します。
最後にLintチェックに引っかかったらcontext.reportでIssueを報告するようにします。
package com.example.lint.checks.detector
import com.android.SdkConstants.*
import com.android.resources.ResourceFolderType
import com.android.tools.lint.detector.api.*
import com.android.utils.forEach
import org.w3c.dom.Element
import org.w3c.dom.Node
class ConstraintLayoutDetector : LayoutDetector() {
// layoutファイルだけ検出するように設定
override fun appliesTo(folderType: ResourceFolderType) =
(folderType == ResourceFolderType.LAYOUT)
// ConstraintLayoutの要素だけ検出するように設定
override fun getApplicableElements() = setOf(
CONSTRAINT_LAYOUT.oldName(),
CONSTRAINT_LAYOUT.newName()
)
// 検出したConstraintLayoutに対してLintCheckを実行
override fun visitElement(context: XmlContext, element: Element) {
var child = element.firstChild
while (child != null) {
if (child.nodeType != Node.ELEMENT_NODE) {
child = child.nextSibling
continue
}
// ConstraintLayout内の子Viewの設定をチェックしていく
child.attributes.forEach { attribute ->
val name = attribute.localName ?: return@forEach
val value = attribute.nodeValue
// android:layout_width or android:layout_heightにmatch_parentが含まれているかチェック
if (name != ATTR_LAYOUT_WIDTH && name != ATTR_LAYOUT_HEIGHT) return@forEach
if (value != VALUE_MATCH_PARENT) return@forEach
// Lintチェックに引っかかったのでreportする
context.report(
issue = ISSUE_USE_MATCH_PARENT_IN_CONSTRAINT_LAYOUT,
location = context.getLocation(element),
message = ISSUE_USE_MATCH_PARENT_IN_CONSTRAINT_LAYOUT.getExplanation(TextFormat.TEXT)
)
return
}
child = child.nextSibling
}
}
companion object {
// Lintで検知した際に表示する内容などを設定する
@JvmStatic
internal val ISSUE_USE_MATCH_PARENT_IN_CONSTRAINT_LAYOUT = Issue.create(
id = "UseMatchParentInConstraintLayout",
briefDescription = "Similar behavior can be defined by using MATCH_CONSTRAINT with the corresponding left/right or top/bottom constraints being set to \"parent\"",
explanation = "MATCH_PARENT is not recommended for widgets contained in a ConstraintLayout.",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
implementation = Implementation(
ConstraintLayoutDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
),
androidSpecific = true
).addMoreInfo("https://developer.android.com/reference/androidx/constraintlayout/widget/ConstraintLayout")
}
}
また、カスタムLint開発時はテストコードでデバッグしながらやるとわかりやすいです。
package com.example.lint.checks
import com.android.tools.lint.checks.infrastructure.TestFiles.xml
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import com.example.lint.checks.detector.ConstraintLayoutDetector
import org.junit.Test
@Suppress("UnstableApiUsage")
class XmlDetectorTest {
@Test
fun testXML() {
lint()
.files(
xml(
"res/layout/sample.xml",
"""
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
<EditText
android:id="@+id/message_1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="MainFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/message_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MainFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
""".trimIndent()
).indented()
)
.issues(ConstraintLayoutDetector.ISSUE_USE_MATCH_PARENT_IN_CONSTRAINT_LAYOUT)
.allowMissingSdk()
.run()
.expectErrorCount(1)
}
}
あとは発表内容にもあるように
作成したカスタムLintようのIssueRegistryの作成や
src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
リソースの追加を行えばカスタムLintモジュールが完成します。
作成したLintを導入する
作成したカスタムLintモジュールを対象のプロジェクトに導入してappのbuild.gradleにlintChecksで追加したLintCheckモジュールを参照します。
// build.gradle(app)
dependencies {
// Custom Lint Check
lintChecks project(':lint-checks')
}
これで対象のプロジェクトで作成したカスタムLintが機能するようになります。
まとめ
コードレビューで何度も指摘することを検出するカスタムLintを作成することでレビューの負荷が減らすことができます。
また、特定のクラスを参照しないように検知するといったプロジェクト固有のルールを適用するLintも作成できます。
これにより複数人での開発でもプロジェクトがカオスになっていくのを防ぐことができます。
カスタムLintの書き方はドキュメントも少なく分かりづらいですが、
既存Lintのコードを参考にすればなんとか作成していけると思います。