【JetpackCompose】 Composable 関数の Modifier について知らなかった3つのルール

【JetpackCompose】 Composable 関数の Modifier について知らなかった流儀

detekt に指摘されながら、ドキュメントを読む。


こんなコードがあったとして。


Column {
    InnerContent()
}

@Composable
private fun InnerContent() {
    Text(...)
    Image(...)
    Button(...)
}

以下、知らなかった流儀。

 

🧩 Composable 関数はレイアウトを一つだけ出力する

A composable function should emit either 0 or 1 pieces of layout, but no more. A composable function should be cohesive, and not rely on what function it is called from.

コンポーザブル関数は、レイアウトのピースを0個または1個だけ発行するべきで、それ以上は発行してはいけません。コンポーザブル関数は結束性を持ち、呼び出される関数に依存すべきではありません。

レイアウトのネストのコストはあまり気にしなくて良い、とのこと。

Nesting of layouts has a drastically lower cost vs the view system, so developers should not try to minimize UI layers at the cost of correctness.

レイアウトのネストには、ビューシステムに比べてはるかに低いコストがかかるため、UIの階層を正確性の犠牲にして最小化しようとしないべきです。

👉 Do not emit multiple pieces of content - Twitter Jetpack Compose Rules hatena-bookmark

ということで、一見不要に見える Column を追加する。


@Composable
private fun InnerContent() {
    Column {
        Text(...)
        Image(...)
        Button(...)
    }
}

 

🧩 デフォルトを持つパラメータ modifier: Nodifier = Modifier は必須

They are especially important for your public components, as they allow callers to customize the component to their wishes.

特に、公開コンポーネントにとって Modifier は非常に重要であり、呼び出し元がコンポーネントを希望に合わせてカスタマイズできるようにします。

👉 When should I expose modifier parameters? - Jetpack Compose Rules hatena-bookmark

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 and assign the parameter a default value of Modifier.

Composable 関数内でコンポーザブル関数を表すコンポーネント全体に適用するための修飾子をパラメータとして受け入れる場合、そのパラメータは "modifier" という名前を付け、パラメータに Modifier のデフォルト値を割り当てるべきです。

👉 Modifiers should have default parameters - Jetpack Compose Rules hatena-bookmark

ということで、親からの Modifier を受け入れるようにします。デフォルト値もつけておきます。


@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
    Column {
        Text(...)
        Image(...)
        Button(...)
    }
}

 

🧩 受け取った Modifier パラメータは最上位のレイアウトにのみ適用する

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 に他の Modifier を連鎖させることは許可されています。

👉 Modifiers should be used at the top-most layout of the component - Jetpack Compose Rules hatena-bookmark

ということで、最上位ルートの Column で適用します。


@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(...)
        Image(...)
        Button(...)
    }
}

 

🧩 まとめ

@Composable 内のレイアウトに関する Modifier は、親 (呼び出し元) から持ってきて、最上位のレイアウトで一度だけ適用する。

detekt を使うことで、なんとなく記述していた部分がスッキリしてきます。

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


【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


ファイルの末尾に改行を追加するように 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