Android Jetpack コンポーネントのNavigationのプロダクトへの導入手順と実装Tipsの紹介

f:id:vasilyjp:20191011160543j:plain

はじめに

こんにちは!

ZOZOテクノロジーズ開発部の鈴木(@zukkey59)です!

普段は、「ファッションコーディネートアプリ WEAR」のAndroidアプリを担当しています。

私のWEARに配属されて初めての大きな仕事は、新規登録・ログイン機能へAndroid Jetpack コンポーネントのNavigationを導入することでした。

導入によってある程度知見がためることができました。

今回はNavigationをこれから導入したい!と検討している方、もしくは興味がある方に向けて、自身の知識の整理もかねて紹介したいと思います。

既に、Navigationの記事はQiitaやMediumに数多くあるので被っている内容もありますが、今回は導入からテストまでの一連の流れとその中で気づいた注意すべきところなどを一通り紹介させていただきます。
要件や仕様として、WEARで使用しなかったNavigationの機能について触れることはありません。

記事を読み進める前の判断として、想定している読者とこの記事に書いてあること、書いていないことをまとめました。

事前に目を通していただけると幸いです。

  • 想定している読者

    • Android開発を主に業務でされている方
    • プロダクトにNavigationを利用したいと思っている方
    • Navigationをまだ利用したことがなく、初めて利用する方
    • NavigationのUIテストを書いてみたい方
    • Navigationのハマりどころについて事前に知っておきたい方
    • Espressoについての基礎知識を持っている方(CodeLaboを終了した程度を想定)
    • AndroidX移行済みである方
  • 今回書いてあること

    • Navigationの説明
    • Navigationの導入手順
    • (DeepLinkを除く)Navigationの実装手順、利用の仕方
    • (DeepLinkを除く)Navigationのテストの書き方
  • 今回書いてないこと

    • NavigationのDeepLink周りについて
    • マルチモジュール関連の挙動
    • プロダクト全体でのSingleActivityでの運用など
    • Mockito、Espressoの導入、使い方などの説明

実際に導入した箇所はこちらです。

f:id:vasilyjp:20191010175339p:plain

また、開発環境は次のとおりです。

  • Mac OS Mojave 10.14.5
  • Android Studio 3.5.1

それでは早速、Navigationについてみていきましょう!!

Navigationとは?

Android Jetpack コンポーネントのライブラリの一つです。

Android Jetpackは、開発者向けにAndroidアプリを簡単に作成するためのライブラリやツール、ガイダンスをまとめたものです。

その中の一つである、Navigationはアプリ内の複雑であった画面遷移を、簡単にしてくれるライブラリです。

Navigationの実装のTipsに入る前に、まずはNavigationの原則について確認していきましょう。

Navigationの原則

公式は、こちらにあります。

簡単に要約すると次の通りです。

  1. Fixed start destination
    出発点を決めます。Navigationを利用する場合には開始位置を固定する必要があります。

  2. Navigation state is represented as a stack of destinations
    Navigationの状態はスタックで表現されます。 バックスタックはすべてNavigationが管理してくれますが、自分でバックスタックを管理することもできます。

  3. Up and Back are identical within your app's task
    アプリ内において画面上部のapp barに表示されるUpボタンと画面下部のナビゲーションバーに表示される戻るボタンの挙動は同じです。

  4. The Up button never exits your app
    Upボタンはアプリを終了させる事はしません。 Deeplinkでアプリを起動した場合、Upボタンはアプリ内で新たに手動による操作を模倣して作成されたバックスタックをたどり、Deeplinkを起動したアプリに戻ることはありません。 戻るボタンはDeeplinkを起動したアプリに戻ります。

  5. Deep linking simulates manual navigation
    Navigationは、DeepLinkに対応しています。 DeepLinkは手動での画面操作を模倣したバックスタックを生成します。 DeepLink経由で起動した場合には、既存のバックスタックは削除されてDeepLinkにより生成されたバックスタックに置きかわります。

