DMMグループの一番深くておもしろいトコロ。
テクノロジー

【Android】Google IO 2018で新発表された navigation についての詳細レポート

DMMグループの一番深くておもしろいトコロ。

はじめに

こんにちは、 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 とは、Androidアプリにおける 画面遷移 を簡単に行うためのライブラリです。 ドキュメントには明記されていませんが、Google IO期間中いくつものセッションで navigation という単語とともに 「no more fragment transaction!!!」 というセンテンスを繰り返し見かけました。navigation は、難解な FragmentTransaction の扱いを簡単にするということを、モチベーションの大きなところとしているようです。

また、navigation は Android Architecture Components の一つであり、 JetPack の一部であるという側面もあります。Google IOのとあるセッションでは Architecture Components by default というスライドも見かけました。これからの 画面遷移 の標準の実装方法は navigation を使うものになっていきそうです。

原則

navigation は、一貫した予測可能なエクスペリエンスをユーザーに提供するために、以下の原則に準拠するとしています。

  1. 固定された開始画面を持つ
  2. navigation state はスタックで表現される
  3. Upボタンは決してアプリを終了しない
  4. 開始画面でなく、他のアプリのスタック上にもない時、UpボタンとBackボタンは同等の動きをする
  5. deep linkだろうと、手動で画面遷移しようと、同じ画面ならば同じ navigation stack を生成する

オリジナルはこちらから確認ください。

※ 現在のバージョン 1.0.0-alpha01 を実際に触ってみたところ、上記原則の 5 については実現できていないようでした。この問題については、下記のIssue Trackerがすでに起票されています。一応、すでに修正済みで次のバージョンで治るそうです 。興味のある方はご確認ください。

とりあえず、遷移するところまで実装

早速 navigation を使って画面遷移を実装していきましょう。手順は大きく分けると4ステップになります。

  1. build.gradle に依存を追加
  2. NavHostFragment を追加する
  3. Navigation Editor を使って、遷移グラフを作成する
  4. 各 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日現在ではまだ準備されてないようです。

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()

{モジュール}/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 での画面遷移の実装でやることは下記だけです。

  1. NavControllerを取得する
  2. 引数にその 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 がこれからの画面遷移の実装のデファクトスタンダードになってくることは明確です。しっかり押さえておきましょう。

採用情報

現在、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい!

https://dmm-corp.com/recruit/engineer/

  • Android

シェア

関連する記事

関連する求人