✍️

【Jetpack Compose】適当に引数にModifierを渡していない?

2021/09/25に公開

2021年7月28日にJetpack Composeがstableになり、最近ゆるゆるとComposeを書いている中で少し疑問に思ったこととがあったので、メモがわりに記事を書きます。

地味でくだらないことではありますが、意外と雰囲気で書いてしまっていることもあるのではないかと思います。

Composableな関数の引数にModifierを渡す

Jetpack ComposeのCodelabsや、 公式が出しているCompose製のSample でComposeの書き方を勉強しつつ個人でアプリを作っている最中、よくこういったコードが書かれているのを目にします。

@Composable
fun PostImage(post: Post, modifier: Modifier = Modifier) {
  /** 略 */
}

Composableな関数の引数にデフォルト引数付きでModifierを渡しています。

Jetpack Composeの記事やGithubのリポジトリを色々物色していると、なんとなくでModifierをScaffoldから末端のTextまで全て連れ回しているコードを書いている方もちらほらいました。

初めてComposeを学び始めた時、割と混乱していたので軽くメモを残したいと思います。

ModifierはメソッドチェーンでUI要素の表示/動作の方法を制御できるKotlinのオブジェクトということを理解した上で、どういう場面で引数に渡すべきかを考えてみます。

どういう場面でModifierを渡すのか

デフォルト引数があるということは、呼び出し側でModifierを渡さなくてもその関数の構成要素であるComposableの表示/動作方法は指定できるのです。

つまり、子要素で完結するAttributeは子要素でModifierにメソッドチェーンで指定し、子要素のみで完結しないAttributeは親要素からModifierを渡すことで指定する感じです。

例えば以下のような投稿画像を表示する末端のComposableを作成する際に、子要素で完結する size や、 clip は 子要素で指定しています。

@Composable
fun PostImage(post: Post, modifier: Modifier = Modifier) {
    Image(
        painter = painterResource(post.imageThumbId),
        contentDescription = null, // decorative
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

JetNews - PostCardsより抜粋

呼び出し側では、

@Composable
fun PostCardSimple(
   // 略
) {
   // 略
   PostImage(post, Modifier.padding(end = 16.dp))
}

や、

fun PostCardHistory(
   // 略
) {
   PostImage(
      post = post,
      modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp)
   )
}

のように、呼び出し側で子要素の表示の仕方を制御したいときに指定したModifierを子要素に渡している感じです。

また、このようなレイアウトを作成する際にConstraintLayoutを使う例として

@Composable
fun SampleIcon(imageUrl: String, modifier: Modifier = Modifier) {
  val painter = rememberImagePainter(
    data = imageUrl,
    builder = { transformations(CircleCropTransformation()) }
  )
  Image(painter = painter, contentDescription = "Sample Icon", modifier = modifier.size(64.dp))
}
@Composable
fun SampleThumbnail(imageUrl: String, modifier: Modifier = Modifier) {
  val painter =rememberImagePainter(data = imageUrl)
Image(
    painter = painter,
    contentDescription = "Sample Thumbnail",
    modifier = modifier
      .width(90.dp)
      .height(160.dp)
  )
}

というそれぞれModifierを引数に受け取るComposableを定義します。

呼び出し側は、

@Composable
fun SampleConstraintLayout(
  iconUrl: String,
  thumbnailUrl: String
) {
  ConstraintLayout {
    val (icon, thumbnail) = createRefs()

    SampleThumbnail(
      imageUrl = thumbnailUrl,
      modifier = Modifier
        .constrainAs(thumbnail) {
          start.linkTo(parent.start)
          end.linkTo(parent.end)
          top.linkTo(parent.top, margin = 32.dp)
          bottom.linkTo(parent.bottom)
        }
    )
    SampleIcon(
      imageUrl = iconUrl,
      modifier = Modifier
        .constrainAs(icon) {
          start.linkTo(parent.start)
          end.linkTo(parent.end)
          top.linkTo(parent.top)
        }
    )
  }
}

のように、ConstraintLayoutScope内でしか使えない constrainAs で子要素の表示を制御したい場合にも引数にModifierを受け取るようにして親要素で制御するようにします。

結論

  • 呼び出し側で子要素のUI表示方法を制御したい場合に、子要素にModifierを渡す
    • 子要素で完結する場合は渡さなくて良い
  • 特定のScope内でのみ使えるModifierを子要素に適用したい場合に、Modifierを渡す。

地味なことですが意外と他で言及されていなかったので書いてみました。

Discussion