今回、WEARで導入した新規登録・ログインのリニューアルに関しては、要件としてDeepLinkは用いなかったので、DeepLink以外で関わることのみを紹介いたします。

Navigationの原則について確認いたしましたので、次は導入の手順について見ていきましょう。

導入手順

早速、Navigationを利用する準備を始めましょう! Navigationの最新バージョンは、 2.1.0 です。(2019年10月現在)

はじめに、Navigation Supportを導入するためにapp配下のbuild.gradleに次の2行を記載しましょう。

build.gradle

dependencies {
  // Kotlin
  implementation "androidx.navigation:navigation-fragment-ktx:2.1.0" // 追加
  implementation "androidx.navigation:navigation-ui-ktx:2.1.0" // 追加
}

WEARではKotlinを利用しているので上記のように記載しております。

Javaを利用する場合は次のように記載してください。

build.gradle

dependencies {
  // Java
  implementation "androidx.navigation:navigation-fragment:2.1.0" // 追加
  implementation "androidx.navigation:navigation-ui:2.1.0" // 追加
}

まだ、Jetpackを利用されていない場合は、プロジェクト配下のbuild.gradleに次の1文を追加してください。

build.gradle

allprojects {
    repositories {
        google() // 追加
    }
}

【参考リンク】

Get started with the Navigation component | Android Developers

Adding Components to your Project | Android Developers

導入の準備は、たったこれだけでOKです! 次は実装のTipsについて見ていきましょう!

実装のTips

NavigationGraphを作成する

NavigationGraphは、ナビゲーションを管理する役割を持っており、遷移の目的地と遷移するアクションなどを含むリソースファイルです。NavigationGraph自体はxmlファイルになります。どこに遷移するのか、どのような種類の遷移間のデータを持たせるのかなどをここに定義します。

まずは、NavigationGraphの追加の仕方について説明していきます。

最初に、File > New > Android Resource Fileを選択します。 画像でみると、次のような状態です。

f:id:vasilyjp:20191010121113p:plain

選択後に、New Resource Fileというポップアップが表示されますので、File nameには自分が作成したいNavigationGraphの名前を入れて、Resource typeはNavigationを選択します。これでOKを押すと作成が完了です。 画像でみると、次のような状態です。

f:id:vasilyjp:20191010121209p:plain

作成すると、res > navigationにて自分の定義したファイルが生成されていることがわかります。 今回の機能改修に関しては、navigation_graph_entranceとして作成しました。 画像でみると、次のような状態です。

f:id:vasilyjp:20191010121226p:plain

作成が終わるとXMLの内容は次のようになります。

navigation_graph_entrance.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"
    android:id="@+id/navigation_graph_entrance">

</navigation>

これで、NavigationGraphの作成は終了です。

次は、NavHostFragmentを用意していきましょう。

NavHostFragmentを用意する

NavHostFragmentの内部のjavadocを見ると、次のようにあります。

NavHostFragment provides an area within your layout for self-contained navigation to occur.

ここから考えることとして、NavHostFragmentとは「Navigationが機能するためのハコのようなFragmentである」という認識で良さそうです。

このNavHostFragmentをもった、Navigationの管理をする用に一つのActivityを作成します。 今回の新規登録・ログイン周りでは、入口部分であるという意味を込めて、EntranceActivityと定義しました。

用意したActivityのレイアウトファイルである、activity_entrance.xmlに次のように記載します。

activity_entrance.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <fragment
            android:id="@+id/nav_host"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/navigation_graph_entrance"
            app:defaultNavHost="true"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

WEARでは、DataBindingを併用しているためlayoutタグで囲っています。

まずは、activity_entrance.xmlのfragmentタグに着目してください。 ソースコード中の...は省略記号を示します。

activity_entrance.xml

<fragment
    android:id="@+id/nav_host"
    ...
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/navigation_graph_entrance"
    app:defaultNavHost="true"
    ...
    />

fragmentのandroid:nameに

androidx.navigation.fragment.NavHostFragment

