※本記事は、Rebecca Franks氏、Meghan Mehta氏、Chuck Jazdzewski氏が登壇したGoogle I/O 2026のAndroidセッション「Build custom design systems with Jetpack Compose's new Styles API」の内容を基に作成されています。本セッションでは、Jetpack Composeの新しいStyles APIを使ってカスタムデザインシステムを構築・標準化する方法が紹介されており、テーマレベルでのプロパティ上書きに加え、modifierに対するパフォーマンス上のメリットについても解説されています。あわせて、MaterialによるStylesの採用方針、Style APIを使った様々なアニメーションの実装方法、そしてデバイスサイズに応じて適応するテーマを備えた独自のデザインシステムを、Stylesを組み合わせて構築する方法についても取り上げられています。動画の詳細情報は https://www.youtube.com/watch?v=e-wlF3cmJms でご覧いただけます。本記事では、セッションの内容を要約しております。なお、本記事の内容は登壇者の見解を正確に反映するよう努めていますが、要約や解釈による誤りがある可能性もありますので、正確な情報や文脈については、オリジナルの動画をご視聴いただくことをお勧めいたします。
関連リソースとして、Styles APIの詳細ドキュメント(https://goo.gle/4uM1647)、アダプティブレイアウト向けのMediaQuery API情報(https://goo.gle/4nv0u0B)、Stylesスキルに関する情報(https://goo.gle/compose-styles-skill)も公開されています。また、Google I/O 2026のAndroidセッション一覧(https://goo.gle/Android-at-IO2026)や、Android Developersチャンネルの登録ページ(https://goo.gle/AndroidDevs)もあわせてご参照ください。
1. イントロダクションとMaterialカスタマイズの課題
1.1 登壇者紹介とカスタムデザインシステムのニーズ
Rebecca Franks: 皆さん、こんにちは。「Building Custom Design Systems with Jetpack Compose's New Styles API」へようこそ。私はRebecca Franksと申します。本日は私のほかに、MeganとChuckにも登壇してもらいます。私たちはこれまで、Androidが様々なデバイスフォームファクタをサポートするように進化してきた過程を見てきましたし、それに伴ってアプリ側も自分たちだけの独自デザインシステムで他社と差別化したいというニーズが高まっていることを実感しています。すべてのアプリがそれぞれ固有の個性を持っていることを私たちは理解していますし、Materialをフルに活用したデザインシステムから、完全にカスタムなデザインシステムまで、その間のどこかに位置するアプリも数多く存在します。今日お話しする内容は、そうしたスペクトラムのどこに位置するアプリにも対応できるフレームワークについてです。
1.2 Jetsnackの紹介
Rebecca Franks: その一例として紹介したいのがJetsnackです。Jetsnackは私たちがComposeで最初期にリリースしたサンプルの一つで、Composeを使ってカスタムなものを作る際の好例として、ずっと活用してきました。Jetsnackのデザインシステムには独自の特徴があり、その一つがグラデーションを非常に幅広く使っている点です。コンポーネントのデザインも、Material Designがそのまま提供しているものとは少しずつ異なっています。例えば、フィルターチップには通常状態でグラデーションのボーダーを、プレス状態では単色ではなくグラデーションの塗りつぶしを使うなど、様々な形でグラデーションを取り入れてきました。
1.3 Materialコンポーネントの利点
Rebecca Franks: 一方でMaterialコンポーネントには、そのまま使うだけで得られる大きなメリットがあります。アクセシビリティ標準への対応や、右から左に読む言語向けのレイアウト対応、様々なインタラクション状態のハンドリングといった複雑な部分をMaterialが肩代わりしてくれますし、コンポーネントも基本的に見た目良く仕上がります。だからこそ私たちはMaterialを愛用しているわけです。ただ、先ほどお見せした通り、私たちのデザイナーはもう少しカスタムなものを求めてきます。
1.4 グラデーション対応の試行錯誤
Rebecca Franks: そうなったとき、皆さんは一体どうするでしょうか。このフィルターチップの例で言えば、おそらくまずMaterialのFilterChipをインポートして、選択状態の背景色にグラデーションを設定しようと試みるはずです。しかしそこで気づくのは、このAPIが単色を前提に設計されていて、Brush型をサポートしていないという事実です。そこで多くの方がとる手段はいくつかあります。私の場合は、色を透明に設定した上でチップをBoxで囲み、そのBoxにmodifierのbackgroundでグラデーションを設定するという方法を試しました。FilterChipをそのBoxの中に移動させて、一見うまくいっているように見えたのですが、実際に動かしてみると問題が発生します。コンテナ側にもシェイプを適用しなければならないことに気づきますし、それだけでなく、プレス状態や選択状態のハンドリングロジックも自前で組み直す必要が出てきます。ここまで来ると、Materialのソースをフォークするか、あるいは自分たちで一から独自のコンポーネントを作るしかない、という判断に至る方も多いはずです。実際にJetsnackでも、私たちは独自のコンポーネントを作ってきました。ただしその代償として、Materialが提供してくれていたはずのこうした恩恵をすべて失ってしまうことになります。そこで私たちが取り組んできたのが、本日皆さんにプレビューとしてお見せしたい、Stylesと呼ばれる新しい仕組みです。
2. Styles APIの概要と基本的な使い方
2.1 Styleの定義と位置づけ
Rebecca Franks: 私たちは、Stylesという新しい仕組みに取り組んできました。StylesはJetpack Composeにおける要素のカスタマイズのための新しいメカニズムです。フォームとファンクションを分離し、状態に応じたスタイリングを簡単に実現できるようにすることを目的として設計しており、それに加えてパフォーマンス面でのメリットも得られるようになっています。
Megan Mehta: ありがとうございます、Rebecca。それでは早速本題に入っていきましょう。先ほどお話しした通り、私たちはStyle APIを使ってJetsnackを作り直そうとしているのですが、そもそもStyleとは何なのか、疑問に思う方も多いと思います。Styleとは、コンポーネントの外観を定義するインターフェースのことです。Styleは特殊化されたmodifierであり、パフォーマンスと再利用性の恩恵を自動的に得られるようになっています。ここで強調しておきたいのは、Stylesはmodifierを置き換えるためのものではないということです。あくまで、見た目に関するパラメータを置き換えるためのものだとご理解ください。実際にJetsnackでは、グラデーション効果を実現するためにボタンを独自実装していますが、このボタンの定義には、無効化状態やシェイプ、ボーダーなど、非常に多くのスタイリング用パラメータが並んでいました。Stylesを使えば、こうした行をすべて単一のStyleパラメータに置き換えることができ、あらゆるコンポーネントに設定できる標準化されたプロパティ群を提供できるようになります。
2.2 リリース状況
Rebecca Franks: Style APIは、Compose Foundationのバージョン1.12にて実験的な機能としてリリースされています。Materialでの採用については、近日中に対応予定です。これは、皆さんが今日コンポーネントとやり取りする方法における大きな転換になるはずです。本日は、Style APIを使うことでコンポーネントのカスタマイズがどれほど簡単になるかを、Jetsnackを一新したバージョンを通してお見せしていきます。グラデーションは引き続き使いますが、これまでより少し控えめな仕上がりになっています。
2.3 コンポーネントのスタイル可能化
Megan Mehta: さて、Styleが何かはご理解いただけたと思いますので、次は実際の使い方を見ていきましょう。コンポーネントにStyleを追加する場合、ユースケースは2つあります。1つは、カスタムコンポーネントを作成してそれをスタイル可能にすること、もう1つは、すでにStyleパラメータを持つコンポーザブルを利用することです。まずは、カスタムコンポーネントをスタイル可能にする方法から見ていきます。JetsnackのボタンはRowをclickableにする形で内部実装されています。これをスタイル可能にするには、stylable modifierを追加し、メソッドシグネチャにStyleパラメータを追加します。こうすることで、このカスタムボタンの実装側がStyleパラメータを使ってカスタマイズできるようになるわけです。デザインシステムのコンポーネントには、常にmodifierを設定できるようにするのと同じ発想で、常にStyleパラメータを用意しておくことが推奨されています。そして、StyleStateとStyleパラメータをstylable modifierに渡していきます。StyleStateについては、この段階では深く立ち入りませんが、この後の内容で改めて取り上げます。これで、私たちのカスタムボタンはStylesを使える準備が整いました。
2.4 Styleパラメータの適用例
Megan Mehta: もう1つのユースケースは、先ほど作成したボタンのように、すでにStyleパラメータを持つコンポーネントにスタイルを適用することです。Jetsnackでは、このカスタム実装のボタンを使っていきます。Styleブロックに何も指定していない状態がこちらです。それでは、実際にスタイルを追加してみましょう。利用可能なStyleプロパティの一覧をご覧いただくと分かる通り、かなり充実した内容になっています。ただ、細部にとらわれる必要はありません。すべてドキュメントに載っていますので。ここでは、背景を薄い灰色に更新するためにbackgroundプロパティを使っていきます。Styleブロックの中でbackgroundを追加し、light grayを渡します。これだけで、コンポーネントの見た目を簡単に変更できてしまいます。もちろん、ここでお見せしたのはまだ表面をなぞった程度に過ぎません。この後は、グラデーションやアニメーション、状態に応じたスタイリングをStyle APIでどのように実現していくか、順を追って見ていきます。ただ、その前にまず、Styleの基本原則についてしっかり押さえておく必要があります。
3. Styleの4つの基本原則とその実演
Megan Mehta: OK、皆さん。それでは、Styleの基本原則についてお話しします。1つ目は、Styleプロパティは互いに上書きし合うということです。2つ目は、Styleプロパティは継承されるということです。3つ目は、Styleは再利用可能であるということです。そして4つ目は、Styleはマージ可能であるということです。これらの4つを、新しいJetsnackボタンをスタイリングしながら、実際に見ていきましょう。
3.1 上書き可能性
Megan Mehta: ボタンのコードに戻りますと、すでにbackgroundプロパティを使って背景色を薄い灰色に変更しています。同じように、他のプロパティも簡単に設定できます。構文上は、1行につき1つのプロパティを追加していくだけで、カンマは不要です。widthやheight、shape、fontWeight、textAlignを追加してみましょう。さらに外側のパディングを32DP追加し、その上でトップパディングだけを16DPに変更しようとします。ここで皆さんは、32DPに16DPが加算されて合計48DPのパディングになると予想するかもしれませんが、実際には16DPのパディングしか得られません。これは、同じプロパティが2回設定された場合、Styleプロパティは互いに上書きし合うためです。ボタンには32DPのパディングが設定されていますが、外側パディングのトップに関して最後に定義されたプロパティがexternalPaddingTopであるため、それがトップパディングを上書きし、結果として16DPになります。もしこれをmodifierで行った場合には、2つのパディングが加算され、トップパディングは48DPになります。
3.2 継承可能性
Megan Mehta: Jetsnackには、Styleパラメータを受け取るカスタムのTextコンポーザブルもあります。ここで疑問に思われるかもしれませんが、なぜfontWeightやtextAlignをTextのStyleプロパティとして設定するのではなく、ボタン側で設定しているのでしょうか。結局のところ、これらのプロパティが影響するのはボタンではなくテキストのはずです。そこで、textAlignについて詳しく見ていきましょう。もしButtonStyleからtextAlignを取り除き、代わりにTextStyleに直接追加しても、同じ結果が得られます。ユースケースによっては、ボタンレベルでtextAlignを設定することを選びますが、それは特定のTextStyleプロパティが継承可能であるためです。ボタンレベルでtextAlignを設定すると、テキストコンテンツは親コンポーザブルからテキストアラインメントを継承します。子のコンポーザブルが1つしかない場合は、あまり違いが分からないかもしれません。しかし、複数のTextコンポーザブルをカラムとしてボタンのコンテンツに渡した場合、それらすべてが中央揃えのtextAlignを継承することになります。そして、もし特定のTextコンポーザブルに直接textAlignのStyleパラメータを追加すれば、それは継承されたコンポーザブルよりも優先されます。実際、「Add」と「Cart」は中央揃えのtextAlignを上書きしていますが、「2」はそうしていないため、ボタンのtextAlignをそのまま継承しています。優先順位のトーテムポールの中で最も低い位置にあるのが、親で定義されたスタイルです。そして、もしコンポーネントにStyleを設定するmodifierを指定すれば、それは親で定義されたスタイルよりも高い優先順位を持ちます。最後に、直接Styleパラメータを設定することが、最も高い優先順位を持ちます。これはあくまで、継承可能な特定のTextStyleプロパティに限った話です。
3.3 再利用可能性
Megan Mehta: さて、これで私たちは非常に良い感じのボタンStyleを手に入れましたので、これをアプリ内の複数のボタンで使い回したいと考えるかもしれません。そこで、ButtonStyleというStyleオブジェクトを作成できます。このブロックの中に、先ほどボタンのStyleブロックに追加していたのと同じプロパティを追加していきます。そして、ButtonStyleのプロパティをすべてButtonStyleオブジェクトに置き換えます。Stylesは再利用可能であるため、このStyleを複数の異なるボタンに渡して、それぞれにスタイリングを提供できるようになります。
3.4 マージ可能性
Megan Mehta: それでは次に、シャドウやグラデーションといった、より特別なカスタムスタイリング向けに、GradientStyleという新しいStyleを追加しましょう。この中に、ドロップシャドウとインナーシャドウを追加します。そして、ButtonStyleブロックの中でButtonStyleをGradientStyleに置き換えてしまうと、ButtonStyleが持っていたすべての要素、つまりwidth、height、shape、padding、textAlignを失ってしまいます。しかし、私たちが本当に求めているのは、ButtonStyleとGradientStyleの両方を組み合わせた結果です。そこで、Style Mergingを使います。Styleパラメータの中に、ButtonStyleとGradientStyleの両方を渡すために、Styleコンストラクタにもう1つ引数を追加するだけです。これで、GradientStyleによるシャドウがきちんと確認でき、ボタンに奥行きが生まれます。次に、背景のグラデーションを追加してみましょう。GradientStyleの中に、linearGradientを使ったbrushを追加します。ただ、思い出してほしいのですが、backgroundはButtonStyleとGradientStyleの両方で定義されています。それでも私たちが目にしているのはGradientStyle側の背景です。これは、StyleパラメータにButtonStyleの後にGradientStyleを渡しているためです。Styleプロパティが上書き可能であるという点を思い出してください。最後に定義されたStyleプロパティが勝つのでしたね。gradientが2番目に渡されているため、そのbackgroundプロパティが最後に定義されたことになり、その結果が表示されているわけです。もしGradientStyleとButtonStyleの順序を入れ替えれば、今度はユーザーには灰色の背景が見えることになります。それでもシャドウは引き続き見えますが、これはGradientStyleが依然として適用されているためです。上書きされるのは、2回定義されているbackgroundプロパティだけです。なお、Styleは上書き可能であるため、グラデーションの背景は灰色の背景の下に重なるのではなく、それを置き換える形になります。また、構文的にはinfixのthen演算子を使ってStyle同士を組み合わせることも可能です。このセクションでは、上書き可能性、継承可能性、再利用可能性、マージ可能性というStyleの基本原則について学び、それらを活用してこの美しいグラデーションボタンを作り上げました。
4. インタラクション状態とアニメーション
4.1 組み込みの状態とトリガー条件
Megan Mehta: それでは次に、インタラクション状態とStyleについてお話ししていきます。ここまでで美しくスタイリングされたボタンができましたが、今度は異なるインタラクション状態に応じたスタイリングを追加していきたいと思います。Style APIには、デフォルトでいくつかの状態が組み込まれています。何もインタラクションが発生していないときのdefault状態、pressed状態、hovered状態、そしてfocused状態です。hoveredとfocusedは、マウスが接続されているデバイスで発火します。focusedについては、TVのようにDパッドでのナビゲーションを行うデバイスでも発火します。これらの状態はすべて、インタラクション可能な要素ごとに定義しておくことが望ましいです。というのも、ユーザーは様々なデバイス構成でアプリを使うことになるためです。
4.2 状態別スタイルの定義実演
Megan Mehta: さて、すでにButtonStyleとGradientStyleをStyleパラメータに渡している状態ですが、状態に応じたスタイリングを追加する方法としては、Styleオブジェクトを追加する方法と、括弧の後にブロックを追加してインラインでStyleを記述する方法があります。今回は後者を選びました。そのブロックの中に、pressブロックを追加できます。press ブロック内の構文は、通常のStyleブロックと同じです。ボタンが押されたときのStyleを定義するために、backgroundプロパティを追加してlight purpleに設定します。これで、ボタンを押すとlight purpleに変わることが確認できます。続いて、hoveredとfocusedも追加してみましょう。hoveredはラベンダー色の背景に、focusedは薄い紫色のボーダーになります。少々お待ちください。……このように、すべての状態が反映されました。状態に応じたスタイリングを実現するために必要な作業は、基本的にこれだけです。
4.3 animateラムダによる遷移
Rebecca Franks: それでは、私が個人的に大好きなトピックである、アニメーションとStyleについて簡単にお話しします。私たちは状態に応じたスタイリングを追加できるようになり、それだけでもアプリに大きな価値をもたらしますが、状態間の遷移自体は、私たちが望むほど滑らかで洗練されたものにはなっていませんでした。今のままでは、遷移アニメーションなしに終了状態へ単純にスナップしてしまいます。これらの状態間を滑らかに遷移させるためには、任意のStyleプロパティをanimateラムダで囲むことができます。これにより、ラムダブロック内で定義された開始状態と終了状態の間で、自動的に遷移が実行されるようになります。この例では、インナーシャドウの初期値をオフセット0DPに設定し、hover状態のときにはインワード方向に-6DPと-2DPへ変更しています。ドロップシャドウについても同様に動かしており、その結果、hover時にシャドウが心地よく動くアニメーションが実現できています。
4.4 アニメーション仕様のカスタマイズ
Rebecca Franks: アニメーションさせられるプロパティは他にも数多くあります。例えば、背景色をアニメーションさせることで、hover時にボタンの色を変化させることもできます。この仕組みの良いところは、自分でステートの変化を追跡したり、これまで必要だった様々なアニメーション用の状態変数を自前で作成したりする手間が不要になる点です。こうしたアニメーションをカスタマイズするには、このanimateブロックにアニメーション仕様を渡すこともできます。ここでは、少しアニメーションの動きをゆっくり見せるために、より長い持続時間を設定したtweenスペックを渡していますが、他の任意のアニメーション仕様を渡すことも可能です。それでは、次のセクションについてはChuckに引き継ぎます。
5. カスタムStyleStateとCustom Style
5.1 StyleStateの役割
Chuck Jazdzewski: それでは、カスタムのStyleStateについて、そしてそれをなぜ使いたいのかについてお話ししていきます。先ほどのボタンの定義に戻ってみると、StyleStateを定義してstylable modifierに渡していたことが分かります。そもそも、このStyleStateとは一体何なのでしょうか。StyleStateは、UI要素の現在の状態を表すものであり、pressedやhovered、focusedといったインタラクション状態を追跡するために使われ、その状態に基づいてどのStyleプロパティを選択するかという、状態に応じたスタイリングのために利用されます。これによって、インタラクションソースとStyle自体を結び付けることができます。もし状態とStyleを結び付けていなければ、コードはコンパイルは通りますが、ボタンを押しても何も起こらないということになります。これがStyleStateの定義です。クラス定義を見ると、いくつかの状態が組み込まれていることが分かります。定義の一番最後にgetオペレーターがあることに気づいた方もいるかもしれません。このgetオペレーターがカスタム状態を可能にしています。というのも、StyleStateはStyleStateKeyから値へのマップになっているためです。それでは、これがどのように使われるのか、具体例を見ていきましょう。Jetsnackでは、「Add to Cart」ボタンについて、何かをロード中のときやエラーが発生したときに、見た目を変えたいと考えていました。ここに、それぞれの状態がどのような見た目になるべきかを示した、更新後のデザインがあります。ロード完了状態のデザインは変わっていない点に注目してください。これはデフォルトなので当然です。しかし、ロード中とエラーの状態については、デザインが異なっています。こうしたデザインを実現するには、ロード状態をStyleに何らかの形で伝え、デザインに合わせてボタンの見た目を変更できるようにする必要があります。これがまさに、カスタム状態の出番です。
5.2 カスタム状態(ローディング/エラー)の実装
Chuck Jazdzewski: カスタム状態を作成するには、まずその状態のための型を作成する必要があります。Jetsnackでは、ロード状態を追跡したかったため、loading、loaded、errorという3つの値を持つenumクラスを作成しました。次に、対応するStyleStateKeyを作成する必要があります。このキーは、コンポーネントがロード状態の現在値をStyleStateに記録するために使われ、後ほど、そのStyleの中で状態を読み取る際にも同じキーを使うことになります。StyleStateKeyを作成する際には、デフォルト値を指定する必要があります。これは、特別な状態ではないときの値であるべきです。今回のケースでは、すでにロードが完了している状態がそれにあたるため、デフォルトとしてLoadingState.Loadedを使用します。このStyleがこのカスタム状態を読み取れるようにするには、StyleScopeに拡張関数を追加する必要があります。この関数は、カスタム状態がloadingであるときにのみ、そのブロックを実行するというものです。これは例えば、pressedがどのように実装されているかと同じ考え方です。isPressedがtrueのときにのみ、そのブロックが呼び出されるという仕組みです。StyleScopeは、Styleラムダのレシーバースコープであり、padding、background、borderといったStyleプロパティを導入する役割を担っています。このStyleScopeを拡張することで、loadingという状態に応じてStyleをカスタマイズできるようになります。StyleScopeには、loadingのような拡張関数のために使われる、Stateというヘルパー関数が用意されています。この関数は、キー、ブロック、そしてアクティブかどうかを表す式を必要とします。キーは先ほど作成したもの、つまりロード状態のキーです。ブロックはブロックパラメータをそのまま渡すだけです。このブロックパラメータが、ロード状態に固有のプロパティを設定します。そして最後にactiveラムダがあり、これはロード状態にあるときにtrueを返します。今回のケースでは、現在の状態とLoadingState.loadingを比較するだけであり、それによってロード状態に固有のプロパティが設定されることになります。同じパターンに従って、エラー状態のときに使うloadingErrorという拡張関数も追加できます。次に、ボタン側に戻って、ロード状態を受け取り、それをStyleに伝えるように更新していきます。まず、ボタンがロード状態をパラメータとして受け取るように更新します。デフォルト値はLoadingState.loadedとし、StyleStateKeyのデフォルトと一致させます。次に、ロード状態をStyleStateに反映させるように更新する必要があります。これは、StyleStateを作成するのと同じ式の中で行います。updateStyleStateはトレーリングラムダを受け取り、これはボタンが再コンポジションされるたびに呼び出されることを思い出してください。このロード状態キーを使って、ロード状態をStyleStateに記録します。これで、ボタンをカスタマイズできるようになりました。ここでは、focused、pressed、hoveredをカバーする簡略化されたバージョンのボタンを使っていますが、これを先ほど作成した拡張関数を使って、ロード状態のときのボタンの見た目を変更するように更新していきます。このように、StyleStateラムダによって、pressedやhovered、focusedと同じように、loadingやloadingErrorの値を上書きできるようになります。さて、私たちが実現したかったデザインを思い出してください。ロード中は色をlight orangeに変え、エラー状態のときはlight redに変えることで、これらのデザインを実装しました。これで、カスタムの状態に応じたスタイリングを実装できたことになります。StyleStateをloadingという状態に拡張したことで、Styleを使うあらゆるコンポーネントが、このloading状態を利用できるようになります。例えば、Jetsnackのカードやフィルターチップでも使えるということです。
5.3 Custom Styleによるスコープ制限
Chuck Jazdzewski: ただし、これは多くのコンポーネントに共通する状態であれば問題ありませんが、そうでない場合には、そのコンポーネントには本来関係のないStyleState用の関数がスコープに散らかってしまうことになります。例えば、Jetsnackのカードやフィルターチップは、どちらもロード状態を更新するようなコンポーネントではありません。loadingやloadingErrorといった状態関数は、本来そのロード状態が意味を持つコンポーネントでのみ利用できるようにするべきです。理想を言えば、ロード状態を持たないコンポーネントのStyleの中でloadingを使おうとした場合には、構文エラーになるようにしたいところです。これを実現するのが、Custom Styleです。Custom Styleを使うと、コンポーネントごとにStyleScopeを制限し、そのコンポーネントにふさわしいプロパティだけを表示させることができ、さらにどの拡張関数を利用可能にするかも制御できます。今回のケースでは、loadingとloadingErrorがどこで見えるようにするかをコントロールしたいと考えました。それでは、その実現方法を見ていきましょう。Custom Styleを作成するには、まずスコープの中で見えるようにしたいものを定義するところから始めます。ボタンの場合、Styleの拡張のされ方だけをコントロールしたく、特に制限を設けたいわけではないため、LoadingStyleScopeをStyleScopeから直接継承させる形にします。次に、先ほど作成したこのスコープを使って、カスタムのLoadingStyleを作成します。そして、先ほど書いた拡張関数を、StyleScopeではなくこの新しいスコープを拡張するように更新します。これにより、これらの拡張関数はLoadingStyleの内部でのみ使用可能になり、通常のStyleの中ではもはや使えなくなります。最後に、ボタンに戻り、この新しいCustom Styleを使うように更新します。このCustomLoadingStyleは実際にはStyle型ではないため、Styleパラメータに渡す前にStyleへ変換する必要があります。そのため、toStyle()を呼び出します。これで、loadingとloadingErrorはボタンをカスタマイズする際には使えますが、フィルターチップをカスタマイズする際には使えなくなり、まさに私たちが狙っていた通りの結果になりました。
6. パフォーマンスに関するベンチマーク結果
6.1 ベストケースのベンチマーク結果
Chuck Jazdzewski: さて、ここまでお話ししてきたこれだけの機能を実現するにあたって、一体どれほどのコストがかかるのか、疑問に思う方もいるでしょう。実は意外なことに、Stylesは対応するmodifierを使う場合と比べて、通常はより安価に済みます。その主な理由は、Stylesを使うことで、Composeのパフォーマンスに関するベストプラクティスを、はるかに簡単に実践できるようになるためです。ここで、ベストケースの例を見てみましょう。このコードは、Boxのボーダーカラーを変更しつつ、いくつかの他のプロパティも設定するというものです。私たちが作成したすべてのベンチマークは、基本的にこれと同じような構成になっています。つまり、あるコンポーネントをmodifierで実装した場合と、同じコンポーネントをStylesで実装した場合とを比較するというものです。このベンチマークでは、Stylesを使うことでアロケーション数が約70%削減され、次のピクセルを描画するまでの速度が最大2.3倍高速化しました。
6.2 Stylesが高速な理由
Chuck Jazdzewski: なぜこのような結果になるのか、Stylesの仕組みをもう少し詳しく見ていきましょう。Stylesは、レイアウトと描画にのみ影響を与えるmodifierです。contentPaddingのようなレイアウトに関わるプロパティが変化した場合は、レイアウトと描画のフェーズだけが実行されます。backgroundのような描画に関わるプロパティが変化した場合は、描画フェーズだけが実行されます。先ほどのベンチマークでは、次のピクセルを更新するために描画フェーズだけが実行されていました。Stylesによるパフォーマンス上のメリットは、主に3つの要素から生まれています。まず、Stylesはコンポジションの中で作成されますが、実際に評価されるのはコンポジションの後になってからです。これは、これまでmodifierが担っていた作業を、Composeのより後のフェーズへとシフトさせることを意味しており、これによってStyleが変化してもコンポジションをスキップできるようになります。また、Stylesは単一のmodifierを使用するため、同じ機能を実現するために必要なアロケーション数が削減されます。さらに、コンポジション中にアニメーションが作成されることがない点も重要です。アニメーションは必要になったときにだけ作成されるため、コンポーザブルを最初に作成する際のコストが削減されます。先ほどのボーダーのベンチマークは、Stylesの得意分野を際立たせるために、非常に的を絞った内容になっています。
6.3 実践的な事例での検証結果
Chuck Jazdzewski: より実世界に近い例では、すでに改善の効果が確認できています。Materialのボタン実装のプロトタイプにおいて、Styleベースのバージョンに切り替えたところ、アロケーション数が約37%削減され、次のピクセルを描画する速度も1.6倍高速化しました。それでは、テーマについてお話しするために、再びRebeccaにバトンを渡します。
7. テーマへの統合とMediaQuery API
7.1 コンポーネントデフォルトスタイル層の導入
Rebecca Franks: ありがとう、Chuck。ここまでで、Styleを使ったComposeのスタイリングについて多くのことを学んできました。ここからは、これを皆さんのテーマにどう組み込んでいけるのかを見ていきましょう。冒頭でお話しした通り、アプリによってMaterialの採用度合いには様々なスペクトラムがあり、テーマにStylesをどう取り入れていくかも、その採用レベルによって変わってきます。それでは、実際にどうやってStylesを採用していけばよいのでしょうか。Stylesをアプリに組み込む方法としては、コンポーネントのデフォルトStyleをまとめたStyleオブジェクトをテーマ経由で公開することが挙げられます。これは、皆さんのデザインシステムがすでに持っているであろう既存のサブシステムの上に乗る、新しい層になります。例えばJetsnackでは、テーマレベルでcolors、shapes、typographyを定義しています。ここに、コンポーネントデフォルトスタイル層という新しい層を導入します。これは、colors、shapes、typographyといったトークン用のサブシステムにアクセスするStyleの集まりです。そうすると、ボタンのコンポーザブルは、以前のように各サブシステムに直接アクセスするのではなく、テーマからデフォルトのStyleを利用するようになります。ボタンがレンダリングされる際には、Meganが先ほど説明した優先順位の階層に従ってStyleを解決していくことになります。具体的には、ボタンに渡ってくるStyleパラメータ、テーマのボタンStyle、そしてツリーを遡って継承された属性を組み合わせることで、そのボタンの計算済みStyleを解決します。
7.2 Jetsnackでの実装例
Rebecca Franks: もしMaterial Designを使っていない場合であっても、自分たちのデザインシステムの中で今日からStylesを試すことができます。皆さんが該当するのは、次の2つの陣営のどちらかだと思います。1つはMaterialをまったく使っていない場合、もう1つはMaterialコンポーネントをラップしたカスタムテーマを持っている場合です。私たちのおすすめは、自分たちのテーマのコンポーネントの中でStylesを使うことであり、Materialコンポーネントの側がStyleパラメータを公開するようになったタイミングで、それをラップしたコンポーネントの定義の中で使っていくというアプローチです。それでは、Jetsnackの例を見てみましょう。このアプリは独自のカスタムテーマを持ちつつ、一部のMaterialコンポーネントも使用しています。私たちはすでに、colors、typography、shapesといった、Materialとは異なる独自のサブシステムを備えたカスタムテーマを用意していました。Stylesを採用するにあたっては、静的なオブジェクトを公開する新しいJetsnackStylesオブジェクトを、テーマ上のプロパティとして追加します。また、Style自体の中からサブシステムのトークンに簡単にアクセスできるように、StyleScopeの拡張関数も追加します。Styleはcomposition localにアクセスできます。そして、このJetsnackStylesオブジェクトの中に、各コンポーネントのデフォルトを作成していきます。例えば、先ほど定義していたButtonStyleをこのクラスの中に移し、ハードコードされていた色を、各サブシステムのテーマレベルのトークンを使うように切り替えます。例えばここでは、backgroundを、私たちのカスタムデザインシステムが持つ色のリストを使ったlinearGradientのbrushに設定しています。これによって、サブシステムが切り替わったときに、Styleも自動的に追従して変化するようになります。今回の例では、ダークモードからライトモードへの切り替えを行っていますが、色は自動的に適応します。デザインシステムのコンポーネント定義の中では、テーマレベルのStyleにアクセスし、渡ってきたStyleパラメータとテーマ側のStyleを組み合わせることで、Styleの効果を解決するようになり、その結果、私たちのボタンはテーマ対応になります。これで、このボタンを簡単に利用しつつ、追加のStyleを渡すことで特定のパラメータだけを上書きしてカスタマイズできるようになりました。この例では、異なるshapeを渡していますが、これはテーマレベルで設定されているデフォルトのStyleと組み合わされます。また、下層のcomposition localを使っているため、ボタンにはライトモードとダークモードそれぞれで正しくテーマが反映されていることも確認できます。
7.3 Material採用度合い別の推奨アプローチ
Rebecca Franks: それでは、Materialコンポーネントを使っている場合はどうでしょうか。Materialは、UIコンポーネントの中でStylesを採用する方向で進めており、これは今後のリリースで、Material 3から順次提供される予定です。もしまだMaterial 2のコンポーネントを使っている場合は、この機会にMaterial 3への移行をおすすめします。Materialを使う場合、Materialデザインシステム全体を利用しているケースもあると思いますが、その場合には、コンポーネントがStyleパラメータを提供しているときに、それをそのまま使えばよいということになります。もし皆さんのデザインシステムがMaterial 3ベースでありつつ、1つか2つだけグローバルに異なる部分がある場合には、テーマレベルでそれらを上書きできるようになります。Styleパラメータを公開しているMaterialコンポーネントであれば、思い通りにカスタマイズできるようになります。例えば、このMaterial 3のボタンにはStyleパラメータが用意される予定であり、実装をフォークすることなくMaterialコンポーネントを利用しつつ、background、shape、paddingなど、必要な要件に応じてカスタマイズできるようになります。これによって、これまで変更できなかったかもしれないMaterialコンポーネントの他のプロパティも、変更できるようになります。Materialが皆さん自身のカスタマイズを取り入れつつ進化していく様子については、ぜひ関連のセッションもチェックしてみてください。それでは、再びMeganにバトンを渡します。
7.4 MediaQuery API
Megan Mehta: ここまで、Style APIについて、そしてそれをテーマにどうセットアップして組み込んでいくかについて、詳しくお話ししてきました。ただ、テーマをデバイスの特性に応じて適応させる方法も別に用意されています。私たちは、MediaQueryと呼ばれる、もう1つの実験的なAPIを導入しました。このAPIが提供するのは、ウィンドウの特性に簡単にアクセスできる仕組みであり、これによってデバイスのスペクトラム全体で使える単一のテーマを構築できるようになります。このAPIはStyle APIと連携して動作し、デバイスのプロパティに応じてStyleを変化させる、使いやすいクエリを提供します。簡単な例を見てみましょう。Androidがますます多くのデバイスに適応できるようになる中で、皆さんもこうした様々な適応に、どうすれば簡単に対応できるのか気になるところだと思います。例えば、マウスが接続されている場合、pointerPosition変数はfineなポインタ位置を報告するようになり、これはより小さなデバイスターゲットをクリックできる能力を持つことを意味します。MediaQueryに基づいてStyleプロパティを変更するには、次のような例を使うことができます。これは単一のBoolean値だけを返すものであり、条件が変化するとコンポジションの後にStyleが無効化され、再解決されることになります。ここでは、マウスが接続されているデバイス上で、これらのボタンの集まりがpaddingとsizeを変更する様子を確認できます。一番下にあるボタンは、まったく同じコンポーザブルを使っているにもかかわらず、全体的に小さく見えます。デバイスのプロパティに基づいて、Styleが自動的に調整されているということです。最新のMediaQuery APIについては、こちらのリンクからさらに詳しく確認できます。また、「Make Your UI Adaptive with Android Studio's AI agent」というセッションもぜひチェックしてみてください。あるいは、この会場にいるDonとJoseに直接質問していただいても構いません。それでは、再びChuckにバトンを渡します。
8. モディファイアとStyleの使い分け、まとめ
8.1 設計思想の違い
Rebecca Franks: 立ってもらう?
Chuck Jazdzewski: 私が立ちます。さて、この段階まで来ると、皆さんはこう思うかもしれません。modifierとStyle、一体どちらを選べばいいのだろうか、と。ここまで見てきた通り、この2つのシステムには重なり合う部分もあります。改めて確認しておきたいのですが、Stylesはmodifierを置き換えるためのものではなく、見た目に関するパラメータを置き換えるためのものです。両者を比較すると、modifierは低レベルな機能として設計されており、振る舞いやセマンティクス、加算的なプロパティ、複雑なレイアウトを扱う手段としては、依然としてmodifierが担う領域です。一方でStyleは、見た目や個々のサイジング、テーマ化可能なプロパティのために設計された、高レベルなmodifierという位置づけになります。Stylesはmodifierより1段上のレイヤーにあり、modifierの上に構築されています。ロジックの観点で見ると、modifierは加算的であるのに対し、Styleは上書き的です。テーマ化という観点では、modifierはそもそもテーマレベルに引き上げられることを想定して設計されていないため、かなり扱いにくい面があります。一方でStyleは、テーマの中で使われることを前提に設計されています。パフォーマンスの観点でこの2つを比較すると、これはもちろん一概には言えない部分もありますが、Styleは設計上コンポジションをスキップできるのに対し、modifierはしばしば3つのフェーズすべてを必要とします。そしてアニメーションに関しては、modifierは別途アニメーション用のプリミティブが必要になるのに対し、Styleにはアニメーションが組み込まれています。
8.2 用途別の選択基準
Chuck Jazdzewski: では、StyleとmodifierのどちらをUsersが選べばよいのでしょうか。既存コンポーネントのデフォルトを上書きしたいとき、高パフォーマンスなインタラクティブな状態アニメーションを実現したいとき、あるいはコンポーネントに対してテーマ全体で共通のプロパティ群を定義したいときには、Styleを選んでください。一方で、clickableのような振る舞いや他のジェスチャーを扱いたいとき、独自の1回限りのレイアウトを定義したいとき、あるいはコンポーザブルに加算的なプロパティを持たせる必要があるときには、modifierを選んでください。それでは、締めくくりのために再びRebeccaにバトンを渡します。
8.3 フィードバック募集と今後の案内
Rebecca Franks: ぜひ皆さんには、Composeの中でStylesを実際に試していただき、このAPIについてご意見があれば私たちに教えてください。改めてお伝えしておきますが、このAPIは実験的なものであり、皆さんからいただくフィードバックに応じて今後変更される可能性があります。すでに、こちらのリンクから利用できる充実したドキュメントも用意しています。さらに、もし皆さんが独自のカスタムデザインシステムへの移行や、あるいはComposeでの基本的な使い方をサポートしてくれるエージェントと一緒に、新しいStylesスキルを試してみたいということであれば、こちらのリンクからスキルにアクセスするか、新しいAndroid CLIを使ってスキルをダウンロードすることもできます。本日はご参加いただき、本当にありがとうございました。