この記事は、ニフティグループ Advent Calendar 2022 24日目の記事です。
はじめに
昨日に引き続き、会員システムグループの山田です。
前回はJetpack Composeのよかった点についてでしたが、今回はイマイチだった点になります。
イマイチだった点
前回同様、記載するコードは簡略化の都合上、一部の属性値などを省略しています。
Navigation Composeがつらい
Jetpack Composeで画面遷移を実装しようとする場合、手段は大きく2通りあります。
- FragmentでComposeを描画し、Activity上でFragmentを切り替えることにより画面遷移する
- fragmentTransactionを使うか、Navigation Componentを利用する
- Fragmentを使わず、Activity上で描画するCompose関数を切り替える
後者を実現するためのライブラリがNavigation Composeです。これはNavigation ComponentのJetpack Compose版なのですが、使い勝手が大きく異なっています。
定義の違い
Navigation Componentでは、画面遷移の定義は以下のようにXMLで行っていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" app:startDestination="@id/fragment1"> <fragment android:id="@+id/fragment1" android:name="com.example.navigationsample.Page1Fragment"> <action android:id="@+id/action_fragment1_to_fragment2" app:destination="@id/fragment2" /> </fragment> <fragment android:id="@+id/fragment2" android:name="com.example.navigationsample.Page2Fragment" android:label="Fragment2" tools:layout="@layout/fragment_page2"/> </navigation> |
- <navigation>の中に各画面を<fragment>として設置し、IDやFragmentのクラスなどを指定
- 最初に表示される画面はstartDestination属性で指定
- 画面間の遷移は<action>として指定
一方、Navigation Composeでは以下のように指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Composable fun Router() { val navController = rememberAnimatedNavController() NavHost( navController = navController, startDestination = "page1", ) { composable("page1") { Page1(navController) } composable("page2") { Page2(navController) } } } |
- NavHost関数の中に各画面をcomposableとして設置し、呼び出すComposable関数を指定
- 最初に表示される画面は引数のstartDestinationで指定
- 画面間の遷移の定義は存在しない
actionの定義がない以外はNavigation Componentとほぼ同じような記述ですが、使ってみると辛さが表れてきます。
画面遷移が型安全でない
Navigation ComponentではSafe Argsという機能があり、XMLの定義からクラスを自動生成してくれます。これを利用して画面遷移が以下のように行えました。
1 2 3 |
val action = Page1FragmentDirections .actionFragment1ToFragment2() findNavController().navigate(action) |
Page1FragmentDirectionsが自動生成されたクラスで、これはXMLに記載されたactionの定義から作られています。このため、XMLに記載されていない遷移を呼び出すことを防止できていました。
一方、Navigation Composeでは以下のようになります。
1 |
navController.navigate("page2") |
画面遷移は遷移先の画面を文字列で指定します。誤った文字列を与えてしまうことも起こり得ますし、リファクタリングで名前を変えたとしても追従できません。
画面間の引数渡しがつらい
画面遷移時に画面間でデータの受け渡しを行おうとするとより辛くなります。
Navigation Componentではactionに引数を設定することができます。
1 2 3 4 5 6 7 8 |
<action android:id="@+id/action_fragment1_to_fragment2" app:destination="@id/fragment2"> <argument android:name="text" app:argType="string" android:defaultValue="hoge" /> </action> |
こうして設定された引数や引数の型は自動生成クラスにも反映されるので、以下のように型安全に利用できます。
1 2 3 |
val action = Page1FragmentDirections .actionFragment1ToFragment2("hoge") findNavController().navigate(action) |
内部ではBundleが利用されており、引数がBundleに詰められて遷移先Fragmentに渡されることになります。
一方でNavigation Composeでは以下のようになります。
1 2 3 4 5 6 7 8 9 10 |
composable( "page2/{text}", arguments = listOf( navArgument("text") { type = NavType.StringType } ) ) { backStackEntry -> val text = backStackEntry.arguments?.getString("text") Page2(navController, text) } |
画面間のデータ渡しはURL形式の文字列により行われます。必須引数はパスパラメータ、非必須パラメータはクエリパラメータの形で記述します。
遷移の呼び出し側は以下のようになります。
1 |
navController.navigate("page2/hoge") |
ここでも型安全性が失われています。文字列としてはなんでも渡すことができてしまうため、ビルド時に誤りに気づくことは不可能です。
加えてURL形式文字列であることもネックで、
- 引数にスラッシュなどが含まれる
- 引数がバイト列である
などの場合はエスケープやBASE64エンコードを自分で行う必要があります。当然、受け取る側は逆の処理が必要になります。
これらをなるべく安全に扱えるように、マイ ニフティではsealed classによる遷移先管理を行っています。Googleの公式サンプルでは定数値やenumなどで管理を行っているようです。
1 2 3 4 5 6 7 8 |
sealed class MainRoute(val route: string) { object Page1 : MainRoute("page1") object Page2 : MainRoute("page2/{text}") { fun createRoute(text: string) { return "page2/${text}" } } } |
遷移アニメーションの不具合
Navigation Componentでは画面遷移時のアニメーションが設定できたのですが、Navigation Composeにはその機能が(2022年12月現在)存在しません。
従来あった機能でJetpack Composeに未実装な機能を補完するものとして、Accompanistライブラリが存在しています。事実上の半公式ライブラリで、ここにNavigation Animationというものが存在するのでこちらを併用することになるのですが、これを利用したアニメーションに不具合があります。
まずはNavigation Componentで実装したものです。
1つ目の画面の上に2つ目の画面がスライドインし、戻る操作でスライドアウトします。
一方でNavigation Composeでの実装です。
スライドインは正常に行われるのですが、スライドアウトがおかしくなっています。
現状のAccompanistの実装では画面のスタック状態を考慮しておらず、常に遷移先の画面が上に描画されます。このため、戻る操作を行うと描画順が逆転してしまい、このような結果になります。
マイニフティではこれを解消できなかったため、遷移時に画面同士が重ならないようなアニメーションのみを利用することで回避しています。
先月公開されたAccompanist 0.27.1でz-orderの変更が入ったため、現在ではこの不具合は修正されている可能性があります。
AppBarを操作できない
従来のActivityとFragmentによる実装の場合、画面間で共通して利用するAppBarやBottomAppBarのようなコンポーネントはActivityで実装し、切り替わる画面のみをFragmentで実装することが多かったかと思います。マイ ニフティを従来の方法で実装するなら以下のように分割したでしょう。
共通利用とは言いつつ、AppBarの表示内容は画面によって異なることが多いかと思います。AppBarの領域はFragmentの管理外になるはずですが、FragmentのonCreateMenu()を利用することで例外的に書き換えが可能でした。
Navigation ComposeではFragmentを利用しないため、このような書き換えが不可能です。 画面別に出し分けを行うには、上位の現在の画面状態をStateに持って
1 2 3 4 5 6 7 8 9 |
TopAppBar( title = { if (currentScreen == "page1") { Text("Page1") } else { ... } } ) |
のような分岐処理を入れざるを得ず、画面数が増えるほど分岐が増える好ましくない実装になってしまいます。
非効率にはなってしまいますが、マイ ニフティでは共通部分を作らず、各画面で別々にAppBarを持たせる実装としています。
従来のViewとの連携
Jetpack Composeがリリースされたとはいえ、すべての機能がJetpack Composeで記述できるようになったわけではありません。WebViewをはじめとして、旧来の仕組み(View)でしか記述できないものは残っています。
Viewとの混在は想定されていて、例えばComposeの中でWebViewを使いたければAndroidViewというものがあります。単純な実装ではこれで十分でしょう。
1 2 3 4 5 6 7 8 9 |
@Composable fun ComposeWebView(url: string) { AndroidView( factory = { context -> WebView(context) }, update = { view -> view.loadUrl(url) } ) } |
しかしマイ ニフティの場合はPull to Refreshの機能が存在しており、実装当初は
- Pull to RefreshをJetpack Composeで実装
- WebViewをAndroidViewでラップして実装
という状態でした。Pull to RefreshとWebViewはどちらもスクロールの機能を持つため、Nested Scrollを利用してスクロールを制御する必要があるのですが、これがJetpack ComposeとViewの間で伝播せず、どちらかのスクロールが機能しない状態となっていました。
このため、マイ ニフティではPull to Refresh機能ごとView側で実装しているのですが、この弊害としてWebView利用部分のプレビューが行えないという状態になっています。
一部端末での不具合
一部の端末で予期しない動作をすることがありました。例えばXiaomiのMIUI 13を搭載した端末において、作成していない真っ白の画面が挿入される問題を確認しており、issueに上げています。
ほとんどの端末では問題なく動作するのですが、特にOSに対するカスタマイズが多いメーカーの端末では注意する必要がありそうです。
おわりに
Jetpack Composeのイマイチな部分についてご紹介しました。
見ていただければ分かるとおり、ほとんどの問題はNavigation Composeによるものでした。 ここだけは従来の方法と比べて機能的なデグレードが大きく、明確に使いづらいと言える部分です。画面遷移を引き続きFragmentで行うような実装も可能なので、すべてJetpack Composeで書くことを諦めるということも十分選択肢に入るのではないかなと思います。
総合的にはJetpack Compose導入の利点の方が大きく、今後はこちらが主流になっていくと思われますので、まだ導入されていない皆様もぜひ導入を検討してみてください。
明日はたけろいどさんによる「サービス開発にSvelteKitを導入するために行なったアプローチ」です。お楽しみに。