を設定します。NavHostを実装したクラスを指定する必要があるようです。特になければ、上記のように設定すれば問題ないでしょう。

次に、fragmentのapp:navGraphに先ほど作成したNavigationGraphを設定します。

今回の場合は、navigation_graph_entranceを作成したので、@navigation/navigation_graph_entrance と設定します。 navGraphの属性は、NavHostFragmentに自分が作成したNavigationGraphを関連付ける役割を持ちます。

defaultNavHostをtrueにされたNavHostFragmentが戻るボタンをインターセプトします。 同時に複数あるNavHostのdefaultNavHostをtrueにしてはいけません。

複数のNavHostがなければ、trueにしておくのが良いかと思います。

次は、NavigationEditorについてです。

NavigationEditorを利用する

NavigationEditorとは、視覚的にNavigationGraphを編集することができたり、直接xmlでNavigationGraphを編集することができるAndroid Studioの機能の一つです。

WEARで利用している箇所でのEditor表示は次のとおりです。

f:id:vasilyjp:20191010121254p:plain

左の赤い枠の部分がDestinations panelといわれるところで、ここではNavigationのHostと後述する青枠のGraph Editorの中にある全ての遷移先の目的地のリストを表現します。

真ん中の青い枠の部分が先ほど出たGraph Editorといわれるところで、自身が作成したNavigationGraphを視覚的にわかりやすく表示してくれます。ここで編集したことと、xmlの中で編集したことは同期されます。

右の緑の枠の部分がAttributesといわれるところで、後ほど説明いたしますが選択しているGraphのArgumentsやactionなどの属性を表示したり、actionのAnimationやAttributesのIDやPop behaviorなどの属性を表示します。 ここで編集したことと、xmlの中で編集したことは同期されます。

左下のオレンジの枠の部分では、Designタブでデザインのプレビュー画面に、TextタブでXML画面に表示を切り替えることができます。

次は、NavigationEditorで遷移先となるDestinationの追加の仕方について見ていきましょう。

Destinationの追加の仕方

Destinationの追加の仕方は、とてもシンプルです。

f:id:vasilyjp:20191010121320p:plain

上記画像の、赤枠で囲まれている New Destinationアイコンをクリックすると、検索ボックス、作成するボタン、並びにクラスのリストが表示されます。

検索ボックスには、まだ追加されていないクラスを入力すると一覧に出てくる仕組みになっているので、先に新しくActivityやFragmentを作成した場合は、ここから検索してNavigationリソースにも追加するのが良いと思います。

また、青枠で囲まれている、Create new destinationと書かれている箇所をクリックすると、Fragmentを作成する画面が表示され特にチェックを外さずにFinishすると、新しいFragmentとレイアウトリソースファイルを自動的に作成し、Navigationリソースにも追加してくれます。

緑枠には、既存でNavigationリソースに追加していないActivityやFragmentのファイルがリスト形式で表示されます。

xmlにFragmentタグで直接定義することで追加することもできます。

以上が、Destinationの追加の仕方についてでした。 次は、Actionの追加の仕方について紹介します。

Actionの追加の仕方

Actionの追加の仕方もとても簡単です。

f:id:vasilyjp:20191010121345p:plain

赤枠で囲まれている右矢印のアイコンをクリックすると、Add Actionというポップアップが出てきます。 また、AttributesのActionsの+をクリックすると、リストが表示されAdd Actionという項目が出てくるので、それをクリックすることでも同様のことができます。

ここで遷移元と遷移先やAnimationの仕方など遷移に関わる項目を設定します。Navigationではこれら全てをまとめてActionとしています。 このとき何も選択していなければデフォルトでFromにnav_hostが入りますが、前章で出てきたDestinations panelのGRAPHのリストの項目に存在する、いずれかのGraphを選択していると選択されたGraphがデフォルトでFromに入ります。 メールフォーム入力画面を選択した状態でしたので、画像ではFromにmail_formと入力されていることがわかります。

IDは、指定がなければ自動的にActionのIDを生成してくれるので、特に気にする必要はありません。

