このシリーズでは、Android と Google Play の製品情報で、日本の皆さんに特に重要な記事を見やすくお届けするために、 グローバルで発表された 2 週間分のブログ記事の URL を、1 つのブログ記事にまとめます。
*リンク先は英語の記事になります。お手数ですが Chrome ブラウザの翻訳機能などを使って、投稿を日本語でご確認ください。
Android
Jetpack Composeの安定性を最適化する新しい方法 (New ways of optimizing stability in Jetpack Compose)
新しい Samsung Galaxy の 折りたたみスマートフォンやスマートウォッチに対応するアプリを準備しよう(Prepare your app for the new Samsung Galaxy foldables and watches!)
この記事は Manuel Vivo による Android Developers - Medium の記事 " Introduction to Hilt in the MAD Skills series " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
MAD Skills 記事シリーズの Hilt についての記事です。この記事では、依存関係インジェクション(DI)が皆さんのアプリや Hilt にとって重要である理由について説明します。Hilt は、Android で DI を行うための Jetpack の推奨ソリューションです。
動画で視聴したい方は、こちらをご覧ください。
Android アプリで依存関係インジェクションの原理に従うことで、優れたアプリ アーキテクチャの土台を築くことができます。その結果、コードの再利用性が高まり、リファクタリングやテストが簡単になります。DI のメリットの詳細は、こちらをご覧ください。
プロジェクトでクラスのインスタンスを作成する場合、そのクラスが必要とする依存関係や推移的依存関係を満たしていくことで、依存関係グラフを手動で実現できます。
しかし、毎回これを手動で行うと、ボイラープレート コードが必要になり、エラーも起こりやすくなる可能性があります。たとえば、オープンソースの Google I/O アプリ iosched で利用している ViewModel をご覧ください。依存関係と推移的依存関係を含めると、FeedViewModel を作成するために必要なコードがどのくらいの量になるか想像できますか?
FeedViewModel
class FeedViewModel( private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase, loadAnnouncementsUseCase: LoadAnnouncementsUseCase, private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase, getTimeZoneUseCase: GetTimeZoneUseCase, getConferenceStateUseCase: GetConferenceStateUseCase, private val timeProvider: TimeProvider, private val analyticsHelper: AnalyticsHelper, private val signInViewModelDelegate: SignInViewModelDelegate, themedActivityDelegate: ThemedActivityDelegate, private val snackbarMessageManager: SnackbarMessageManager) : ViewModel(), FeedEventListener, ThemedActivityDelegate by themedActivityDelegate, SignInViewModelDelegate by signInViewModelDelegate { /* ... */}
class FeedViewModel(
private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
getTimeZoneUseCase: GetTimeZoneUseCase,
getConferenceStateUseCase: GetConferenceStateUseCase,
private val timeProvider: TimeProvider,
private val analyticsHelper: AnalyticsHelper,
private val signInViewModelDelegate: SignInViewModelDelegate,
themedActivityDelegate: ThemedActivityDelegate,
private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
FeedEventListener,
ThemedActivityDelegate by themedActivityDelegate,
SignInViewModelDelegate by signInViewModelDelegate {
/* ... */
}
これは難解で繰り返しが多いので、容易に間違った依存関係を取得してしまうこともあると思います。依存関係インジェクション ライブラリを使えば、依存関係を手動で提供することなく、DI のメリットを活用できます。必要なコードはライブラリがすべて生成してくれます。その際に活躍するのが Hilt です。
Hilt は Google が開発した依存関係インジェクション ライブラリです。Hilt を使えば手動で書かなければならないボイラープレートをすべて生成してくれるので、アプリで DI のベスト プラクティスを最大限に活用できます。
Hilt はアノテーションを使ってコンパイル時にコードを生成するので、実行はとても高速です。その際に利用するのが、JVM の DI ライブラリである Dagger です。Hilt は、Dagger をベースに作られています。
Hilt は Android アプリの Jetpack 推奨 DI ソリューションであり、ツールや他の Jetpack ライブラリのサポートも含まれています。
Hilt を使うアプリには、@HiltAndroidApp アノテーションを付けた Application クラスを含める必要があります。このアノテーションにより、コンパイル時に Hilt のコード生成が実行されます。また、Hilt がアクティビティに依存関係を注入するには、そのアクティビティに @AndroidEntryPoint アノテーションを付けておく必要があります。
@HiltAndroidApp
@AndroidEntryPoint
@HiltAndroidAppclass MusicApp : Application() @AndroidEntryPointclass PlayActivity : AppCompatActivity() { /* ... */ }
class MusicApp : Application()
class PlayActivity : AppCompatActivity() { /* ... */ }
依存関係を注入するには、Hilt から注入したい変数に @Inject アノテーションを付けます。Hilt が注入したすべての変数は、super.onCreate が呼び出されたときに利用できるようになります。
@Inject
super.onCreate
@AndroidEntryPointclass PlayActivity : AppCompatActivity() { @Inject lateinit var player: MusicPlayer override fun onCreate(savedInstanceState: Bundle) { super.onCreate(bundle) player.play("YHLQMDLG") }}
class PlayActivity : AppCompatActivity() {
@Inject lateinit var player: MusicPlayer
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(bundle)
player.play("YHLQMDLG")
この例では、PlayActivity に MusicPlayer を注入しています。しかし、Hilt は MusicPlayer 型のインスタンスを提供する方法をどのようにして認識しているのでしょうか。実は、この段階ではまだ認識していません。 Hilt にその方法を伝えるためにアノテーションを使います。
PlayActivity
MusicPlayer
クラスのコンストラクタに @Inject アノテーションを付けることで、Hilt にそのクラスのインスタンスの作成方法を伝えることができます。
class MusicPlayer @Inject constructor() { fun play(id: String) { ... }}
class MusicPlayer @Inject constructor() {
fun play(id: String) { ... }
アクティビティへの依存関係の注入に必要なのは、これだけです。とても簡単でしたね。最初の例は簡単で、MusicPlayer は他の型に依存していません。しかし、他の依存関係がパラメータとして渡されると、Hilt はそれを管理し、MusicPlayer のインスタンスを提供する際にその依存関係を満たさなければなりません。
実は、ここで示した例はとても簡単で、単純すぎるものです。しかし、これを手動で行う場合、どうするかを考えてみてください。
手動で DI を行う場合、必要な型を提供し、提供するインスタンスのライフサイクルを管理する 依存関係コンテナ クラスを作ることが考えられます。つまり、まさに Hilt が内部で行っていることです。
アクティビティに @AndroidEntryPoint アノテーションを付けると、PlayActivity と関連付けられた依存関係コンテナが自動的に作成され、管理されます。手動で行うこの実装を PlayActivityContainer と呼ぶことにしましょう。MusicPlayer に @Inject アノテーションを付けることで、MusicPlayer 型のインスタンスの提供方法をコンテナに伝えます。
PlayActivityContainer
// PlayActivity annotated with @AndroidEntryPointclass PlayActivityContainer { // MusicPlayer annotated with @Inject fun provideMusicPlayer() = MusicPlayer() }
// PlayActivity annotated with @AndroidEntryPoint
class PlayActivityContainer {
// MusicPlayer annotated with @Inject
fun provideMusicPlayer() = MusicPlayer()
そしてアクティビティでは、コンテナのインスタンスを作成し、それを使ってアクティビティの依存関係を設定します。これも Hilt ではアクティビティに @AndroidEntryPoint アノテーションを付けることで処理してくれます。
class PlayActivity : AppCompatActivity() { private lateinit var player: MusicPlayer // Created by Hilt when annotating the activity with @AndroidEntryPoint private lateinit var container: PlayActivityContainer override fun onCreate(savedInstanceState: Bundle) { // @AndroidEntryPoint also creates and populates fields for you container = PlayActivityContainer() player = container.provideMusicPlayer() super.onCreate(bundle) player.play("YHLQMDLG") }}
private lateinit var player: MusicPlayer
// Created by Hilt when annotating the activity with @AndroidEntryPoint
private lateinit var container: PlayActivityContainer
// @AndroidEntryPoint also creates and populates fields for you
container = PlayActivityContainer()
player = container.provideMusicPlayer()
ここまで説明してきたのは、クラスのコンストラクタに @Inject アノテーションを付けると、そのクラスのインスタンスの提供方法を Hilt に伝えられることです。また、@AndroidEntryPoint が付いたクラスの変数にこのアノテーションを付けると、Hilt はその型のインスタンスをそのクラスに注入します。
@AndroidEntryPoint は、アクティビティだけでなく、ほとんどの Android フレームワーク クラスに追加できます。このアノテーションは、そのクラスの依存関係コンテナのインスタンスを作成し、@Inject アノテーションが付いたすべての変数を設定します。
Application クラスに付けられた @HiltAndroidApp アノテーションは、Hilt のコード生成をトリガーにするだけでなく、Application クラスに関連付けられた依存関係コンテナも作成します。
Application
Hilt の基本について理解できたので、もう少し複雑な例を見てみましょう。次の例では、MusicPlayer のコンストラクタが依存関係 MusicDatabase を受け取ります。
MusicDatabase
class MusicPlayer @Inject constructor( private val db: MusicDatabase) { fun play(id: String) { ... }}
private val db: MusicDatabase
) {
ここでは、MusicDatabase のインスタンスを提供する方法を Hilt に伝えなければなりません。型がインターフェースである場合や、例えばライブラリから提供されているために自分がクラスを所有していない場合は、コンストラクタに @Inject アノテーションを付けることはできません。
アプリで、永続化ライブラリとして Room を使っているとしましょう。再度、PlayActivityContainer を手動で実装する場合について考えてみます。MusicDatabase を提供するとき、Room を使うと MusicDatabase は抽象クラスになります。そのため、依存関係を提供するコードを実行します。次に、MusicPlayer のインスタンスを提供するときに、MusicDatabase の依存関係を提供する(または満たす)メソッドを呼び出す必要があります。
class PlayActivityContainer(val context: Context) { fun provideMusicDatabase(): MusicDatabase { return Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() } fun provideMusicPlayer() = MusicPlayer( provideMusicDatabase() )}
class PlayActivityContainer(val context: Context) {
fun provideMusicDatabase(): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
fun provideMusicPlayer() = MusicPlayer(
provideMusicDatabase()
)
Hilt では、推移的依存関係について心配する必要はありません。Hlit はすべての推移的依存関係を自動的に取得しますが、MusicDatabase 型のインスタンスの提供方法を伝えておかなければなりません。これを行うために、Hilt のモジュールを使います。
Hilt のモジュールとは、@Module アノテーションが付いたクラスです。このクラスでは、ある型のインスタンスの提供方法を Hilt に伝える関数を作成できます。Hilt が認識するこの情報は、Hilt の専門用語で バインディング とも呼ばれます。
@Module
@Module@InstallIn(SingletonComponent::class)object DataModule { @Provides fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase { return Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() }}
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
@Provides アノテーションが付いた関数で、MusicDatabase 型のインスタンスの提供方法を Hilt に伝えています。関数本体には、Hilt が実行するコードのブロックが含まれており、先ほどの手動実装のコードとまったく同じものです。
戻り値の型が MusicDatabase となっているので、Hilt はこの関数が提供する型を認識できます。また、関数のパラメータから、対応する型の依存関係も認識できます。この例では、既に Hilt が利用できる ApplicationContext がパラメータになっています。このコードから、Hilt は MusicDatabase 型のインスタンスの提供方法を認識します。別の表現を使うなら、MusicDatabase の バインディング を取得したことになります。
ApplicationContext
Hilt のモジュールには、@InstallIn アノテーションも付いています。これは、この情報がどの依存関係コンテナやコンポーネントで利用できるかを示します。では、コンポーネントとは何でしょうか。この点について詳しく説明しましょう。
@InstallIn
Hilt が生成する コンポーネント クラスは、先ほど手動でプログラミングしたコンテナのように、型のインスタンスを提供する役割を担います。Hilt はコンパイル時にアプリケーションの依存関係グラフをたどり、すべての推移的依存関係の型を提供するコードを生成します。
Hilt が生成する コンポーネント クラスは、型のインスタンスを提供する役割を担う
Hilt は、ほとんどの Android フレームワーク クラスに対して、コンポーネント、つまり依存関係コンテナを生成します。各コンポーネントの情報(バインディング)は、コンポーネント階層を伝播します。
Hilt のコンポーネント階層
MusicDatabase バインディングが Application クラスに対応する SingletonComponent で利用できる場合、その他のコンポーネントでも利用できます。
SingletonComponent
これらのコンポーネントは、コンパイル時に Hilt によって自動生成されます。コンポーネントが作成、管理され、対応する Android フレームワーク クラスに関連付けられるのは、これらのクラスに @AndroidEntryPoint アノテーションを付けたときです。
モジュールの @InstallIn アノテーションは、そういったバインディングが利用できる場所や、利用できる他のバインディングを管理するうえで便利です。
再び、手動で作成した PlayActivityContainer コードについて考えてみます。気づいた方もいらっしゃるかもしれませんが、MusicDatabase の依存関係が必要になるたびに、別のインスタンスが作成されています。
アプリ全体で MusicDatabase の同じインスタンスを再利用したい場合もあるので、この動作は理想的ではありません。そこで、関数を使うのではなく、変数に格納すれば同じインスタンスを共有できます。
class PlayActivityContainer { val musicDatabase: MusicDatabase = Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() fun provideMusicPlayer() = MusicPlayer(musicDatabase)}
val musicDatabase: MusicDatabase =
Room.databaseBuilder(
fun provideMusicPlayer() = MusicPlayer(musicDatabase)
つまり、MusicDatabase 型のスコープがこのコンテナに適用されるようにすることで、依存関係として常に同じインスタンスが提供されるようにしています。これを Hilt で行うには、どうすればよいでしょうか。もうおわかりと思いますが、ここでも別のアノテーションを使います。
@Provides メソッドに @Singleton アノテーションを付けると、そのコンポーネントでは常にこの型の同じインスタンスを共有するように Hilt に伝えることができます。
@Singleton
@Module@InstallIn(SingletonComponent::class)object DataModule { @Singleton @Provides fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase { return Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() }}
@Singleton はスコープ アノテーションです。それぞれの Hilt コンポーネントには、1 つのスコープ アノテーションが対応付けられています。
それぞれの Hilt コンポーネントのスコープ アノテーション
ある型のスコープを ActivityComponent にしたい場合、ActivityScoped アノテーションを使います。スコープ アノテーションはモジュールで利用できますが、コンストラクタに @Inject アノテーションが付いているクラスでも利用できます。
ActivityComponent
ActivityScoped
バインディングには次の 2 つのタイプがあります。
Hilt は、ViewModel、Navigation、Compose、WorkManager といった人気のある Jetpack ライブラリと統合されています。
ViewModel 以外と統合する場合は、別のライブラリをプロジェクトに追加する必要があります。詳細は、ドキュメントをご覧ください。このブログ投稿の冒頭で紹介した iosched の FeedViewModel コードを覚えているでしょうか。Hilt を使うと、どのようになるか見てみましょう。
@HiltViewModelclass FeedViewModel @Inject constructor( private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase, loadAnnouncementsUseCase: LoadAnnouncementsUseCase, private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase, getTimeZoneUseCase: GetTimeZoneUseCase, getConferenceStateUseCase: GetConferenceStateUseCase, private val timeProvider: TimeProvider, private val analyticsHelper: AnalyticsHelper, private val signInViewModelDelegate: SignInViewModelDelegate, themedActivityDelegate: ThemedActivityDelegate, private val snackbarMessageManager: SnackbarMessageManager) : ViewModel(), FeedEventListener, ThemedActivityDelegate by themedActivityDelegate, SignInViewModelDelegate by signInViewModelDelegate { /* ... */}
@HiltViewModel
class FeedViewModel @Inject constructor(
この ViewModel のインスタンスを提供する方法を Hilt に伝えるために、コンストラクタに @Inject アノテーションが付いています。その点を除けば、クラスに @HiltViewModel アノテーションを付けるだけです。
これでだけです!ViewModel のプロバイダを手動で作成する必要はありません。Hilt がそれを行ってくれます。
Hilt は、よく使われている別の依存関係インジェクション ライブラリ Dagger がベースになっています。今後のエピソードには、Dagger が頻繁に登場する予定です。現在 Dagger をご利用の場合、Dagger と Hilt は連携して動作できます。移行 API の詳細は、ガイド(英語)をご覧ください。
Hilt についてさらに詳しく知りたい方は、特によく使われるアノテーションやその効果、使用方法を説明したクイック リファレンス(英語)をご覧ください。Hilt のドキュメントのほかに、ハンズオン形式で学習できる Codelab も公開しています。
今回のエピソードは以上ですが、この話はこれで終わりではありません。MAD Skills シリーズはさらに続くので、Android Developers の Medium 記事 (英語) をフォローして、投稿されたときに確認できるようにしておきましょう。
Reviewed by Tamao Imura - Developer Marketing Manager, Google Play
この記事は Jonathan Koren による Android Developers - Medium の記事 " Trackr comes to the Big Screen " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Trackr は、タスク管理のサンプルアプリです。Trackr の主な用途は、ユーザー補助をサポートする観点で一般的な UI パターンを確認できるようにすることですが、最先端の Android 開発におけるベスト プラクティスの実例にもなっています。先日、このアプリを大画面対応にしました。その際にマテリアル デザインやレスポンシブ パターンを適用することで、大画面デバイスで洗練された直感的なユーザー エクスペリエンスをどのように実現したのかをご紹介します。
変更前 : タスク一覧画面の下部にあるアプリバーのメニューから、アーカイブと設定にアクセスできました。大画面では、タッチのターゲットとなるメニュー コントロールが小さく、不便な場所にありました。また、下のアプリバーが伸びすぎていました。
左: スマートフォンのナビゲーション。右: タブレットのナビゲーション
変更後 : 画面が広い場合は、ナビゲーション レール (英語) を表示します。さらに、ナビゲーション レールにフローティング アクション ボタン(新規タスク画面を開きます)を表示し、下のアプリバーは完全になくしました。
大画面のナビゲーション レール
この変更は大画面デバイスを念頭に行いましたが、スマートフォンの横表示でも有効で、タスク一覧を表示する縦のスペースを広く使えるようになります。
横向きのスマートフォンのナビゲーション レール
変更前 : タスク一覧画面とアーカイブ画面が横幅いっぱいに広がり、項目をタップすると、一覧が項目の詳細に置き換わっていました。大画面では、UI 要素が引き伸ばされるか、片側に寄ってしまい、画面のバランスが悪く感じられました。
スマートフォンでは自然に見えるが、大画面では最適な形でスペースを利用できていない
変更後 : タスク一覧画面とアーカイブ画面の両方で、SlidingPaneLayout (英語) を使って一覧/詳細 UI を表示します。これを行う方法は、Google I/O アプリに対して行った変更に関する以前の記事 (英語) で解説していますので、技術的な詳しい説明に興味がある方は、ぜひご覧ください。
タスク詳細画面にもフローティング アクション ボタン(タスク編集画面を開きます)がありますが、ナビゲーション レールがある場合に 2 つのフローティング アクション ボタンが表示されることになるので、理想的ではありません。そこで、2 つ目のフローティング アクション ボタンは非表示にし、右上のツールバーに編集ボタンを追加します。
2 ペイン化で画面スペースを活用
変更前 : タスクを編集すると、タスク編集 UI がタスク詳細の代わりに表示され、画面全体を覆っていました。タスク詳細と同じく、画面のバランスが悪く感じられます。新規タスク UI でも同じ問題が発生していました(実は、新規タスクとタスク編集は、ナビゲーション グラフで同じ遷移先になっています)。
左: スマートフォンのタスク編集。右: タブレットのタスク編集
変更後 : 大画面では、DialogFragment を使って他のコンテンツの上にタスク編集 UI を表示します。
ユーザーが現在の目的に集中できるようにするフローティング UI
最初は、タスク詳細の代わりにこの UI を詳細ペインに表示することを考えました。その方が単純でしたが、次の理由により、このアプローチでは納得できないとすぐに感じました。
新規タスク UI は、タスク編集と同じパターンを利用
ここでの学びは、何も考えずにシンプルにデザインしてしまうと、実装してみると機能的におかしい可能性があるということです。そうなったときは、一歩下がってユーザー エクスペリエンスに注目し、ユーザー エクスペリエンスを向上させるデザイン パターンを探しましょう。
タブレットや折りたたみ式デバイスの人気は上昇しています。そのため、レスポンシブな UI を作ることの重要性がこれまでになく高まっています。ナビゲーション レールを追加したり、SlidingPaneLayout を使ったりすると、Trackr アプリの見栄えがよくなるだけでなく、ユーザビリティが劇的に向上し、スマートフォンにはないエクスペリエンスを実現できることをお見せしました。さらに、単に画面のスペースだけではなく、ユーザビリティに注目し、ユーザーのニーズを最大限に満たすことができるようなデザインの再考が必要になる場合があることもお伝えしました。
新たに改善された Trackr が気に入ってもらえることを期待しています。コードは github.com/android/trackr から確認できます。
この記事は Ian Lake による Android Developers - Medium の記事 " Animations in Navigation Compose" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Jetpack Composeは、「時間があれば調整する」というアニメーションの考え方を「とても簡単なのでやらない手はない」に変えます。その際、大きな役割を果たすのが、画面レベルの遷移です。そのために次の 3 つの具体的な問題を解決する一連のソリューションを目指して Navigation Compose の開発が進められています。
上記のソリューションごとに微妙に異なるアプローチが必要になります。ここでは、その詳細について詳しく説明します。
Jetpack Compose は、最初の 0.1.0-dev01 リリースから最新の Compose 1.0.1 リリースまで、とても長い道のりを歩んできました。ビューの世界と比べて大きく改善された領域の 1 つが、アニメーションと遷移です。完璧なアニメーション API を追求し、Compose が 1.0.0 に進む中、多くの変更が行われました。
信じられないほど強力な animateTo() や animate*AsState() など、たくさんの低レベルのアニメーション API は安定版になりましたが、現時点では、Compose の土台を構成する多くの API が、@ExperimentalAnimationApi とマークされた構成要素に基づいています。
animateTo()
animate*AsState()
@ExperimentalAnimationApi
試験運用版 API(Kotlin の世界では @RequiresOptIn API を使っている API)は、今後変更される可能性がある API であることを表します。つまり、これらの API は、今後のリリースで変更、改善、置換される可能性があります。たとえば、Compose 1.1.0-alpha04 や 1.2.0-alpha08 などで変更されるかもしれません。そのため、試験運用版 API を使うライブラリでは、使っていた Compose のバージョンをアップデートする場合、そのライブラリ も 同時にアップデートしない限り、クラッシュしたり動作しなくなったりします(初期の Compose リリースを使っていた方なら、その苦しみがわかるはずです)。
@RequiresOptIn API
AndroidX リリース ページに記載されているように、Navigation や Compose を含むすべての AndroidX ライブラリは、セマンティック バージョニングに厳密 (英語)に従います。逆に、試験運用版でない API は、ライブラリがリリース候補(RC)フェーズに入ったタイミングで確定します。こういった安定版 API が互換性のない形で変更されるのは、メジャー バージョンが変更されるタイミング(「2.0」など)になります。
これは、上位互換性や下位互換性の観点で見れば素晴らしいことです。たとえば、新しいアルファ版を試すために Fragment のバージョンをアップグレードしつつ、他の依存関係は安定版リリースの状態を維持すれば、すべて問題なく動作します。
しかしながら、試験運用版 API は根底から覆る可能性もあるので、異なるアーティファクト グループにわたって試験運用版 API を使うことは厳しく禁じられています。つまり、androidx.fragment のバージョンをアップグレードしても、androidx.appcompat が動作しなくなることはありません。androidx.navigation や androidx.compose.animation も同様です。
androidx.fragment
androidx.appcompat
androidx.navigation
androidx.compose.animation
Navigation 2.4 は大きなリリースです。Navigation Compose の最初のリリースであるとともに、Navigation Compose と Fragment 対応の Navigation 、両方で複数バックスタックがサポートされた最初のリリースでもあるからです。つまり私たちは、ベータ版、RC 版を経て安定版に向かう準備として、残りの関連 API リクエストに対応する作業を行っています。
Navigation Compose では、Compose 1.0.1 を土台としつつ、Compose 1.1.0-alpha01 以降を利用するために移行したい人(または既に移行した人)のために上位互換性を持たせることが目的となります。
この上位互換性要件が意味するのは、Navigation Compose 2.4.0 のコードが安定版の Compose Animation API しか利用できないということです。私たちはこのようにして、Navigation 2.4.0-alpha05 にクロスフェードのサポートを追加できました。Compose の世界では、ジャンプカットは真っ先に排除すべきです。
安定版の Compose Animation API のみを使えるという制限により、Navigation 2.4 から AnimatedContent (英語)などの API を直接使うことはできないため、皆さんが Navigation 2.4 の一部として直接利用したい高度なアニメーション制御などが提供されない場合があります。ただし、Navigation は拡張可能なので、ベースとなるフレームワークは既に構築されており、それを利用できます。
AnimatedContent
今回リリースされた Navigation 2.4.0-alpha06 で Accompanist Navigation Animation を実現できたのは、このような宛先間のアニメーションがベースとしてサポートされているからです。Navigation Animation アーティファクトは、これまで利用されてきたバージョンの Navigation Compose API で実現できる一連のアニメーションを提供します。
rememberNavController()
rememberAnimatedNavController()
NavHost
AnimatedNavHost
import androidx.navigation.compose.navigation
import com.google.accompanist.navigation.animation.navigation
import androidx.navigation.compose.composable
import com.google.accompanist.navigation.animation.composable
一見、アプリの見た目は変わっていないように見えます。デフォルトのアニメーションは同じ種類の fadeIn と fadeOut のままで、Navigation 2.4 のクロスフェードがこれを行ってくれます。しかし、1 つの重要な新機能が利用できるようになります。それは、アニメーションを構成し、独自の画面遷移に置き換える機能です。
fadeIn
fadeOut
この制御は、すべての Composable の宛先に存在する 4 つの新しいパラメータを通して行います。
enterTransition
navigate()
exitTransition
popEnterTransition
popBackStack()
popExitTransition
パラメータはすべて同じ形式です。
enterTransition: ( ( initial: NavBackStackEntry, target: NavBackStackEntry ) -> EnterTransition?)? = null,
(
initial: NavBackStackEntry,
target: NavBackStackEntry
) -> EnterTransition?
)? = null,
受け取るのはラムダです。このラムダは、移動元(initial)と移動先(target)を表す NavBackStackEntry を受け取ります。たとえば enterTransition の場合、次に入る宛先が target で、それが enterTransition の適用対象となります。exitTransition はその逆で、initial 画面が exitTransition の適用対象です。
initial
target
NavBackStackEntry
つまり、宛先は次のように書くことができます。
composable( "profile/{id}", enterTransition = { _, _ -> // Let's make for a really long fade in fadeIn(animationSpec = tween(2000) }) { // Add content like normal}
"profile/{id}",
enterTransition = { _, _ ->
// Let's make for a really long fade in
fadeIn(animationSpec = tween(2000)
// Add content like normal
または、移動元や移動先に基づいてアニメーションを制御することもできます。
composable( "friendList" exitTransition = { _, target -> when (target.destination.route) { "profile/{id}" -> ExitTransition.fadeOut( animationSpec = tween(2000) ) // slowly fade it out else -> null // use the defaults } }) { // Add content like normal}composable( "profile/{id}", enterTransition = { initial, _ -> when (initial.destination.route) { "friendList" -> slideInVertically( initialOffsetY = { 1800 } ) // slide in the profile screen else -> null // use the defaults }) { // Add content like normal
"friendList"
exitTransition = { _, target ->
when (target.destination.route) {
"profile/{id}" -> ExitTransition.fadeOut(
animationSpec = tween(2000)
) // slowly fade it out
else -> null // use the defaults
composable(
enterTransition = { initial, _ ->
when (initial.destination.route) {
"friendList" -> slideInVertically(
initialOffsetY = { 1800 }
) // slide in the profile screen
この例では、友達リスト画面は、プロフィール画面に戻る際の遷移を制御しています。また、プロフィール画面は、友達リスト画面から入ってくる際の遷移を制御しています。これにより、2 つの宛先間でカスタムのスライド オーバー アニメーションを実現しています。この例からは、null を指定すると「デフォルトを使用」という意味になることもわかります。デフォルトは、親のナビゲーション グラフ、そして親の親のナビゲーション グラフというように、ルートとなる AnimatedNavHost までの階層によって決まります。つまり、AnimatedNavHost でグローバルな enterTransition と exitTransition を変更するだけで、デフォルトのアニメーション(ここでは、クロスフェードのタイミング)を設定できます。
null
1 つのサブグラフだけのデフォルトを変更したい場合は(たとえば、ログインフローでは常に水平スライド アニメーションを使う)、ネストしたグラフのレベルでアニメーションを設定することもできます。
navigation( startDestination = "ask_username" route = "login" enterTransition = { initial, _ -> // Check to see if the previous screen is in the login graph if (initial.destination.hierarchy.any { it.route == "login" }) { slideInHorizontally(initialOffsetX = { 1000 } } else null // use the defaults } exitTransition = { _, target -> // Check to see if the new screen is in the login graph if (target.destination.hierarchy.any { it.route == "login" }) { slideOutHorizontally(targetOffsetX = { -1000 } } else null // use the defaults } popEnterTransition = { initial, _ -> // Check to see if the previous screen is in the login graph if (initial.destination.hierarchy.any { it.route == "login" }) { // Note how we animate from the opposite direction on a pop slideInHorizontally(initialOffsetX = { -1000 } } else null // use the defaults } popExitTransition = { _, target -> // Check to see if the new screen is in the login graph if (target.destination.hierarchy.any { it.route == "login" }) { // Note how we animate from the opposite direction on a pop slideOutHorizontally(targetOffsetX = { 1000 } } else null // use the defaults }) { composable("ask_username") { // Add content } composable("ask_password") { // Add content } composable("register") {
startDestination = "ask_username"
route = "login"
// Check to see if the previous screen is in the login graph
if (initial.destination.hierarchy.any { it.route == "login" }) {
slideInHorizontally(initialOffsetX = { 1000 }
} else
null // use the defaults
// Check to see if the new screen is in the login graph
if (target.destination.hierarchy.any { it.route == "login" }) {
slideOutHorizontally(targetOffsetX = { -1000 }
popEnterTransition = { initial, _ ->
// Note how we animate from the opposite direction on a pop
slideInHorizontally(initialOffsetX = { -1000 }
popExitTransition = { _, target ->
slideOutHorizontally(targetOffsetX = { 1000 }
composable("ask_username") {
// Add content
composable("ask_password") {
composable("register") {
階層拡張メソッド (英語)を使って宛先が実際にログイングラフの一部であるかどうかを判断する方法に注目してください。このようにすると、ログイングラフ への 遷移とログイングラフ からの 遷移に、デフォルトの遷移(または上位レベルで設定した遷移)を使うことができます。
水平スライドのような方向性のある遷移では、この仕組みのおかげで、enterTransition と popEnterTransition との違いが非常に役立ちます。ある画面は右にスライドし、他の画面は左にスライドするような事態を避けることができます。
Accompanist は Jetpack ライブラリのブースター ロケットとしてはたらき、Compose 1.1 の開発が進行中でも、試験運用版機能をすぐに 利用できるようにします。
Accompanist Navigation Animation は、次のようにして追加します。
implementation "com.google.accompanist:accompanist-navigation-animation:0.16.1"
implementation
"com.google.accompanist:accompanist-navigation-animation:0.16.1"
Compose 1.0.1 をベースとした Navigation 2.4 と、試験運用版 API によって Compose 1.0 の限界を押し広げる Accompanist Navigation Animation の先には、別の景色、すなわち Compose 1.1 が見えています。Compose ロードマップ (英語)を見てみると、実に重要な 1 つの機能が予定されています。
共通要素遷移のサポート
Navigation 2.5 で目指しているのは、Compose 1.1 のすべての利点を Navigation Compose に導入することです。つまり、アニメーション API が試験運用版でなくなれば、直接 Navigation Compose に組み込めるようになります。さらに、共通要素遷移が利用できるようになり、それをサポートする API も構築できます。
さらに言えば、Accompanist Navigation Animation は一時的な対応と考えるべきです。Navigation Compose 自体が同じレベルのアニメーション API を提供すれば(皆さんのフィードバックに合わせて調整します)、直接それを使うことができ、Accompanist Navigation Animation は完全に削除できます。
短時間で機能を提供できる Jetpack ライブラリとして、安定性と、私たちが自らに課している上位互換性要件、下位互換性要件のバランスを取るのは、思うほど簡単なことではありません。Jetpack Compose は、Accompanist という大きな助けを借りて勢いを増し、このブースター ロケットが必要なくなるところまで加速していきます。Accompanist に時間を費やしてくれた Chris Banes (英語)を始めとするすべてのデベロッパー、Compose を支えるすべてのチーム、そして Android 開発の未来を形作ることに貢献してくださっているすべての皆さんに感謝します。
追伸: Navigation + Accompanist のメリットをさらに知りたい方は、新しい Accompanist Navigation Material (英語)をチェックしてみてください!
Navigation-Material の紹介 🧭🎨️ (英語)
この記事は Jolanda Verhoef による Android Developers Blog の記事 "Android Dev Challenge Finale: Weather app" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
#AndroidDevChallenge の最終週は、皆さんの創造性を輝かせましょう!先週はスピードの勝負でしたが、最終週となる今回で全力を出し切ってください。
UI はすべて Compose で構築する必要があります。気象データはダミーのもので構いません。提出するアプリは、最低でも英語をサポートする必要があります。
皆さんの作品は、以下の 4 つのカテゴリに基づいて審査します。
レイアウト、テーマ、グラフィックに関する Compose ドキュメントを確認し、美しいデザインの実装の参考にしてみましょう。アニメーションやジェスチャーの斬新な使用法も考えてみてください。コードの質は、アーキテクチャとテストによって改善できます。また、動作全般については、ユーザー補助機能を忘れずに確認するようにしましょう。
回答は、パブリック GitHub リポジトリで実装する必要があります。Github リポジトリ テンプレートのコピーを作成し、README の手順に従ってください。テンプレートには、Compose による基本的な Hello World! と継続的インテグレーション設定が含まれています。
Hello World!
※1 応募に関する詳細は必ず公式ルールをご確認ください。
今週も究極の 5G Google スマートフォン、Google Pixel 5 を獲得するチャンスです!4 つのカテゴリそれぞれの勝者と、最も優秀な作品に、Google Pixel 5 を進呈します。
Jetpack Compose の中核はデベロッパー コミュニティです。プロダクトをさらに使いやすく改善するために、皆さんのフィードバックをお寄せください。
この記事は Jolanda Verhoef による Android Developers Blog の記事 "Android Dev Challenge: Week 3 - Speed round" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
位置について... 用意… ちょっと待ってください!#AndroidDevChallenge 第 3 週の予定を空けておきましょう。本日 3 月 13 日に、同じタイムゾーンのデベロッパーとのスピード競争を開催します。日本の皆さんは、午前 10 時からのスタートです。Compose スキルが一番速い人が勝者ですので、ぜひチャレンジしてください。第 1 週と第 2 週の創造性あふれる作品はすべてすばらしいものでしたが、次に求められるのはスピードです。
UI はすべて Compose で構築する必要があります。また、課題のデザインで指定されるすべてのガイドラインに厳密に一致しなければなりません。実装にあたっては、テーマ、レイアウト、ナビゲーションに関する Compose ドキュメントを参考にしてください。ハンズオンの学習教材として Compose Pathway を試すこともできます。このチャレンジに役立つトピックを説明した Codelabs が含まれています。
回答は、GitHub リポジトリで実装する必要があります。こちらの Github リポジトリ テンプレートをコピーし、README に記載された手順に従ってください。テンプレートには、Compose による基本的な Hello World! と継続的インテグレーション設定が含まれています。
今週のチャレンジの賞品は、究極の 5G Google スマートフォン、Google Pixel 5 です。アジア太平洋地域タイムゾーンからの応募者の中で、課題のチャレンジを最速で実装して送信したデベロッパー 1 名に贈呈します。
この記事は Florina Muntenescu による Android Developers Blog の記事 "Android Dev Challenge: Week 2 - Countdown timer" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
3...2...1… 次のチャレンジの時間です!#AndroidDevChallenge の第 2 週にようこそ。第 1 週は、創造性あふれるたくさんの作品が寄せられ、とても楽しく見せていただきました。第 2 週の作品も楽しみにしています。では早速、今週のチャレンジを出題します。
UI はすべて Compose で作成する必要があります。実装にあたっては、状態とアニメーションに関する Compose ドキュメントを参考にしてください。ハンズオンの学習教材として Compose Pathway を試すこともできます。このチャレンジに役立つトピックを説明した Codelabs が含まれています。
チャレンジに参加するためには、GitHub リポジトリで実装する必要があります。こちらの Github リポジトリ テンプレートをコピーし、README に記載された手順に従ってください。テンプレートには、Compose による基本的な Hello World! と継続的インテグレーション設定が含まれています。
2 週目の賞品は、皆さんと一緒に作り上げるアート作品です。このチャレンジを成功させた最初の 500 名の方に、Jetpack Compose ポスターと Android ペン一式を差し上げます。ストレス解消のために皆さんだけの塗り絵を作りましょう。さらに、チーム Jetpack が悪しき UI から宇宙を救う物語が描かれた Jetpack Compose の限定マンガポスターもついています。
Reviewed by Hidenori Fujii - Google Play Developer Marketing APAC
この記事は The Jetpack Compose Team による Android Developers Blog の記事 "Android Dev Challenge: lift off with Jetpack Compose" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Jetpack Compose は、Android でネイティブ UI を作成する最新のツールキットです。強力なツールと直感的な Kotlin API によって、少ないコードですばやくアプリを作成できます。
本日 Jetpack Compose ベータ版をリリースしました。そこで、Jetpack Compose を使い始める皆さんのお役に立てるように、新たに #AndroidDevChallenge を開催します。Compose の機能を学び、導入する準備を始める絶好のチャンスです。
#AndroidDevChallenge では、Jetpack Compose で優れたアプリをすばやく作成する技術を身につけられるよう、これから 4 週間にわたって週単位のチャレンジを出題します。各チャレンジでは、「インサイトを開放する」をテーマに、アニメーションやマテリアル テーマ、Composable やリストなど、毎回 Compose の新しい領域を扱います。毎回のチャレンジの勝者に新しい賞品があり、Pixel 5 を含む 1000 個以上の賞品を準備しています*。最初のチャレンジは本日より始まります。
毎週提示される新しいチャレンジには、それぞれのルールとタスクがあります。本日より、毎週水曜日(日本時間木曜日)に、課題と締め切りについての詳しい説明を含むブログ投稿を公開します。いずれのチャレンジも、Text や List などの基本的な Composable から状態やアニメーションまで、Compose のメンタルモデルやさまざまな Compose API に親しむために役立ちます。
Text
List
チャレンジに参加するためには、GitHub リポジトリで実装する必要があります。すぐにスタートできるよう、Compose による基本的な Hello World! と継続的インテグレーション設定を含む Github リポジトリのテンプレートを作成していますのでご利用ください。
#AndroidDevChallenge 初回のチャレンジは、子犬の里親探しアプリの作成です。このアプリでは、概要画面で子犬の一覧を、詳細画面でそれぞれの子犬の詳細を表示する必要があります。エントリは、太平洋標準時 3 月 2 日 23 時59 分(日本時間 3 月 3 日 16 時 59 分)までにこちらから送信してください。
UI はすべて Compose で作成する必要があります。審査対象はアプリの UI レイヤーのみです。実装にあたっては、レイアウト、リスト、テキスト、ナビゲーションに関する Compose ドキュメントを参考にしてください。ハンズオンの学習教材として Compose Pathway を試すこともできます。このチャレンジに役立つトピックを説明した Codelabs が含まれています。
🐶 よりも 🐱 のほうが好きな方も大丈夫です。ペットの里親探しアプリであれば、どんな種類でも歓迎します。
多数のご応募をお待ちしています。
最初のチャレンジの賞品は、Compose で飛び回る際の相棒としてぴったりな、LEGO ブロックの Jetpack Compose スーパーヒーロー限定トロフィーです。チャレンジを成功させてエントリを送信した最初の 500 名には #AndroidDevChallenge 初週の勝者である証として、Android フィギュアのコレクションにこのトロフィーを追加できます。
*毎週のチャレンジに新しい賞品が設定されています。Google Pixel 5 が賞品になる週で、Google Pixel 5 が利用できない国にお住まいの方には、同程度の価値がある電子ギフトカードをお送りします。詳しくは公式ルールをご覧ください。
Reviewed by Yuichi Araki - Developer Relations Team and Hidenori Fujii - Google Play Developer Marketing APAC