【Kotlin/Android】Spotlessでktlintを使用してコードフォーマットを実装する

【Kotlin/Android】Spotlessでktlintを使用してコードフォーマットを実装する

この記事からわかること

  • Kotlin/AndroidSpotlessktlintを使用してコードフォーマット実装する方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Spotlessとは

公式リファレンス:spotless/plugin-maven

Spotless(スポットレス)」はGradle用の静的解析(Lint) & コード整形(Format)ツールのプラグインです。Java / Kotlin / Groovy / Scala / Markdown / XMLなどの複数言語に対応しています。コード整形はJavaであれば「Google Java Format」、Kotlinであれば「ktlint」などをSpotlessと連携させることでSpotlessがフォーマットを呼び出す形で活用されます。

ktlintと一緒に導入する

実際にKotlinベースのプロジェクトで活用できるように実装してみます。「build.gradle.kts(Project)」に対して以下のように記述します。


plugins {

    /** Spotless */
    id("com.diffplug.spotless") version "7.2.1"
}

// Spotless + ktlint の設定
// 解析: ./gradlew spotlessCheck
// 反映: ./gradlew spotlessApply
spotless {
    kotlin {
        // ----- Kotlin ファイルに対する整形ルール -----

        // フォーマット対象のファイルを指定
        target("**/*.kt")

        // ktlint のバージョンを指定
        ktlint("0.50.0")
            // ktlint の設定を上書き(以下は例)
            .editorConfigOverride(
                mapOf(
                    // インデントのスペース数を指定(4スペース)
                    "indent_size" to "4",
                    // ファイル末尾に改行を強制
                    "insert_final_newline" to "true"
                )
            )
    }

    kotlinGradle {
        // ----- build.gradle.kts ファイルに対する整形ルール -----
        // フォーマット対象の Gradle Kotlin DSL ファイルを指定
        target("**/*.gradle.kts")
        ktlint("0.50.0")
    }
}

解析(Check)をかけてみる

導入が完了したら実際にコードに対して解析をかけてみます。コマンドラインから./gradlew spotlessCheckを実行すると解析結果が表示されます。成功すればBUILD SUCCESSFUL、失敗(違反があれば)すればBUILD FAILEDになります。

$ ./gradlew spotlessCheck

実際にプロジェクトで実行してBUILD FAILEDになると以下のようなログが出力されます。