Fromは遷移する元の画面のIDを指定し、Destinationには遷移させたい画面のIDを設定します。

Transitionでは、次画面への遷移時と、元画面へ戻る遷移時のアニメーションを選択することができます。 アニメーションをさせたくない場合は未入力でも問題はありません。自分で作成したアニメーションも設定することが可能です。

Pop Behaviorでは戻るときに「どこまで戻るのか」の定義ができます。 Inclusiveにチェックを入れると、popUpToの前まで戻ることができます。 実際にプロダクトに導入してみたところ、戻るの挙動を変えたい場合が想像以上に出てきたので非常に有用でした。

Launch Optionsではシングルトップで起動するかどうかを選べます。必要な場合はチェックを入れましょう。

後から変更したくなった場合にも、xmlを編集すれば変更可能なので、間違えても問題はありません。

Actionを追加する方法について学んだら、次はAttributesの追加について見ていきましょう。

Attributesの追加の仕方

AttributesもNavigationEditorから簡単に追加することができます。 次の画像を見てください。

f:id:vasilyjp:20191010121408p:plain

Attributesのところで、Argumentsという項目があり右側に+アイコンが表示されています。こちらをクリックすると、Add Argument Linkというポップアップが出てきます。 ここで追加したい情報を設定することができます。 Nameにはargumentの名前を入れます。 Typeは、Integer / Float / Long / Boolean / Stringだけでなく、ResourceやCustomSerializableなども選ぶことができます。 ArrayやNullableもチェックを入れられる場合は設定可能です。また、defaultValueも設定可能です。

もちろん、NavigationEditorからだけでなく直接xmlを編集して追加することも可能です。

SafeArgsを利用する

SafeArgsとは、NavigationのGradleプラグインでActivityやFragmentへの引数付きの遷移をする際に、型安全なObjectとビルダークラスを生成してくれます。 こちらを利用することで、自身でキーを設定したり、型があっているかの確認をしなくても自動生成されたクラスを利用するだけになるため非常に便利です。

導入の手順はいたってシンプルです。app配下のbuild.gradleにて次の一文を追加します。

build.gradle

apply plugin: 'androidx.navigation.safeargs'

次に、project配下のbuild.gradleに次のコードを追加します。

build.gradle

buildscript {
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"
    }
}

2019年10月現在、この記事を書いている時の最新のStableバージョンは2.1.0です。 バージョンの確認を行いたい方は、こちらのリンクから確認することができます。

公式ドキュメントにも導入方法が記載されております。

【参考リンク】

Use Safe Args to pass data with type safety

もし、kotlinOptionsをapp配下のbuild.gradleに記載していない場合は、JVM targetを設定する必要があります。 その場合はapp配下のbuild.gradleに次のように記載しましょう。

build.gradle

android {
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8
    }
}

SafeArgsの利用手順としては次の通りです。

  1. 遷移先にて渡したい情報を持つArgumentを定義
  2. 遷移元にてActionを定義
  3. 遷移元のFragmentで自動生成されたクラスに渡したい情報を付加して遷移
  4. 遷移先にて渡された情報を受け取る

実際のソースコードを見ていきながら、手順を確認していきましょう。

まず、遷移先にて渡したい情報を持つArgumentを定義するということを行います。

今回は、メールアドレス入力画面(mail_form)からアカウント作成画面(create_account)への遷移について例にとります。 次の画像の画面間の遷移になります。

f:id:vasilyjp:20191010121437p:plain

アカウント作成画面にて、受け取りたい情報を定義します。 navigationのリソースファイルを開き、次のように記載をします。今回は、度々例に挙げている、navigation_graph_entrance.xmlで説明します。

navigation_graph_entrance.xml

<fragment
    android:id="@+id/create_account"
    android:label="fragment_create_account"
    android:name="(package名とパス).CreateAccountFragment"
    tools:layout="@layout/fragment_create_account">
    <argument
        android:name="snsType"
        app:argType="(package名とパス).SnsType" />
