【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件

そもそも Twitter Jetpack Compose Rules が話題になったのは2、3年前、

その後の commits が過疎とは思っていましたが。

【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件
👉 twitter/compose-rules: Static checks to aid with a healthy adoption of Compose hatena-bookmark

issues を見ていると以下のようなコメンツ。

Since both @mrmans0n and @chrisbanes left Twitter (sorry guys 😢), the future doesn't look so bright. At least not under Twitter's scope. Maybe this project can find a new home at Google, for instance?

👉 The future of this project · Issue #112 · twitter/compose-rules hatena-bookmark

I started my fork in https://github.com/mrmans0n/compose-rules

First order of business was supporting ktlint 0.48.x and kotlin 1.8.x. I submitted them as a PR in this repo too, to ease with transition, but I doubt anybody is looking on the Twitter side (all the admins of this repo don't work at the company any longer, and our permissions were removed).

I will work towards a maven central publication for my fork as soon as possible.

👉 The future of this project · Issue #112 · twitter/compose-rules hatena-bookmark

しれっと、フォークが元の管理者によって進化を続けています。

【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件

Note
This repository is a fork of the Twitter Jetpack Compose Rules by its original maintainer. As none of the admins/maintainers continue working at the company, its development will continue here from now on. If you come from that project, check out the migration guide.

注意
このリポジトリは、元の管理者による Twitter Jetpack Compose Rules のフォークです。会社の管理者/保守者がもはや働いていないため、今後はこちらで開発が続行されます。もしあなたがそのプロジェクトから来た場合、移行ガイドをチェックしてください。

👉 mrmans0n/compose-rules: Static checks to aid with a healthy adoption of Compose. Maintained fork of the Twitter Compose rules. hatena-bookmark

使ってみると以下のような新しいルールで怒られる。


dependencies {
    detektPlugins "io.nlopez.compose.rules:detekt:<VERSION>"
}

The main Modifier of a @Composable should be applied once as a first modifier in the chain to the root-most layout in the component implementation.

You should move the modifier usage to the appropriate parent Composable.

See https://mrmans0n.github.io/compose-rules/rules/#modifiers-should-be-used-at-the-top-most-layout-of-the-component for more information. [ModifierNotUsedAtRoot]

@Composable のメインの Modifier は、コンポーネントの実装内の最もルートに配置されたレイアウトに対して最初の Modifier として一度だけ適用すべきです。Modifier の使用を適切な親の Composable に移動すべきです。

という感じで、Twitter オリジナル版では見つからなかった Modifier 使いまわし部分の違反が見つかります。

良さげです。

変更された rule をざっくり config.yml で確認する。

【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件
👉 Comparing da89b71..027a205 · mrmans0n/compose-rules hatena-bookmark

👉 compose-rules/rules/detekt/src/main/resources/config/config.yml at main · mrmans0n/compose-rules hatena-bookmark
👉 compose-rules/rules/detekt/src/main/resources/config/config.yml at main · twitter/compose-rules hatena-bookmark

新しく追加されたルールを見てみます、

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 DefaultsVisibility - ComponentDefaults object should match the composable visibility

ComponentDefaults object should match the composable visibility
If your composable has an associated Defaults object to contain its default values, this object should have the same visibility as the composable itself. This will allow consumers to be able to interact or build upon the original intended defaults, as opposed to having to maintain their own set of defaults by copy-pasting.

ComponentDefaults オブジェクトはコンポーザブルの可視性に合わせるべきです。
コンポーザブルに関連付けられたデフォルト値を保持する Defaults オブジェクトがある場合、このオブジェクトはコンポーザブル自体と同じ可視性を持つべきです。これにより、コンシューマーは元々意図されたデフォルト値に対して対話したり、それを拡張したりすることができ、コピーアンドペーストで独自のデフォルト値セットを保守する必要がなくなります。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 ModifierClickableOrder - Modifier order matters

Modifier order matters
The order of modifier functions is very important. Each function makes changes to the Modifierreturned by the previous function, the sequence affects the final result. Let's see an example of this:

修飾子の順序が重要です
修飾子関数の順序は非常に重要です。各関数は前の関数によって返された Modifier に変更を加え、その順序が最終的な結果に影響します。これについて例を見てみましょう:


@Composable
fun MyCard(modifier: Modifier = Modifier) {
    Column(
        modifier
            // Tapping on it does a ripple, the ripple is bound incorrectly to the composable
            .clickable { /* TODO */ }
            // Create rounded corners
            .clip(shape = RoundedCornerShape(8.dp))
            // Background with rounded corners
            .background(color = backgroundColor, shape = RoundedCornerShape(8.dp))
    ) {
        // rest of the implementation
    }
}

The entire area, including the clipped area and the clipped background, responds to clicks. This means that the ripple will fill it all, even the areas that we wanted to trim from the shape.

We can address this by simply reordering the modifiers.

切り抜かれた領域と切り抜かれた背景を含む、全体のエリアがクリックに反応します。つまり、リップルは、形状からトリミングしたい領域も含めて、全体を満たします。

この問題は、修飾子を単に再順序化することで解決できます。


@Composable
fun MyCard(modifier: Modifier = Modifier) {
    Column(
        modifier
            // Create rounded corners
            .clip(shape = RoundedCornerShape(8.dp))
            // Background with rounded corners
            .background(color = backgroundColor, shape = RoundedCornerShape(8.dp))
            // Tapping on it does a ripple, the ripple is bound incorrectly to the composable
            .clickable { /* TODO */ }
    ) {
        // rest of the implementation
    }
}

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 ModifierNaming - Naming modifiers properly

Naming modifiers properly
Composables that accept a Modifier as a parameter to be applied to the whole component represented by the composable function should name the parameter modifier.

In cases where Composables accept modifiers to be applied to a specific subcomponent should name the parameter xModifier (e.g. fooModifier for a Foo subcomponent) and follow the same guidelines above for default values and behavior.

適切な修飾子の命名
修飾子を受け入れるコンポーザブルは、そのコンポーザブル関数によって表されるコンポーネント全体に適用されるためのパラメータに modifier という名前を付けるべきです。

コンポーザブルが特定のサブコンポーネントに適用する修飾子を受け入れる場合、そのパラメータには xModifier(たとえば Foo サブコンポーネントの場合は fooModifier)という名前を付け、デフォルト値と動作に関して上記のガイドラインに従うべきです。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 ModifierNotUsedAtRoot - Modifiers should be used at the top-most layout of the component

Modifiers should be used at the top-most layout of the component
Modifiers should be applied once as a first modifier in the chain to the root-most layout in the component implementation. Since modifiers aim to modify the external behaviors and appearance of the component, they must be applied to the top-most layout and be the first modifiers in the hierarchy. It is allowed to chain other modifiers to the modifier passed as a param if needed.

Modifier はコンポーネントの最上位レイアウトで使用するべきです
Modifier はコンポーネントの外部の動作との外観を変更することを目的としているため、最上位のレイアウトに適用され、階層の最初の Modifier に配置される必要があります。必要であれば、他の Modifier パラメーターとして、渡された Modifier に連鎖させることは許容されています。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 PreviewAnnotationNaming - Naming multipreview annotations properly

Properly Naming Multipreview Annotations
Multipreview annotations should be named using "Previews" as a prefix. These annotations must be explicitly named to ensure they are easily distinguishable as an alternative to the @Preview when used.

適切な名前付け:マルチプレビューアノテーション
マルチプレビューアノテーションは、接頭辞として「Previews」を使用して命名すべきです。これらのアノテーションは、使用時に @Preview の代替として明確に識別できるように、明示的に名前をつける必要があります。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 まとめ

どこまで管理が続くかは心配ですが、とりあえず今は良い感じです。

👉 【AndroidStudio】detekt で JetpackCompose 記述をチェックする hatena-bookmark
👉 【Gradle Plugin】detekt「baseline」とは hatena-bookmark


【Gradle Plugin】detekt「baseline」とは

何のことか分からないまま遊んでいたら、ルール違反が検出されなくなった。

毎度のことながら、

気づいてからドキュメントに書いてあることが分かる。

Code Smell Baseline
👉 Code Smell Baseline | detekt hatena-bookmark

まあ、そんなに隅々まで読まないわな。

なので簡単にメモ。

 

🧑‍💻「baseline」とは


./gradlew detekt

で「code smell」が検出されたときに、それらを baseline に保存しておくと、次からそれらは検出されなくなる。

「検出のベースライン」ということなのだろう。

 

🧑‍💻 baseline ファイル

ファイル位置を指定しておくと分かりやすい。


// build.gradle.kts

detekt {
  baseline = file("$rootDir/config/detekt/baseline.xml")
}


❯ ./gradlew tasks | grep detekt | grep -v EXPERIMENTAL
detekt
detektBaseline - Creates a detekt baseline on the given --baseline path.
detektGenerateConfig - Generate a detekt configuration file inside your project.

検出後、./gradlew detektBaseline で指定した位置に書き出される。


❯ ./gradlew detekt             

> Task :app:detekt FAILED
~(略)~ The function EditBar(target: TargetTodo, focusRequester: FocusRequester, onDone: KeyboardActionScope.() -> Unit, onClear: () -> Unit, onValueChange: (TextFieldValue) -> Unit, onAdd: () -> Unit, onUpdate: () -> Unit, onDelete: () -> Unit, modifier: Modifier) has too many parameters. The current threshold is set to 6. [LongParameterList]
~(略)~ The function TodoScreen is too long (99). The maximum length is 60. [LongMethod]
~(略)~ The function MainScreen is too long (63). The maximum length is 60. [LongMethod]

FAILURE: Build failed with an exception.

❯ ./gradlew detektBaseline                            

BUILD SUCCESSFUL in 510ms

❯ cat config/detekt/baseline.xml                                            
<?xml version="1.0" ?>
<SmellBaseline>
  <ManuallySuppressedIssues></ManuallySuppressedIssues>
  <CurrentIssues>
    <ID>LongMethod:MainScreen.kt$@Composable fun MainScreen( modifier: Modifier = Modifier )</ID>
    <ID>LongMethod:TodoScreen.kt$@Composable fun TodoScreen( modifier: Modifier = Modifier, viewModel: TodoViewModel = hiltViewModel() )</ID>
    <ID>LongParameterList:TodoScreen.kt$( target: TargetTodo, focusRequester: FocusRequester, onDone: KeyboardActionScope.() -&gt; Unit, onClear: () -&gt; Unit, onValueChange: (TextFieldValue) -&gt; Unit, onAdd: () -&gt; Unit, onUpdate: () -&gt; Unit, onDelete: () -&gt; Unit, modifier: Modifier = Modifier )</ID>
  </CurrentIssues>
</SmellBaseline>

ここで検出しようとすると、何も検出されない。


❯ ./gradlew detekt              

BUILD SUCCESSFUL in 582ms

削除すると検出されるようになる。


❯ rm config/detekt/baseline.xml

❯ ./gradlew detekt             

> Task :app:detekt FAILED
~(略)~ The function EditBar(target: TargetTodo, focusRequester: FocusRequester, onDone: KeyboardActionScope.() -> Unit, onClear: () -> Unit, onValueChange: (TextFieldValue) -> Unit, onAdd: () -> Unit, onUpdate: () -> Unit, onDelete: () -> Unit, modifier: Modifier) has too many parameters. The current threshold is set to 6. [LongParameterList]
~(略)~ The function TodoScreen is too long (99). The maximum length is 60. [LongMethod]
~(略)~ The function MainScreen is too long (63). The maximum length is 60. [LongMethod]


FAILURE: Build failed with an exception.

 

🧑‍💻 まとめ

キャッシュが効いてるのかと思ってあれこれやっていたが、この「baseline」だった。

「最初からドキュメントを隅々まで読んでおけば良かった。」などは言わない。

👉 【AndroidStudio】detekt で JetpackCompose 記述をチェックする hatena-bookmark


ファイルの末尾に改行を追加するように Android Studio を設定するにはどうすればよいですか?

🧑‍💻 NewLineAtEndOfFile

👉 【AndroidStudio】detekt で JetpackCompose 記述をチェックする hatena-bookmark

つい忘れてしまうファイル最後の改行。


[ Settings... ]

  ↓

[ Editor ]

  ↓

[ General ]

  ↓

[ Ensure every saved file ends with a line break ]

ファイルの末尾に新しい行を追加するように Android Studio を設定するにはどうすればよいですか?

これで、ファイルを開いた時点で最終行の改行は自動で必ず付いている、自動保存なので。

てか、IDE デフォルトで ON でもいいように思う機能。

 

🧑‍💻 と思ったら付かないので「Keep tarailing spaces on caret line」を OFF に

「Keep tarailing spaces on caret line」 を OFF にしないと付きません。

あと、余計な末尾の空白行は、「Remove trailing blank lines at the end of saved files」を ON にすると自動で消してくれます。

 

🧑‍💻 自動保存のタイミングはいつなのか

AndroidStudio デフォルトの設定では、

アプリを切り替えたとき、

または、ビルトインのターミナルに切り替えたとき

に自動保存されるようです。

自動保存のタイミングはいつなのか

なので、上で述べた最終行まわりの自動処理はその時に行われます。

自動保存のタイミングはいつなのか

 

🧑‍💻 まとめ

自動保存時に、

ファイル末尾の改行がなかったら付ける。

ファイル末尾の無駄な空白行は削除する。

の AndroidStudio の設定は以下。


[ Settings... ]

  ↓

[ Editor ]

  ↓

[ General ]

  ↓

□ [ Keep tarailing spaces on caret line ] OFF
✅ [ Ensure every saved file ends with a line break ] ON
✅ [ Remove trailing blank lines at the end of saved files ] ON

こんな機能あるとは知らなかったです。便利です。

👉 【AndroidStudio】detekt で JetpackCompose 記述をチェックする hatena-bookmark