$ ./gradlew spotlessCheck
> Task :spotlessKotlinCheck FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':spotlessKotlinCheck'.
> The following files had format violations:
      app/src/main/java/com/XXXXXX/XXXXXXX/repositories/di/RepositoryModule.kt
          @@ -21,11 +21,11 @@
           
           ····@Provides·//·対象のインターフェースにどの実態を渡すかを定義する
           ····fun·provideSeriesRepository(
          -········impl:·RoomSeriesRepositoryImpl
          +········impl:·RoomSeriesRepositoryImpl,
           ····):·SeriesRepository·=·impl
           
           ····@Provides
           ····fun·provideImageFileRepository(
          -········impl:·ImageFileRepositoryImpl
          +········impl:·ImageFileRepositoryImpl,
           ····):·ImageRepository·=·impl
          -}
          +}
      app/src/main/java/com/XXXXXX/XXXXXXX/services/ImageService.kt
          @@ -19,7 +19,7 @@
           ········return·FileProvider.getUriForFile(
           ············context,
           ············"${context.packageName}.provider",
          -············photoFile
          +············photoFile,
           ········)
           ····}
           }
      app/src/main/java/com/XXXXXX/XXXXXXX/ui/theme/Color.kt
          @@ -17,4 +17,4 @@
           //·======·ダークモード·======
           val·ExTextDark·=·Color(0xFFFFFFFF)
           val·ExBaseDark·=·Color(0xFF222222)
          -val·ExFoundationDark·=·Color(0x000000)
          +val·ExFoundationDark·=·Color(0x000000)
      app/src/main/java/com/XXXXXX/XXXXXXX/ui/theme/Theme.kt
          @@ -11,7 +11,7 @@
           ····onPrimary·=·ExWhite,
           ····primaryContainer·=·ExBaseDark,
           ····background·=·ExFoundationDark,
          -····onBackground·=·ExTextDark
          +····onBackground·=·ExTextDark,
           )
           
           private·val·LightColorScheme·=·lightColorScheme(
          @@ -19,13 +19,13 @@
           ····onPrimary·=·ExWhite,
           ····primaryContainer·=·ExBase,
           ····background·=·ExFoundation,
          -····onBackground·=·ExText
          +····onBackground·=·ExText,
           )
      ... (18 more lines that didn't fit)
  Violations also present in:
      app/src/main/java/com/XXXXXX/XXXXXXX/ui/theme/Type.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/components/layout/HeaderView.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/mydata/MyDataScreen.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/series/SeriesDetailScreen.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/series/SeriesListScreen.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/series/input/CategoryInputScreen.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/series/input/SeriesInputScreen.kt
      app/src/main/java/com/XXXXXX/XXXXXXX/views/settings/SettingsScreen.kt
  Run './gradlew :spotlessApply' to fix these violations.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 8s

自動整形(Format)をかける

違反していた内容に対して自動整形(フォーマット)をかけるには./gradlew spotlessApplyを実行します。こちらも整形が全て完了すればBUILD SUCCESSFUL、失敗(完了しなければ)すればBUILD FAILEDになります。違反の中には自動整形で修正しきれないものもあるのでその場合は手動で修正してあげる必要があります。

$ ./gradlew spotlessApply

実際にプロジェクトで実行してみると以下のような感じになります。

$ ./gradlew spotlessApply
> Task :spotlessKotlinApply FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':spotlessKotlinApply'.
> There were 34 lint error(s), they must be fixed or suppressed.
  app/src/androidTest/java/com/XXXXXX/XXXXXXX/ExampleInstrumentedTest.kt:L5 ktlint(standard:no-wildcard-imports) Wildcard import
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Dao/CapsuleToyDao.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Dao/CategoryDao.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Dao/LocationDao.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Dao/SeriesCategoryCrossRefDao.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Dao/SeriesDao.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Database/AppDatabase.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Entity/CapsuleToy.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Entity/Category.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Entity/Location.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Entity/Relation/SeriesCategoryCrossRef.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Entity/Relation/SeriesWithRelations.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/Entity/Series.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Domain/RoomConverters.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/models/Enum/AppScreen.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/repositories/impl/ImageFileRepository.kt:L1 ktlint(standard:filename) File 'ImageFileRepository.kt' contains a single class and possibly also extension functions for that class and should be named same after that class 'ImageFileRepositoryImpl.kt'
  app/src/main/java/com/XXXXXX/XXXXXXX/repositories/impl/RoomSeriesRepository.kt:L1 ktlint(standard:filename) File 'RoomSeriesRepository.kt' contains a single class and possibly also extension functions for that class and should be named same after that class 'RoomSeriesRepositoryImpl.kt'
  app/src/main/java/com/XXXXXX/XXXXXXX/repositories/repository_interface/ImageRepository.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/repositories/repository_interface/SeriesRepository.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/view_models/CategoryInputScreenViewModel.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/view_models/RootEnvironment.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/view_models/SeriesDetailScreenViewModel.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/view_models/SeriesInputScreenViewModel.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/view_models/SeriesListScreenViewModel.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/common/App.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/views/common/MainActivity.kt:L1 ktlint(standard:package-name) Package name contains a disallowed character
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/CustomAlertDialog.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/CustomText.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/DataEmptyView.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/ThemeIconButton.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/ThemeInputBox.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/ThemeTextFiled.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/main/java/com/XXXXXX/XXXXXXX/views/components/ui_parts/WhiteBackStackView.kt:L1 ktlint(standard:package-name) Package name must not contain underscore
  app/src/test/java/com/XXXXXX/XXXXXXX/ExampleUnitTest.kt:L3 ktlint(standard:no-wildcard-imports) Wildcard import
  Resolve these lints or suppress with `suppressLintsFor`

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 9s

違反の種類を制限する

発生した違反に関してはktlint側の設定を変更することで違反を制限することが可能です。

ファイル単位で無視する

@file:Suppress("ktlint:standard:no-wildcard-imports")

package com.XXXXXXXX.XXXXXXXX.models.domain.entity

import com.XXXXXXXX.XXXXXXXX.models.domain.entity.* // これも無視される

行単位で無視する

// ktlint-disable no-wildcard-imports
import com.XXXXXXXX.XXXXXXXX.models.domain.entity.* // これだけ無視される

行範囲で無視する

// ktlint-disable no-wildcard-imports
import com.XXXXXXXX.XXXXXXXX.models.domain.entity.*
// ktlint-enable no-wildcard-imports

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index