</fragment>

NonNullなSNS連携情報を持つクラスを渡したいので、上記のように遷移先にて定義するようにします。

次に、遷移元にてActionを定義します。 メールフォーム画面からアカウント作成画面へ遷移するためのActionタグを、遷移元であるメールフォーム画面のfragmentタグに情報を追加します。

navigationのリソースファイルを開き、次のように記載をします。

navigation_graph_entrance.xml

<fragment
        android:id="@+id/mail_form"
        android:label="fragment_mail_form"
        android:name="(package名とパス).MailFormFragment"
        tools:layout="@layout/fragment_mail_form">
        <action
            android:id="@+id/action_mail_form_to_create_account"
            app:destination="@+id/create_account"
            app:enterAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_in_right" />
</fragment>

fragmentタグの中にactionタグを追加してアニメーションとdestinationを遷移先のcreate_accountにします。

Actionの定義が終わったら、最後は遷移元のFragmentで自動生成されたクラスに渡したい情報を付加して遷移させます。

遷移元のMailFormFragmentにて、引数なしの場合は次のコードのように定義していた箇所を変更します。

MailFormFragment.kt

findNavController().navigate(R.id.create_account)

SafeArgsプラグインを利用すれば、Fragment名 + Directionsのクラスが自動生成されて、自身で定義したアクション名をもつ関数が自動で定義されるようになっています。

遷移元のMailFormFragmentでは次のようにして定義します。

MailFormFragment.kt

findNavController().navigate(
  MailFormFragmentDirections.actionMailFormToCreateAccount(
    snsType
  )
)

これだけでOKです。

最後に、遷移先のCreateAccountFragmentにて情報を受け取ります。

SafeArgsプラグインを利用すると遷移先のクラスで、Fragment名+Argsのクラスが自動生成されます。 こちらの自動生成されたクラスを用いて、次のようにして情報を受け取ります。

CreateAccountFragment.kt

// navArgsでarguments全体を取得する
val args: CreateAccountFragmentArgs by navArgs()

// 渡したデータを受け取ることもできる
val snsType = args.snsType

受け取りはこれだけでOKです。

SafeArgsについての説明は以上です。

次に、今回の開発においてNavigationEditorを利用しましたが、非常に便利であったので、覚えておくといいことをご紹介させていただきます!

NavigationEditorを利用する時に覚えておくと良いこと

AutoArrangeについて

色々とDestinationを追加していったら、Graph Editorの中がごちゃごちゃしてくることがあります。 そんな時は、AutoArrangeを利用しましょう。 Auto Arrangeは次の画像の赤い枠で囲った部分をクリックすることで利用できます。

f:id:vasilyjp:20191010121504p:plain

このAutoArrangeを利用した時の挙動が次のとおりです。

f:id:vasilyjp:20191010121521g:plain

一瞬で、整列してくれるので、Graph Editor がゴチャゴチャして見辛くなったときに、利用するようにしましょう。

Go to XMLというショートカットについて

開発を行っていると、Graph Editorを利用していたら該当箇所のxmlに素早くジャンプしたい時が出てきます。

その際に便利なショートカットが、⌘(コマンドキー) + B(Macの場合)で選択したアイテムのxmlリソースへ飛ぶことができます。 Graphだけでなく、Actionでも遷移することが可能です。

レイアウトの設定について

xmlにて、toolsで該当のレイアウトリソースファイルを指定するとGraphEditorに反映され、視覚的にもGraphがどの画面なのかすぐにわかります。

例えば、WEARでのメールアドレス入力画面について見ていきましょう。

xmlでの定義は次のようにします。

navigation_graph_entrance.xml

<fragment
        android:id="@+id/mail_form"
        android:label="fragment_mail_form"
        android:name="(package名とパス).MailFormFragment"
        tools:layout="@layout/fragment_mail_form"  <!-- 追加 -->
        >
        ...

</fragment>        

上記のように対象のレイアウトリソースファイルを指定することで、次のような表示をGraphEditor内で確認できます。

