はじめに
こんにちは、 Androidエンジニアの @kgmyshin です。
先日、5月9日~11日に Google IO 2018 が開催されました。 弊社からは私を含め3名とグループのピックアップ社から3名のエンジニアが参加しました。 フェスのような雰囲気の中で、新しい技術に触れられる最高の環境でした。 また来年も行きたいものです。
さて、今回はそこで新しく発表されたAndroid開発を加速するJet Packの一つである navigation についての記事を書いてみます。
少し出遅れてしまったので、その分少し詳し目に書いてみます。
Google IOで navigation
について一番詳しい説明をしていたセッションは下記になります。
Android Jetpack: manage UI navigation with Navigation Controller (Google I/O '18) - YouTube
この記事では、上記のセッションにない情報も多く詰め込んでるので、ぜひご覧になってください。
navigation とは
まず navigation
とは、Androidアプリにおける 画面遷移 を簡単に行うためのライブラリです。 ドキュメントには明記されていませんが、Google IO期間中いくつものセッションで navigation
という単語とともに 「no more fragment transaction!!!」 というセンテンスを繰り返し見かけました。navigation
は、難解な FragmentTransaction
の扱いを簡単にするということを、モチベーションの大きなところとしているようです。
また、navigation
は Android Architecture Components
の一つであり、 JetPack
の一部であるという側面もあります。Google IOのとあるセッションでは Architecture Components by default
というスライドも見かけました。これからの 画面遷移 の標準の実装方法は navigation
を使うものになっていきそうです。
原則
navigation
は、一貫した予測可能なエクスペリエンスをユーザーに提供するために、以下の原則に準拠するとしています。
- 固定された開始画面を持つ
navigation state
はスタックで表現される- Upボタンは決してアプリを終了しない
- 開始画面でなく、他のアプリのスタック上にもない時、UpボタンとBackボタンは同等の動きをする
- deep linkだろうと、手動で画面遷移しようと、同じ画面ならば同じ
navigation stack
を生成する
オリジナルはこちらから確認ください。
※ 現在のバージョン 1.0.0-alpha01
を実際に触ってみたところ、上記原則の 5 については実現できていないようでした。この問題については、下記のIssue Trackerがすでに起票されています。一応、すでに修正済みで次のバージョンで治るそうです 。興味のある方はご確認ください。
とりあえず、遷移するところまで実装
早速 navigation
を使って画面遷移を実装していきましょう。手順は大きく分けると4ステップになります。
build.gradle
に依存を追加NavHostFragment
を追加するNavigation Editor
を使って、遷移グラフを作成する- 各
Fragment
にて任意のタイミングで、遷移グラフどおりの遷移を実装する
build.gradleに依存を追加
{モジュール}/build.gradle
の dependencies
に下記を追加します。
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"
-ktx
の付いているものはなくても大丈夫ですが、あると便利です。
※ Goolge IO 2018では、サポートライブラリと一緒にAndroid Architecture Component
のライブラリは androidx.
というパッケージになり、バージョンも 1.0.0
からになると発表がありました。すでにいくつかの Architecture Component
については android.x
のものがリリースされていますが、 navigation
については、本記事を執筆した5月21日現在ではまだ準備されてないようです。
NavHostFragment を追加する
navigation
ライブラリ では Activity
が NavHost
インタフェースを実装したインスタンスを使って画面遷移を行います。 NavHost
は空のViewで、この中身を入れ替えることによって画面遷移すると、ドキュメントにはあります。 この NavHost
はインタフェースで、これを実装しているクラスはNavHostFragment
です。 この NavHostFragment
を Activity
に設定することで、画面遷移ができるようになります。
ちなみに、 実装上では NavHost
はインタフェースで getNavController
という NavController
を返却するメソッドを一つ持っているだけです。したがって、あくまでコード上では NavHost
自身には〈空のViewで、この中身を入れ替えることによって画面遷移する〉という制約はなく、この機能はそれを独自で実装している NavHostFragment
の仕様という見方もできます。
対象の Activity
に下記のようにして NavHostFragment
を設定します。
<?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"
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true"
/>
</android.support.constraint.ConstraintLayout>
android:name="androidx.navigation.fragment.NavHostFragment"
で NavHostFragment
を設定します。
そして、app:navGraph
に作成した遷移グラフを設定します。こちらの遷移グラフの作り方は後述します。 これで設定した画面遷移と NavHost
がリンクされます。
app:defaultNavHost="true"
はこの NavHostFragment
に対して BackボタンとUpボタンを連携するかを決めます。基本的には true
を設定します。さらにActivity
で Upボタンと navigation
を連携するためには、 AppCompatActivity#onSupportNavigateUp
を下記のようにオーバーライドしておく必要があります。
override fun onSupportNavigateUp()
= findNavController(R.id.nav_host_fragment).navigateUp()
Navigation Editor を使って、遷移グラフを作成する
{モジュール}/res/navigation
に遷移グラフの xml
を作っていきます。Android Studioの Project
ペーンにて {モジュール}/res/navigation
にカーソルを合わせて、 cmd + n
を押すと出てくるショートカット一覧から Navigation resource file
を選び、適当なファイル名を入力しましょう。
作成したファイルを選択して、〈Design〉タブを選ぶことで Navigation Editor
を開くことができます。
Navigation Editor
の見方をざっくり紹介します。
- ① 遷移先一覧です。追加可能な遷移先一覧があるわけではなく ② に表示されている遷移先一覧がここにまとまっています
- ② 現状の遷移グラフです。
Fragment
を選んで新しい矢印を生やしたり、ネストしたりできます。ちなみに、ここに表示されている各種座標は.idea/navEditor.xml
に保存されていました。もしチームメイトと全く同じ見た目を実現したい方は、このファイルをgit管理下に置いておくと良いでしょう - ③ ここで遷移や遷移先などの属性を編集します
- ④ このボタンで新しい遷移先の追加を行います
遷移先の追加
遷移グラフの作成は、遷移先を追加していき、それぞれの遷移先を繋いでいくことで完成します。 そのため、まずは必要な遷移先を追加しましょう。
遷移先を追加するには、先の図の④のボタンを押すことでできます。
すでに存在している Activity
や Fragment
が表示されているのがわかります。これらを選ぶと遷移先に追加されます。
また〈Create blank destination〉というメニューが見えます。これは遷移先の追加だけでなく、ブランクな Fragment
を fragment_xxx.xml
や XXXFragment.kt
も含めてガガッと作ってくれる大変便利な機能です。メニューを押すと Fragment
の名前を求められるので、入力して Finish
を押せば登録&作成を行います。
※ 〈Create blank destination〉について、現状はまだバグがあるのか自分の環境では Finish
を押すと Android Studioがハングしてしまいます。早く解消されることを願って、Issue Tracker に起票はしておきました。興味ある方はご覧ください。
開始画面の設定
開始画面を設定していきます。
任意の Fragment
を選択して、 〈Set Start Destination〉を押すことで開始画面に設定できます。 開始画面に設定された Fragment
には 🏠のようなアイコンが表示されます。 一つの遷移グラフに対して、 開始画面は一つだけ設定できます。 開始画面に設定することで、ランチャーからアプリを起動した際などにはじめに表示されるようになります。
遷移の追加
遷移を追加していきましょう。
任意の Fragment
を選択すると Fragment
の右側に ●
が表示されます。
これをクリックしてそのまま遷移先のFragment
上までドラッグすることで遷移を追加できます。
このようにして全体の画面遷移を定義していきます。
また、定義した画面遷移(xml上は action
という名前)を選択することで、〈Attributes〉を編集することができます。
Transitions
で Enter
や Exit
のアニメーションを設定できます。
さらに、Pop Behavior
でポップ時は〈X画面まで戻る〉といったことを設定できます。 例えば、クイズ系のアプリでクイズを解いた後の結果画面でバックボタンを押した時に、クイズに戻らずにクイズ一覧画面に戻る、といったユースケースで使うことになると思います。
※ Activity
は遷移先に設定することはできますが、 Activity
から遷移を増やすことはできません。
ネストする
ナビゲーションをネストする、すなわち遷移グラフをグルーピングすることができます。
方法は簡単で、複数を選択して右クリックして Move to Nested Graph => New Graph とするだけです。
※ このNavigation Editor
上でネストする機能について、現状はまだバグがあるのか自分の環境では New Graph
を押すと Android Studioがハングしてしまいます。早く解消されることを願って、こちらも Issue Tracker に起票はしておきました。興味ある方はご覧ください。
私と同様、Android Studioがハングしてしまう方は、下記のように Text
画面で直接編集することもできます。やることは <navigation>
の中に <navigation>
を定義していくだけです。
<navigation
:
app:startDestination="@+id/launcher_home">
<fragment
android:id="@+id/launcher_home"
:>
<action
:
app:destination="@id/nested" />
</fragment>
<navigation
android:id="@+id/nested"
app:startDestination="@id/one">
<fragment
android:id="@+id/one"
:>
<action
:
app:destination="@id/two" />
</fragment>
<fragment
android:id="@+id/two"
:>
</fragment>
<action
:
app:destination="@id/end_dest" />
</navigation>
<fragment
android:id="@+id/end_dest"
:/>
</navigation>
この xml
上では launcher_home
-> nested
-> end_dest
という遷移を、また nested
の中身では one
-> two
という遷移をしています。
Deep Linkの設定
navigation
では、簡単に Deep Linkを設定することができます。
xml上では下記のようになります。
<fragment
:>
<deepLink app:uri="twitter.com/{name}" />
<deepLink app:uri="mobile.twitter.com/{name}" />
:
</fragment>
ワイルドカード*
を使用でき、上記のように変数も指定できます。ここで指定したname
などの 値は Intent
から Bundle
に詰め直されていて Fragment#getArguments("name")
のようにして取得できます。
※ Deep Link
について、原則のところでも触れましたが、1.0.0-alpha01
にはバグがあります。原則5に違反しているというもの以外にも、ネストされている場合にもまだ意図した挙動をしません。具体的には、 oneFragment
-> twoFragment
というネストされた遷移グラフのグループがある時に、 twoFragment
に Deep Linkの設定をしたとします。この時、 Deep Linkで設定したURLを踏むと、twoFragment
ではなく、ネストされた遷移グラフの開始先である oneFragment
に遷移してしまいます。この問題については、原則5の違反を指摘する IssueTracker でも触れられています。こちらも次のバージョンでは修正済みのようです。
各 Fragment にて任意のタイミングで、遷移グラフどおりの遷移を実装する
Navigation Editor
を使用して(もしくは手書きで)作成した遷移グラフのxmlは、いわば設計図みたいなものです。 これどおりに画面遷移を定義していきます。
下記のような遷移グラフがあるとします。
<navigation
:
app:startDestination="@+id/one">
<fragment
android:id="@+id/one"
:>
<action
android:id="@+id/to_two_action"
app:destination="@id/two" />
</fragment>
<fragment
android:id="@+id/two"
android:name="sample.TwoFragment"
android:label="Two"
tools:layout="@layout/fragment_start">
<action
android:id="@+id/to_three_action"
app:destination="@id/three" />
</fragment>
<fragment
android:id="@+id/three"
: />
</navigation>
one
-> two
-> three
という単純な画面遷移をします。そして、 one
-> two
という画面遷移には to_two_action
という id
が、 two
-> three
という画面遷移には to_three_action
という id
が振られています。
各 Fragment
での画面遷移の実装でやることは下記だけです。
- NavControllerを取得する
- 引数にその
Fragment
で設定している遷移の id を選び、NavController#navigate
を呼ぶ。
two
そのものであるTwoFragment
を実装してみましょう。ボタンをクリックしたら、 three
に遷移する( to_three_action
を実行する ) ようにします。
class TwoFragment : Fragment() {
:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
:
view?.findViewById<Button>(R.id.button)?.setOnClickListener {
view?.let { Navigation.findNavController(it).navigate(R.id.to_three_action) }
}
:
}
}
まず Navigation#findNavController
に Fragment
のルート View
を渡して、 NavController
を取得します。そして、取得した NavController#navigate
に遷移グラフに定義した to_three_action
を渡しています。two
-> three
の遷移の実装が完了です。
Fragment
のルート View
を渡したのは JetPack
のサンプルを参考にしたのですが、 実際は該当の Fragment
上にあるView
ならば問題はありません。
注意点が一つあります。 two
に定義されている遷移は、three
へ遷移する to_three_action
の一つだけです。ここで間違えて one
に定義した to_two_action
を NavController#navigate
に渡してしまうと、IllegalArgumentException
が発生してランタイムで落ちてしまいます。
これに関しては後述する safeargs
を使用することで、いくらかミスを減らすことができます。積極的に safeargs
を使っていきましょう。
また、クリックリスナーを生成するメソッドもあります。
button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.to_three_action, null))
ただし、この方法では safeargs
で生成されるものを引数に取れません。 safeargs
で生成したものでの画面遷移にすべて寄せたいので、現状では私がこのメソッドを使うことはなさそうです。
データの受け渡しをする画面遷移を実装する
画面遷移時にはデータの受け渡しを行うことがよくあります。 例えば、なんらかの id
を渡して、それをもとに取得した情報を画面に表示するというユースケースがよくありますね。
データが欲しい場合、下記のように argument
タグを用いて受け取ることができます。
<navigation>
:
<fragment
android:id="@+id/dest"
:>
<argument
android:name="count"
android:defaultValue="1"
app:type="integer" />
</fragment>
:
</navigation>
ここでは、count
という数値を受け取っています。この画面へ遷移する時、遷移元のコードは下記のようになります。
class HogeFragment {
:
var bundle = bundleOf("count" to count)
view.findNavController().navigate(R.id.toDestAction, bundle)
:
}
bundleOf
は ktx
で用意されている拡張関数です。 このcount
は遷移先の Fragment
内で下記で取得できます
class FugaFragment {
:
arguments.getString("count")
:
}
safeargsを使う
上記の方法でデータの受け渡しを行う画面遷移は実現できています。 ただし、Bundle
でのやり取りでは、遷移時にどういうキー名でどの型の値を詰めればいいのかわかりづらいです。 それがもし、型になっていれば一目瞭然です。それを実現するのが safeargs
プラグインです。
safeargsの準備
トップレベルの build.gradle
に下記を追加します。
buildscript {
:
dependencies {
:
classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'
:
}
}
:
次に {モジュール}/build.gradle
で androidx.navigation.safeargs
をapplyします(ここだけすでにandroidx.
になっているのを注意してください)。
apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'
:
これで完了です。
safeargs適用後の遷移
先ほどの count
を渡す画面遷移がどう変わるのか見てみましょう。
まずは、遷移元のコードから。
class HogeFragment {
:
v.findNavController().navigate(HogeFragmentDirections.toDestAction().apply {
setCount(count)
})
:
}
遷移先では下記のようにして値を取得できます。
class FugaFragment {
:
val args = FugaFragmentArgs.fromBundle(arguments)
args.count
:
}
それぞれ、メンバーになっているのでキー名をタイポすることはなく、どの値がどの型であるのかも明確ですし、AndroidStudioの自動補完の対象にもなって大変便利です。
そして、見てのとおりなのですが、生成されるクラス名が本当にわかりやすいんです。 HogeFragment
から遷移するときは HogeFragmentDirections
を使用して、 FugaFragment
でデータを取り出すには FugaFragmentArgs
を使用する。 Fragment
に Directions
と Args
というサフィックスが付いてるだけです。 HogeFragment
で FugaDirections
が出てきていたら、すぐに間違えているとレビューで気づけます。
また、safeargs
はデータの受け渡しを行わない遷移でも有効です。
safeargs
を使わない遷移ではアクションの id
を指定しなければなりませんが、safeargs
であればメソッドを選ぶだけです。
class HogeFragment {
:
v.findNavController().navigate(HogeFragmentDirections.toDestAction())
:
}
これによって間違った id
を渡すことが一切なくなります。
積極的に使っていきましょう。
各種UIとの連携について
Toolbarとの連携
Toolbar
とナビゲーションの連携方法を見ていきます。
<android.support.constraint.ConstraintLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
: />
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
: />
</android.support.constraint.ConstraintLayout>
Toolbar
との連携は下記で実現できます。setupActionBarWithNavController
を呼ぶだけです。
class StartActivity : AppCompatActivity() {
:
override fun onCreate(savedInstanceState: Bundle?) {
:
setSupportActionBar(findViewById(R.id.toolbar))
val navController = findNavController(this, R.id.my_nav_host_fragment)
setupActionBarWithNavController(navController)
}
:
}
また、 DrawerLayout
を使っている場合は、そのインスタンスを下記のように第二引数に渡してあげればOKです。
setupActionBarWithNavController(navController, drawerLayout)
BottomNavigationViewとの連携
BottomNavigationView
とナビゲーションの連携方法を見ていきます。
<android.support.constraint.ConstraintLayout>
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/nav_graph"
: />
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_nav"
app:menu="@menu/bottom_menu"
: />
</android.support.constraint.ConstraintLayout>
BottomNavigationView
との連携は下記で実現できます。BottomNavigationView#setupWithNavController
( navigation-ui-ktx
に用意されている拡張関数 ) を呼ぶだけです。
class StartActivity : AppCompatActivity() {
:
override fun onCreate(savedInstanceState: Bundle?) {
:
val navController = findNavController(this, R.id.my_nav_host_fragment)
findViewById<BottomNavigationView>(R.id.bottom_nav).setupWithNavController(navController)
}
:
}
どのメニューを押した時に、どの Fragment
が表示されるのか。これは少し特殊な方法なのですが、 BottomNavigationView
に設定する menu.xml
内のアイテムの id
と nav_graph.xml
内の Fragment
の id
を同じにします。
nav_graph.xml
が下記のようになっている時、
<navigation ...>
<fragment
android:id="@+id/hoge"
>
:
</fragment>
<fragment
android:id="@+id/fuga"
>
:
</fragment>
<fragment
android:id="@+id/piyo"
>
:
</fragment>
:
</navigation>
menu.xml
は下記のようになっている必要があります。
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/hoge"
: />
<item
android:id="@+id/fuga"
: />
<item
android:id="@+id/piyo"
: />
</menu>
どのように画面遷移を実現しているのか
どのように画面遷移を実現しているのかを簡単に説明していきます。
NavHostFragment
は一枚の FrameLayout
を持っており、その FrameLayout
上で Fragment
を切り替えることで画面遷移しています。いわゆる Nested Fragment
です。
また NavHostFragment
は初期化時に NavController
を生成して、 rootView
の tag
にインスタンスを保存しています。 遷移時に使用する NavController
は Navigation#findNavController
によってインスタンスを得ることができていました。 これは単純にView
を上に辿って行き NavHostFragment
の rootView
までたどり着いたら getTag
によって取り出していたわけです。
適当に生成した未アタッチな View
からは、 NavHostFragment
の rootView
までたどり着くことはできないので注意しましょう。
NavController
は NavGraph
を持っており、 NavGraph
は複数の NavDestination
を持っています。 そして一つの NavDestination
は、一つの Navigator
を持っています。 Navigator
は navigate
という、実際の画面遷移の実装を持っています。FragmentNavigator
では FragmentTransaction
などを使って遷移してますし、 ActivityNavigator
では Intent
を作って startActivity
を呼んでいます。
NavControlle#navigate(id)
が呼ばれると、id
をもとに正しい NavDestination
探し出し、その NavDestination
の持っている Navigator
で画面遷移するという実装になっています。
おわりに
まだ alpha
版なだけに、多少のバグはあります。ただし、navigation
がこれからの画面遷移の実装のデファクトスタンダードになってくることは明確です。しっかり押さえておきましょう。
採用情報
現在、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい!