ソフトウェア エンジニア
このセッションでは、全画面表示のタイルから、動的で部分的な高さの Wear ウィジェットへの進化について、技術的な観点から特集します。
統合された Jetpack Glance API と新しい Remote Compose エンジンの技術的なメリットを探り、これらが最新の Android 開発にシームレスに適合する仕組みを詳しく説明します。
最後に、最新の UX ガイダンスを共有し、次世代の Glanceable UI の構築に役立つ実践的な移行戦略について説明します。
文字起こし
こんにちは
私は Frank と申します。Wear OS の一目でわかるエクスペリエンスを担当しています。
スマートウォッチは、ユーザーにとって一日中欠かせないパートナーです。本日は、新しい Wear ウィジェット エクスペリエンスを活用する方法をご紹介します。
このセッションでは、最も重要な機能をユーザーのスマートウォッチに直接提供する方法を 2 つ紹介します。そのために使用する最新の API は、Android デベロッパーの皆様が Wear OS 向けの開発を身近で直感的に感じられるように設計されています。
本日は、この取り組みについて詳しく説明するため、私の優秀な同僚であり、デスクが隣の Yuri にも登壇してもらいます。
こんにちは、Yuri Schimke です。
私は Wear OS チームのエンジニアで、主に Remote Compose API の開発を担当しています。
本日は、Remote Compose の概要を簡単に説明した後、タイルから Wear ウィジェットへの移行プロセスについて説明します。
それでは、Frank に引き継ぎます。
これらのエクスペリエンスを強化するために、基盤となる UI スタックを進化させています。
Wear ウィジェットは、リモート UI 用の新しい Android フレームワークである Remote Compose を利用しています。
これにより、Jetpack Compose と密接に連携した宣言型 API を使用して、ウィジェットなどのアプリ外エクスペリエンスを構築できるようになります。
Remote Compose は、パフォーマンスとバッテリー効率を確保しながら、最新のグラフィックとレスポンシブなレイアウトをウィジェットに提供します。
また、UI をアプリケーション プロセスから切り離すことで、ウィジェットが安全にシリアル化され、さまざまな Android デバイスで簡単にレンダリングされます。
つまり、最小限の労力で、サポートされているほぼすべてのリモート サーフェスにこの豊富なエクスペリエンスを提供できます。
タイルからウィジェットへの移行をできるだけスムーズにするため、小と大の 2 つの標準コンテナサイズを導入します。
これらは、モバイルでウィジェットを作成する際にすでに使用している 2x1 と 2x2 のグリッドベースのウィジェット形式に密接に沿うように設計されています。
また、一部のハードウェア メーカーの Wear 7 以降では、Wear ウィジェットが他のアプリのウィジェットとともに、縦方向にスクロールするトレイに表示されるようになります。
たとえば、Samsung Galaxy Watch では、Wear ウィジェットは次のように表示されます。
プラットフォーム アーキテクチャは、ハードウェア メーカーが将来的にこのパターンを採用した場合に、縦スクロール トレイをサポートするように構築されています。
また、既存のユーザーが取り残される心配もありません。
Jetpack Glance Wear API は、Wear 4 までの下位互換性をサポートするように設計されています。
ウィジェットまたはタイルの水平トレイをサポートする古いデバイスでは、システムが処理を行います。
全画面表示モードをサポートすることをコードで指定するだけで、システムが大きなウィジェットを取得し、これらのデバイスで全画面表示の従来のタイルとして表示します。
では、ユリがコードを解説しながら、既存のタイルを Wear ウィジェットに移行する方法を具体的に説明します。
ありがとう、フランク。
新しい Wear ウィジェットをゼロから作成する場合は簡単です。
GlanceWearWidgetService を定義し、Remote Compose を使用して UI を構築するだけです。
ただし、アプリにすでに本番環境のタイルがある場合は、サービス、UI、データの両方を移行する必要があります。
この移行をできるだけスムーズにするため、既存のユーザーがシームレスに移行できるパスを用意しています。
サービスについては、デュアル サービスまたはシングル サービスの 2 つの戦略から選択できます。
デュアル サービス(推奨)では、古い Wear OS デバイス用に既存の TileService を維持し、Wear OS 7 以降用に新しい GlanceWearWidgetService を追加します。
一方、単一サービスでは、両方のプロトコルを処理する新しい単一の GlanceWearWidgetService を作成し、互換モードで実行して、古いデバイスでウィジェットをタイルに変換します。
いずれの場合も、ユーザーにリーチし続けることができます。
影響を受けるのは、今後メンテナンスするコードのみです。
Android デベロッパー サイトに、これら 2 つのパターンの使用方法と使用するタイミングを詳しく説明した包括的なドキュメントを公開しました。
アプリに最適な戦略を判断する際の参考にしてください。
次に、UI レイヤについて説明します。
移行すると、メンタルモデルが変化します。
以前は、タイルは Wear タイル専用に構築されたライブラリである ProtoLayout を使用していました。
一方、Wear ウィジェットは Remote Compose をベースに構築されています。
Remote Compose は、Jetpack Compose UI とまったく同じ基盤を共有し、同じコア コンパイラとランタイムを使用します。
また、Foundation や Material 3 などの Compose UI ライブラリとよく似た、親しみやすいデザインになっています。
ただし、ボックス、列、テキストの代わりに、RemoteBox、RemoteColumn、RemoteText に置き換えます。
この場合、「remote」と記述することに慣れる必要があります。
さらに、ワークフローを高速化するために、ウィジェット専用に設計された、すぐに使用できる Remote Compose コンポーネントのフルセットが含まれています。
つまり、Figma で作成したデザインを含む既存の Wear Material 3 デザインをウィジェットに簡単に適応させることができるため、エンジニアとデザイナーの両方にとって使い慣れたものになります。
これらのフレームワークがどのように連携しているか、具体的な例を見てみましょう。
マテリアル 3 のスイッチボタンをベースに、独自のアプリ コンポーネントを構築します。
左側は、アプリ内に記述する標準の Wear Compose コンポーネントで、右側は Remote Compose バージョンです。
// Wear Compose
@Composable
fun HistoryEraserButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
checked: Boolean = true,
onCheckedChange: (Boolean) -> Unit = {},
)// Wear RemoteCompose
@RemoteComposable
@Composable
fun RemoteHistoryEraserButton(
onClick: Action,
modifier: RemoteModifier = RemoteModifier,
checked: RemoteBoolean = true.rb,
onCheckedChange: Action = Action.Empty,
)パターンがよく似ていることに注目してください。
ただし、Remote Compose コードを記述する場合は、RemoteComposable アノテーションを使用します。
また、修飾子やブール値などの使い慣れたパラメータを使用する場合、RemoteModifier や RemoteBoolean などの同等の Remote Compose タイプがあります。
また、ラムダの代わりにアクションを使用します。
ラムダ内で実行するコードを定義する代わりに、ペンディング インテントの起動、サービスの呼び出し、値の更新などのアクションを宣言する必要があります。
コンポーネントの本文に進みましょう。
テーマも揃っていることがわかります。
標準の Compose と同様に、マテリアル 3 のカラーパターンを Remote Compose コンポーネントに適用できます。これにより、ウィジェットをアプリのブランディングに正確に合わせることができます。
// Wear Compose
val colors = MaterialTheme.colorScheme
SplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser",
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
SwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { Text("History Eraser") },
secondaryLabel = { Text("Don't touch it!") },
)// Wear RemoteCompose
val colors = RemoteMaterialTheme.colorScheme
RemoteSplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser".rs,
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
RemoteSplitSwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { RemoteText("History Eraser".rs) },
secondaryLabel = { RemoteText("Don't touch it!".rs) },
)最後に、結果を見てみましょう。結果として得られる UI は、期待どおりの、クリーンでレスポンシブな、アニメーション化された切り替えボタンです。
違いを見分けるのは困難です。
デバイスやエミュレータで常に実行しなくてもウィジェット レイアウトを設計、反復、検証できるように、Wear ウィジェットの Compose プレビューのサポートを追加しました。
ウィジェットは、サイズ、形状、パディング、角丸など、OEM 固有のコンテナ制約に依存するため、標準のプレビュー アノテーションをベースに、これらの構成を挿入するための追加のパラメータ プロバイダを使用します。
@Preview(name = "Squircle Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetSquirclePreview(
@PreviewParameter(SquircleAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)
@Preview(name = "Round Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRoundPreview(
@PreviewParameter(RoundAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)このライブラリには、実際の Wear OS デバイスに一致する、事前構成済みの PreviewParameter プロバイダ実装が用意されています。
これらのプレビュー アノテーションを使用すると、さまざまなハードウェア形状でレイアウトを簡単に検証できます。
これらのプレビューは、レイアウトのテストだけでなく、オンデバイス ウィジェット ピッカーに必要な静止画像の生成にも使用できます。
@Preview(name = "Widget Picker Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRectangularPreview(
@PreviewParameter(RectangularAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)このライブラリには、切り抜かれていない完全な長方形のコンテナをエミュレートする、長方形のプレビュー形状のパラメータが用意されています。
この API の実際の動作を確認したい場合は、Horologist メディア サンプルアプリを更新して、この API を紹介する新しいウィジェットを追加しました。
メディア ウィジェットの小サイズと大サイズの両方を追加する方法を説明します。
GitHub で、以前の ProtoLayout コードと新しい Remote Compose 実装の比較を並べて確認できます。
きっと気に入っていただけると思います。
最後に、AI について触れずにこの話を終えることはできません。
移行を迅速に行えるよう、GitHub にタイルからウィジェットへの移行スキルを新たに公開しました。
お好みのエージェントで使用することも、Android CLI を使用して Android Studio に直接追加することもできます。
android skills add wear-widgets
android skills add --allでは、ここで François に戻して、まとめに入ります。
ありがとうございました
Yuri さん、ありがとうございます。
このように、タイルを Wear ウィジェットに進化させるプロセスは非常に簡単です。
まとめると、Jetpack Glance Wear と Remote Compose に移行することは、単に UI を更新するだけでなく、より統合された Android ウィジェット エコシステムに参加することを意味します。
Jetpack Compose と完全に連携する最新の宣言型フレームワークを使用して、美しい下位互換性のあるウィジェットを作成できるようになりました。
タイルの移行は今すぐ開始できます。詳しくは、Android デベロッパー サイトのスタートガイドをご覧ください。
皆様がスマートウォッチ向けに、表現力豊かで一目でわかるエクスペリエンスを構築されることを楽しみにしています。
本日はご参加いただき誠にありがとうございました。引き続きカンファレンスをお楽しみください。
ありがとうございました
// Wear Compose
@Composable
fun HistoryEraserButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
checked: Boolean = true,
onCheckedChange: (Boolean) -> Unit = {},
)// Wear RemoteCompose
@RemoteComposable
@Composable
fun RemoteHistoryEraserButton(
onClick: Action,
modifier: RemoteModifier = RemoteModifier,
checked: RemoteBoolean = true.rb,
onCheckedChange: Action = Action.Empty,
)// Wear Compose
val colors = MaterialTheme.colorScheme
SplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser",
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
SwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { Text("History Eraser") },
secondaryLabel = { Text("Don't touch it!") },
)// Wear RemoteCompose
val colors = RemoteMaterialTheme.colorScheme
RemoteSplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser".rs,
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
RemoteSplitSwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { RemoteText("History Eraser".rs) },
secondaryLabel = { RemoteText("Don't touch it!".rs) },
)@Preview(name = "Squircle Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetSquirclePreview(
@PreviewParameter(SquircleAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)
@Preview(name = "Round Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRoundPreview(
@PreviewParameter(RoundAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)@Preview(name = "Widget Picker Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRectangularPreview(
@PreviewParameter(RectangularAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)android skills add wear-widgets
android skills add --all