f:id:vasilyjp:20191010121615p:plain

以上、NavigationEditorを利用する際に覚えておくと開発が捗ると思うことの紹介でした。

次は、Espressoを用いたNavigationの遷移のテストについて紹介させていただきます。

Espressoを用いてNavigationの遷移のテストを行う

Mockito、Espressoは導入している前提で、進めます。

WEARで利用しているEspressoのバージョンは3.1.1です。

Navigationの遷移のテストを行う前に準備をしましょう。 app配下のbuild.gradleに次の1行を追加します。

build.gradle

debugImplementation 'androidx.fragment:fragment-testing:1.1.0'

導入はたったこれだけで大丈夫です!

Navigationのテストの実装の手順は次のとおりです。

  1. NavControllerをMockする
  2. テストしたい画面のFragmentScenarioを作成する
  3. FragmentにNavControllerのプロパティをセットする
  4. Actionで遷移した結果が正しいかのテストを行う

まずはじめに、1~3をやっていきましょう! 今回は度々例として出ていたメールフォーム入力画面(MailFormFragment)を例に紹介いたします。

androidTestディレクトリの中に階層を同じくしたMailFormFragmentTestを作成します。 1~3はテストする前に下準備をします。コードとしては次のようになります。 それぞれコード上にコメントいたしました。

MailFormFragmentTest.kt

@RunWith(AndroidJUnit4::class)
class MailFormFragmentTest {

  private lateinit var mockNavController: NavController
  private lateinit var scenario: FragmentScenario<TestMailFormFragment>

  @Before
  fun setUp() {
      // NavControllerをMockする
      mockNavController = Mockito.mock(NavController::class.java)

      // テストしたい画面のFragmentScenarioを作成する
      scenario =
          launchFragmentInContainer<TestMailFormFragment>(themeResId = R.style.AppTheme)

      // FragmentにNavControllerのプロパティをセットする
      scenario.onFragment { fragment ->
          Navigation.setViewNavController(fragment.requireView(), mockNavController)
      }
  }

}

setUp関数の中で、一通り1~3を行います。 NavControllerをまずはMockします。Mockitoは今回の趣旨に関わらないので説明を省略します。 NavControllerをMockしたら、テストしたい画面のFragmentScenarioを作成します。
Test用にMailFormFragmentを継承したクラスを作り、DaggerAndroidSupportを利用している場合はTest用にViewModelをMockします。 作成したTest用MailFormFragmentのFragmentScenarioを作成します。この時にthemeResIdを指定する必要があるので、AppThemeを指定するようにしてください。

ここまで準備をしたら、各Actionのテストを行います。 今まで、メールフォーム入力画面からアカウント作成画面を例にとって説明していたので、そちらを例にして説明していきます。 NavigationのActionが通るかどうかについては、次の1行だけ記入すれば、Actionが意図した挙動で成功するかを確かめることができます。

verify(mockNavController)
    .navigate(/* ここで遷移先のidを指定するか自動生成されたクラスのActionの関数を呼ぶ */)

実際のソースコード全体としては次のとおりです。 Espresso部分に関してもコメントを追加しました。

MailFormFragmentTest.kt


@RunWith(AndroidJUnit4::class)
class MailFormFragmentTest {

  @Test
  fun testNavigationFromMailFormToCreateAccount() {
      // MailForm入力欄をクリックする
      Espresso.onView(ViewMatchers.withId(R.id.mail_form)).perform(ViewActions.click())

      // Mail用にランダムな文字列を作成
      val randomString = RandomStringUtils.randomNumeric(3) + RandomStringUtils.random(
          20,
          "abcdefghijklmnopqrstuvwxyz"
      )
      val testMail = "wear$randomString@te.st"

      // MailForm入力欄に直接入れる。
      // InputTextActionというViewActionを実装したクラスを作成して入れるとソフトウェアキーボードの影響を受けない。
      Espresso.onView(ViewMatchers.withId(R.id.mail_form)).perform(InputTextAction(testMail))

      // Fabをクリックする
      Espresso.onView(ViewMatchers.withId(R.id.fab)).perform(ViewActions.click())

      // モックしたNavControllerでActionが正しく実行されたかどうかを確認する
      verify(mockNavController)
          .navigate(MailFormFragmentDirections.actionMailFormToCreateAccount(SnsType.Wear))
  }

}

