こんにちは。Android開発担当の斎藤です。Android JetPack にある Navigation コンポーネント(Navigation Architecture Component)について調査したので結果を報告します。
Navigation とは
JetPack とは
- Android アプリ作成の高速化を助けるライブラリ群のこと。
Navigation の原則
詳細が知りたい方は 公式 へ。
開発環境
Android Studio 3.2 Canary 14 以上のバージョンが必要です。 Android Studio 3.2 stable 版が最近(2018/09/24) にリリースされましたが、 Navigation Editor
が無効化されています。
今回は Android Studio 3.3 Canary 14 を利用しています。
Please note, to maintain high product quality, a couple features (e.g. Navigation Editor) you saw in earlier release channels are not enabled by default in the stable release channel.
事前準備
gradle に下記を追加する
implementation "android.arch.navigation:navigation-fragment:1.0.0-alpha06" implementation "android.arch.navigation:navigation-ui:1.0.0-alpha06"
現時点(2018/09/27) では最新が alpha06
となっています。
実装
大まかな実装を簡単に説明します。
- 1つの MainActivity で MainFragment -> NextFragment の切り替えをしています。
- 今までは MainActivity の
FrameLayout
かなんかでFragment
のコンテナを作り、そのコンテナ内で MainFragment と NextFragment の切り替えを行なっていたと思います。
- 今までは MainActivity の
<activity> <FrameLayout android:id="@+id/container" /> </activity>
- Navigation ではこのコンテナに変わるものが用意されておりそれが、
androidx.navigation.fragment.NavHostFragment
というものです。NavHostFragment
はnavigation 用の xml を解析してアプリを遷移させてくれます。
<activity> <fragment android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/navmain" /> </activity>
<?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:id="@+id/navmain" app:startDestination="@id/mainFragment"> <fragment android:id="@+id/mainFragment" android:name="jp.shts.android.navigationsample.MainFragment" android:label="fragment_main" tools:layout="@layout/fragment_main" > <action android:id="@+id/action_mainFragment_to_nextFragment" app:destination="@id/nextFragment" /> </fragment> <fragment android:id="@+id/nextFragment" android:name="jp.shts.android.navigationsample.NextFragment" android:label="fragment_next" tools:layout="@layout/fragment_next" /> </navigation>
^Navigation Editor はこんな感じです。
見た目は iOS のストーリーボードに似ているのですが、 Navigation Editor は画面遷移に関する定義のみで、レイアウトに関する定義はできません。
新規アプリの場合は画面遷移のための navigation xml を作成後、各画面の作成、Activity に NavHostFragment を追加 の順に作業していくとやりやすいと感じました。
A 画面から B 画面へ遷移する
実装イメージ
遷移図を作成するため navigation xml を作成します。
- res/navigation 下に適当な名前のファイルを
Navigation resource file
で作成します。 Navigation resource file
の項目がない場合は AndroidStudio のバージョンが古い可能性があるので確認してください。
- res/navigation 下に適当な名前のファイルを
Navigation Editor で作成した Fragment にはテンプレートが書いてありますが、今回のサンプルには不要処理なので一部というかほぼ削除します。
Navigation Editor で作成した2つの Fragment を繋げます。 ^画面の丸を右へ引っ張ると画面遷移を定義できます。
Activity に
NavHostFragment
を定義して^で作成した navigation xml を 設定する。
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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" tools:context=".MainActivity"> <fragment android:id="@+id/my_nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/navmain" /> <-- 先ほど定義した navigation の xml </android.support.constraint.ConstraintLayout>
- MainFragment のボタンをタップした時に画面遷移するリスナーを設定する。
nextButton.setOnClickListener { it.findNavController().navigate(R.id.action_mainFragment_to_nextFragment) }
action_mainFragment_to_nextFragment
は Navigation Editor で画面遷移を定義すると自動で作成されるIDです。自分で変更も可能。
^リスナに設定するのは上から二番目のIDです。
これで画面の切り替えができるようになります。
アニメーションしながらA 画面から B 画面へ遷移する
実装イメージ
- 前回のA 画面から B 画面へ遷移するのコードをベースとします。
- Navigation Editor から Main <-> Next 画面遷移時にアニメーションをつけることができます。
- Navigation Editor で矢印をタップ後の Attributes パネルに Animations の項目が表示されるのでここでアニメーションリソースを選択するだけで画面遷移アニメーションをつけることができます。
- Enter
- AからBに遷移するときの B のアニメーション
- Exit
- AからBに遷移するときの A のアニメーション
- Pop Enter
- BからAに遷移するとき(バックするとき)の A のアニメーション
- Pop Exit
- AからBに遷移するとき(バックするとき)の B のアニメーション
- Enter
A 画面から B 画面へ遷移する(データを渡す)
ちょっとわかりにくいですが、Hello という文字列をBに渡して表示しています。
- Navigation Editor でデータを受け取りたいクラスを選択して Attributes パネルの Arguments のプラスボタンをタップすると受け取りたいデータを定義できます。
- XMLを直接編集しても問題はありません。
あとは遷移元のFragmentでbundleを作成して渡すだけです。
nextButton.setOnClickListener { val bundle = Bundle().also { it.putString("title", "Hello") } it.findNavController().navigate(R.id.action_mainFragment_to_nextFragment, bundle) }
受け取りも今まで通りです。
text.text = arguments?.getString("title") ?: "Failed"
もっといいやり方
safeargs というライブラリを使えば、これらの処理を型安全に実行できます。
トップレベルのGradleにパスを追加します。
classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'
app配下のGradleでapplyします。
apply plugin: 'androidx.navigation.safeargs'
こうすると MainFragmentDirections
というクラスが作成されます。またわざわざ指定しなければならなかった id も Action_mainFragment_to_nextFragment
のように必要なパラメータを持ってクラスとして生えるので安全です。
nextButton.setOnClickListener { val action = MainFragmentDirections.Action_mainFragment_to_nextFragment("Hello") it.findNavController().navigate(action) }
受け取り側は自動作成された NextFragmentArgs
クラスから fromBundle します。NextFragmentArgs
には Arguments で設定した変数名でメンバ変数が生えているのでこれを参照するだけです。
text.text = NextFragmentArgs.fromBundle(arguments).title
A 画面から B 画面へ遷移する(リストからデータを渡す)
実装イメージ
よくある形かと思いますが、基本的にはデータの受け渡しは^に同じです。
// OnClickAnimalListener はリストクリック時のコールバックです class AnimalListFragment : Fragment(), OnClickAnimalListener { override fun onClick(name: String) { findNavController().navigate( AnimalListFragmentDirections .action_animalListFragment_to_animalDetailFragment(name) ) } }
class AnimalDetailFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) animalName.text = AnimalDetailFragmentArgs.fromBundle(arguments).name } }
注意事項
こういった一覧から詳細へという画面遷移パターンの時、そして詳細へ遷移する時に渡すデータは Parcelable なデータを渡すことが多いと思いますが。
Navigation Editor の Arguments に Parcel なデータを指定できるのですが、 Parcelable を指定しても生成されるソースコードがなぜか String
になってしまいます...
public Bundle getArguments() { Bundle __outBundle = new Bundle(); __outBundle.putString("animal", this.animal); return __outBundle; }
解決できずに今に至ります。なのでこのサンプルでは結局Animal ではなく String で名前だけ渡すようにしました。多分バグだろうと思っています。
ボトムナビゲーションでタブの切り替え
実装イメージ
BottomNavitaion を Activity に設置後に NavigationUI.setupWithNavController
を呼ぶだけで簡単に作れてしまいます。
<fragment android:id="@+id/my_nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" ... app:navGraph="@navigation/navtab" /> <android.support.design.widget.BottomNavigationView android:id="@+id/bottomNav" ... app:menu="@menu/menu" />
val navController = Navigation.findNavController(this, R.id.my_nav_host_fragment) NavigationUI.setupWithNavController(bottomNav, navController)
注意する点として、 Navigation Editor で action を指定する必要はありません。
感想
- Navigation Editor のこと勘違いしてました。思ったより全然使いやすかったです。生成されるコードも全然読めます。
- まだalpha版ということでしたが思ったより安定している印象です。
参考にした資料など
https://developer.android.com/topic/libraries/architecture/navigation/