【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


gradle コマンドを補完しようとして brew にも補完があることを知る今さら感

 

👩‍💻 gradle/gradle-completion

本家から公開されてたんですね!

Gradle ./gradlew を補完しようとして brew の補完を知る今さら感

👉 gradle/gradle-completion: Gradle tab completion for bash and zsh hatena-bookmark

./gradlew でも利用できるようなのでインストールしてみました。


brew install gradle-completion

しかし、補完されないので調べる。


❯ brew info gradle-completion
...
==> Caveats
zsh completions have been installed to:
  /opt/homebrew/share/zsh/site-functions
...


❯ ls /opt/homebrew/share/zsh/site-functions
_brew@     _gradle@   _lsd@      _starship@

手動でロードしてみると弾いてるが。


❯ source /opt/homebrew/share/zsh/site-functions/_gradle

_arguments:comparguments:327: can only be called from completion function
_tags:comptags:36: can only be called from completion function
_tags:comptry:55: can only be called from completion function
_tags:comptags:60: can only be called from completion function
_tags:comptags:67: can only be called from completion function

どうなってるんや。

 

👩‍💻 Basic installation doesn't work on macOS with zsh #95

issues にこんな。


if [ $commands[brew] ]
then
  fpath=("$(brew --prefix)/share/zsh/site-functions" $fpath)
fi

gradle-completion/issues/95#issuecomment-1165679687

見てみると FPATH にない。


❯ echo $FPATH | tr ':' '\n'
/opt/homebrew/share/zsh-completions
/usr/local/share/zsh/site-functions
/usr/share/zsh/site-functions
/usr/share/zsh/5.9/functions

ここに、gradle-completion のインストール先である


/opt/homebrew/share/zsh/site-functions

を追加すると補完は効き始めた。

 

👩‍💻 Homebrew Shell-Completion

さっきの解決方法の issue コメントに書いてあったページをみてみる。

To make Homebrew’s completions available in zsh, you must insert the Homebrew-managed zsh/site-functions path into your FPATH before initialising zsh’s completion facility. Add the following to your ~/.zshrc:


if type brew &>/dev/null
then
  FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}"

  autoload -Uz compinit
  compinit
fi

👉 brew Shell Completion — Homebrew Documentation hatena-bookmark

.zshrc に追加すると、brew コマンドでも TAB キーで補完が効き始める。


❯ brew i

info                  -- Display brief statistics for your Homebrew installa
install               -- Install a formula or cask
install-bundler-gems  -- Install Homebrew's Bundler gems
irb                   -- Enter the interactive Homebrew Ruby shell

知らんかった。

 

👩‍💻 まとめ

Homebrew のコマンド補完を以下で有効にしておくと、


# .zshrc
if type brew &>/dev/null
then
  FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}"

  autoload -Uz compinit
  compinit
fi

Homebrew で以下にインストールされる補完スクリプトたちも、


❯ ls $(brew --prefix)/share/zsh/site-functions
_brew@     _gradle@   _lsd@      _starship@

インストール直後にそのまま使えるようになる。


【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