これで、対象のクラスを右クリックして、Run 'MailFormFragmentTest(自分で作成したテストクラスの名前)'..を実行して、テストがパスすればNavigationの遷移のテストとしてはOKです。 実装に際して参考にした公式リンクは次のとおりです。

【参考リンク】

Test Navigation

Navigationのテストまで実装をすることができたら、開発中にいくつか気をつけることがあったので、一つずつ紹介させていただきます!

プロダクトに導入する際に気をつけること

開始先を分けたい場合は、プログラム的に設定する必要な場合があるので注意

Navigationでは開始する際に、最初のDestinationをデフォルトで設定する必要があります。 先に説明したNavigationの原則1にある通り、最初の目的地をxml上で定義する必要があります。

WEARでは次のように開始先を固定しています。...は省略を意味しています。今回は、度々例に挙げている、navigation_graph_entrance.xmlで説明します。

navigation_graph_entrance.xml

<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_host"
    app:startDestination="@+id/mail_form"> <!-- ここで開始先を定義する -->
...
</navigation>

しかし、開始先を分けたい状況が、少なからず出てくると思います。

WEARでは、Navigation Drawerにログインと会員登録からの導線があり、その後にNavigationを用いているため、開始先を分ける必要が出てきました。

画像でみると次の画面です。

f:id:vasilyjp:20191010121644p:plain

この場合は、条件付きナビゲーションという形になります。

このような場合に備えて、上記リンクのFirst-time user experienceという項目で対応方針を公式ドキュメントに記載されてあります。

やり方としては、main_fragmentのようなFragmentを用意して、登録用のNavigationGraphを用意して遷移させる手法です。こちらの方法が一番綺麗にできるため、できる限りこの方法を採用していくのが良いと思います。

ただし、既存のプロダクトで部分的にNavigationを使用する場合で、遷移箇所がDrawerの項目からとなると、また少し工夫する必要があります。

遷移後の画面では、ログインと会員登録にフローが分かれます。遷移元のDrawerの画面には、今回はNavigationの適用を行う予定はなく、「ボタンを押した後にフローを分けたい」という要件で部分的に適用するという状況が起きました。

この為、ドキュメント通りに

WEARでは、プログラム的に開始位置をFragmentが作られたタイミングで設定するようにしました。

コードとしては次のような形になります。

AuthType.kt

sealed class AuthType : Serializable {

    abstract fun setUpStartDestination(navigationHostFragment: Fragment?)

    object Login : AuthType() {
        override fun setUpStartDestination(navigationHostFragment: Fragment?) {
            // NavigationHostFragmentからNavigationGraphを取得する
            val inflater = navigationHostFragment?.findNavController()?.navInflater
                ?: throw IllegalArgumentException()
            val graph = inflater.inflate(R.navigation.navigation_graph_entrance)

            // NavigationGraphに開始先の目的地のFragmentのIDを指定
            graph.startDestination = R.id.login

            // NavigationGraphをNavigationHostFragmentに設定する
            navigationHostFragment.findNavController().graph = graph
        }
    }

    object Register : AuthType() {
        override fun setUpStartDestination(navigationHostFragment: Fragment?) {
            // NavigationHostFragmentからNavigationGraphを取得する
            val inflater = navigationHostFragment?.findNavController()?.navInflater
                ?: throw IllegalArgumentException()
            val graph = inflater.inflate(R.navigation.navigation_graph_entrance)

            // NavigationGraphに開始先の目的地のFragmentのIDを指定
            graph.startDestination = R.id.mail_form

            // NavigationGraphをNavigationHostFragmentに設定する
            navigationHostFragment.findNavController().graph = graph
        }
    }
}

