Android JetpackのNavigationを試して導入手順をまとめた
Google IO 2018で発表されたJetpackの新しいコンポーネントのNavigationをアプリに導入してみた。
TL;DR
Toolbar
、Bottom Navigation
、onClickイベント
に関わる画面遷移をNavigationに置き換えてみた。- 試した結果、コード短縮をゲットできるのでNavigation導入していきたい。
- Backボタン、Upボタンの挙動を制御してくれるNavigation氏、助かる。
- Bottom Navigationにアニメーションが入ってしまうの厄介(回避策をまとめた)。
- safe-argsの威力がすごかった。これ使うだけでも良さそう。
- onClickイベントだけNavigationに置き換え、とかできるのでプロジェクトに合わせて導入場面をチョイスできそう。
モチベーション
このエントリを通してNavigationを実践投入することがモチベーション。Toolbar
、Bottom Navigation
、onClickイベント
に関わる画面遷移をNavigationでどのように実現するのかをコードと共にまとめていく。
gradle settingにnavigationを追加する
build.gradleに依存を追加していく。
implementation "android.arch.navigation:navigation-fragment:1.0.0-alpha01" implementation "android.arch.navigation:navigation-ui:1.0.0-alpha01" implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha01" implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-alpha01"
これでNavigationを使える準備はOK。
Navigation Editorを使い画面遷移を定義していく
※ Navigation Editorを使うにはAndroid Studio 3.2が必要
このエントリの画面遷移は単純でHome画面、天気予報一覧画面と詳細画面の3つとToolbarのメニューから選択ができる設定画面がある。
resにnavigation
フォルダを追加すると新規作成メニューから Navigation Resource File
が選択できる。
Navigation EditorをつかってFragment、Activityの遷移を定義した。Fragmentの遷移は天気予報一覧(forecastsFragment)から詳細画面(forecastFragment)に矢印が結ばれていることで表現されている。homeFragmentやsettingsActivityはToolbarやBottomNavigationからの遷移があるためEditorで追加をした。
フラグメント間の遷移のtransitionはEditorから定義できる。今回のエントリでは詳細に使っていないので内容については割愛。
homeFragmentは開始位置として定義しているのでhomeアイコン
が表示されている。
Editorからも遷移を定義できるがxmlからも修正ができる。xmlは次のように定義されている。xmlファイルは nav_graph.xml
として保存している。
<?xml version="1.0" encoding="utf-8"?> <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:animateLayoutChanges="false" app:startDestination="@id/homeFragment" > <fragment android:id="@+id/homeFragment" android:name="me.soushin.sunshine.ui.home.HomeFragment" android:label="HomeFragment" /> <fragment android:id="@+id/forecastsFragment" android:name="me.soushin.sunshine.ui.forecast.ForecastsFragment" android:label="ForecastsFragment" > <action android:id="@+id/toForecast" app:destination="@id/forecastFragment" /> </fragment> <fragment android:id="@+id/forecastFragment" android:name="me.soushin.sunshine.ui.forecast.ForecastFragment" android:label="ForecastFragment" > <argument android:name="forecast" app:type="string" /> </fragment> <activity android:id="@+id/settingsActivity" android:name="me.soushin.sunshine.ui.settings.SettingsActivity" android:label="activity_settings" tools:layout="@layout/activity_settings" /> </navigation>
MainActivityのfragmentエリアを修正する
Navigation導入前にfragmentの表示・切り替えを定義していたlayoutを次のようにfragmentタグを使い変更する。
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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:layout_width="match_parent" android:layout_height="match_parent" > <fragment android:id="@+id/navHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" ★ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="48dp" android:layout_marginBottom="56dp" app:defaultNavHost="true" ★ app:navGraph="@navigation/nav_graph" ★ /> </android.support.design.widget.CoordinatorLayout>
android:nameを androidx.navigation.fragment.NavHostFragment
とすることでNavigation機能のターゲットとなる。app:defaultNavHost
をtrueに設定するとBackボタンやUpボタンが連携される。app:navGraph
はNavigation Editorで定義した nav_graph
を指定している。
ここまででNavigationをコードから操作できる準備が整った。ここから各ナビゲーションエリアの遷移をコードで実現していく。
ToolbarとNavigaitonを連携させる
ToolbarとNavigaitonを連携させるには onOptionsItemSelected
とonSupportNavigateUp
を次のようにオーバーライドする。
class MainActivity : AbstractActivity() { val navController: NavController by lazy { findNavController(R.id.navHostFragment) } override fun onOptionsItemSelected(item: MenuItem): Boolean { return NavigationUI.onNavDestinationSelected(item, navController) || super.onOptionsItemSelected(item) } override fun onSupportNavigateUp() = navController.navigateUp() }
NavigationnUIにActionBarとNavcontrollerを渡すために setupActionBarWithNavController
をonCreate内でコールする。
class MainActivity : AbstractActivity() { val navController: NavController by lazy { findNavController(R.id.navHostFragment) } override fun onCreate(savedInstanceState: Bundle?) { findViewById<Toolbar>(R.id.toolbar).also { setSupportActionBar(it) setupActionBarWithNavController(navController) } } }
Navigationと連携させるための注意点
注意点としてNavigationが参照するメニューのIDとToolbarのmenu.xmlで定義するIDを一致させる必要がある。
# menu_main.xml <menu 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" tools:context="me.soushin.sunshine.app.MainActivity"> <item android:id="@+id/settingsActivity" ★ android:orderInCategory="100" android:title="@string/action_settings" app:showAsAction="never" /> </menu> --- # nav_graph.xml <?xml version="1.0" encoding="utf-8"?> <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:animateLayoutChanges="false" app:startDestination="@id/homeFragment" > <activity android:id="@+id/settingsActivity" ★ android:name="me.soushin.sunshine.ui.settings.SettingsActivity" android:label="activity_settings" tools:layout="@layout/activity_settings" /> </navigation>
settingsActivity
でidを一致させている。これでtoolbarの遷移がNavigationに切り替わった。
BottomNavigationとNavigaitonを連携させる
BottomNavigationもToolbarと同様にmenu xmlとnav_graph xmlのIDを一致させる必要がある。詳しくは後述するgithubのコードを参照して頂きたい。
Navigationと連携したコードはToolbarと同様に簡略化できる。次のようなコードになる。
class MainActivity : AbstractActivity() { val navController: NavController by lazy { findNavController(R.id.navHostFragment) } override fun onCreate(savedInstanceState: Bundle?) { findViewById<BottomNavigationView>(R.id.bottomNavigation)?.apply { setupWithNavController(navController) } } }
ボトムナビゲーションの切り替え(タブ切り替え)にアニメーションが入ってしまう
navigation-ui-ktxで用意されている setupWithNavController
を使うとボトムナビゲーションで選択したFragmentに切り替わるときにアニメーションが入ってしまう。Fragment間の遷移ではnavigation editorを使いtransitionを定義できるがボトムナビゲーションの切り替えのアニメーションは決め打ちでアニメーションが定義されてしまっている。
アニメーションが定義されているNavigationUIクラスのコードは次のようになっていた。
public class NavigationUI { private static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController, boolean popUp) { NavOptions.Builder builder = new NavOptions.Builder() .setLaunchSingleTop(true) .setEnterAnim(R.anim.nav_default_enter_anim) .setExitAnim(R.anim.nav_default_exit_anim) .setPopEnterAnim(R.anim.nav_default_pop_enter_anim) .setPopExitAnim(R.anim.nav_default_pop_exit_anim); // } }
NavigationUIを継承したり、このアニメーションを潰すスマートなやり方が見つからなかったので setupWithNavController
を独自で実装した。独自というよりはアニメーションを呼び出している部分をカットしただけのBottomNNavigationViewの拡張関数である。
fun BottomNavigationView.setupWithNavController(navController: NavController) { this.setOnNavigationItemSelectedListener { item -> try { navController.navigate(item.getItemId(), null, NavOptions.Builder().build()) true } catch (e: IllegalArgumentException) { false } } navController.addOnNavigatedListener { controller, destination -> val destinationId = destination.id val menu = this.getMenu() var h = 0 val size = menu.size() while (h < size) { val item = menu.getItem(h) if (item.getItemId() == destinationId) { item.setChecked(true) } h++ } } }
Fragment間の遷移にNavigaitonを連携させる
val onClick: (Forecast) -> Unit = { forecast: Forecast -> Bundle().apply { putParcelable(ForecastFragment.KEY_FORECAST, forecast) }.let { view.findNavController().navigate(R.id.toForecast, it) ★ } }
NavController#navigate
を呼び出せばNavigationと連携できる。第一引数のresIdにはnavigation editorで定義したactionのidを指定する。第二引数にはFragmentへ渡すargumentsを指定する。
このコードではresIdに誤りがある場合、予期しないFragment遷移となってしまう。またFragmentに渡すargumentsが本当に期待するものかは定かではない。ここでsafe-argsを使う出番である。
safe-argsを有効にする
Rootのbuild.gradleのdependencies/class_pathにsafe-argsを追加する
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navVersion"
モジュールのbuild.gradleにpluginを追加する
apply plugin: 'androidx.navigation.safeargs'
これでsafe-argsが有効になったのでプロジェクトをリビルドする。
ビルドが成功すると{from-Fragment}Directionsクラスと{to-Fragment}Argsクラスが生成される。生成されたクラスをナビゲーション遷移に使っていく。
{from-Fragment}Directionsクラスと{to-Fragment}Argsクラスを使う
遷移元のコードは次のようになる。
val onClick: (Forecast) -> Unit = { forecast: Forecast -> view.findNavController().navigate(ForecastsFragmentDirections.toForecast(gson.toJson(forecast))) }
ForecastsFragmentDirections
クラスを使い、safe-argsで生成されたtoForecastメソッドをコールしている。また引数にはnavigation editorで定義したargumentsを渡す。toForecast
メソッドはnav_graph.xmlで定義したaction idが参照されている。
# nav_graph.xml <navigation // > <fragment android:id="@+id/forecastsFragment" android:name="me.soushin.sunshine.ui.forecast.ForecastsFragment" android:label="ForecastsFragment" > <action android:id="@+id/toForecast" ★ app:destination="@id/forecastFragment" /> </fragment> <fragment android:id="@+id/forecastFragment" android:name="me.soushin.sunshine.ui.forecast.ForecastFragment" android:label="ForecastFragment" > <argument android:name="forecast" ★ app:type="string" /> </fragment> </navigation>
遷移先は次のようなコードになる。
class ForecastFragment : AutoDisposeFragmentKotlin() { private val forecast: Forecast by lazy { ForecastFragmentArgs.fromBundle(arguments).let { gson.fromJson(it.forecast, Forecast::class.java) } } }
ForecastFragmentArgs
クラスを使い遷移元から渡されたargumentsを参照できる。
safe-argsはNavigation定義から遷移元、遷移先に必要なクラスとメソッドを用意してくれる。これによりNavigation定義どおりの実装が可能になるしコード品質も格段に向上できる。
まとめ
- TL;DRにまとめたが、必要なところだけ徐々にNavigationに移行するのが良さそう。
- Fragment間を遷移するだけでNavigationがよしなにback stackを操作しているのでBackボタン、Upボタンの挙動に気を使う必要が無くなりそう。
- 既存プロジェクトでback stackを操作してFragment遷移履歴を残している場合は導入は慎重にいきたい。
- 新規プロジェクトであれば迷わずNavigationを導入すれば良いと思う。
- transitionやdeeplinkなどは試せていないので更にインプットしていきたい。
コード
これまでの紹介では断片的なコードとなっているためNavigationを有効にしたプルリクエストを残しています。参照いただき参考になれば幸いです。
Feature/add navigation by soushin · Pull Request #11 · soushin/sunshine-app · GitHub