AuthTypeというsealed classを定義してLogin用とRegister用のobjectを作成し、コメントにある通り、それぞれの分岐に対して目的地を指定することで開始先を分けることが可能です。 既存に部分的に導入した場合にこのようなパターンがありましたので、少しでも参考になれば幸いです。

次は、Navigationでの戻る挙動を変えたい場合について紹介します。

特定のFragmentの戻る挙動を変えたい場合はCustomBackNavigationを利用する

Navigationを用いているときに、要件として「この場合は戻るの挙動を変えて欲しい」ということが出てきます。

特定のFragmentで戻る挙動を変更したい場合に使えるのが、Custom Back Navigationです。

FragmentでonViewCreatedなどが呼ばれた際に、コールバックを設定します。

Fragmentで戻る挙動が変えたい場合については、コードだと次のようになります。

requireActivity().onBackPressedDispatcher.addCallback(this) {
  /**
   * ここら辺に戻るの挙動で追加したい処理を書く
   */
}

onBackPressedDispatcher.addCallbackにて処理したい内容を書いたコールバックを追加することで、該当のFragmentだけ戻るの挙動を変えることができます。 addCallbackの内部を見ると、defaultでenabled=trueになっているので、要件的にONとOFFを切り替える必要がなければ、上記のソースコードのみで問題ないです。

idを確認してから起動しないとクラッシュする

Navigationを用いて遷移する場合に、次のようなエラーが出てクラッシュする場合があります。

stackoverflowにも同様の事象に遭遇している状態がみられるようです。 現状としては、ボタンの連打防止で回避できたという方もいらっしゃいますが、それ以外の条件でも発生している場合もあり、私も連打ではなく、通常通りの押下で再現することがありました。 ライブラリ側のバグの可能性が高いのではないかと考えています。

stackoverflow.com

Fatal Exception: java.lang.IllegalArgumentException
navigation destination XXX is unknown to this NavController

このエラーは、開発中に、次のように遷移する箇所で発生することが度々ありました。

findNavController().navigate(
  // 遷移先のIDなどを指定する
)

調査したところ、現在のスタックの最上部のDestinationのidが遷移する元のFragmentのidと異なるタイミングがあり、そのことが起因しクラッシュすることがあります。 現状の対応方針としては、現在のスタックの最上部のDestinationのidが遷移する元のFragmentのidであるかどうかを事前にチェックするか、例外でエラーを握りつぶすかの、二つの手法のみのようです。

WEARでは、下記のように、チェックを行うようにして対応しました。 次のコードはメールフォーム入力画面を例にしています。

if(findNavController().currentDestination?.id == R.id.mail_form) {
  findNavController().navigate(
    // 遷移先のIDなどを指定する
  )
}

以上で、プロダクトにNavigationを導入する際に、気をつけることについて「これだけは事前に知っておくと嬉しいかも」をまとめてみました。何かの参考になれば嬉しいです!

さいごに

いかがだったでしょうか?

Navigationを実際にプロダクトに導入してみて、次のようなメリットとデメリットがあると感じました。

  • メリット

    • 視覚的に遷移先や各Actionで渡す情報を見ることができる
    • 導入までの流れが簡単にできる
    • 公式ドキュメントが充実している
    • スッキリと遷移部分を書くことができる
    • 遷移する部分でキーの設定や型の確認などをせずに、Navigation側(プラグイン)で自動でやってくれる
  • デメリット

    • 学習コストがある
    • 複数人で同一のNavigationGraphを触る場合、コンフリクトに注意する必要がある
    • Navigationライブラリ側のバグがある

他にもやり方があったり、もっとこうしたほうがいいというアドバイスがございましたら、私のTwitterアカウント@zukkey59までご連絡をいただけますと嬉しいです!

今後、新たに知見が溜まったら追加で書きたいと思います。

さいごに、ZOZOテクノロジーズでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!

tech.zozo.com

カテゴリー