Sunflowerリポジトリで学ぶJetPack〜Navigation編


新型コロナの影響で自宅待機になってしまい、その間勉強するものとして`Sunflower`リポジトリを
勧めてもらいました。

JetPackのライブラリのうち、今回は`Navigation`編です

尚、引用しているソースは明記しているところ以外は、全て`Sunflower`のリポジトリのものです。　

# 環境
* Android Studioは`3.3`以上が必要 
* 確認時は`3.6.2`を使用

# Navgation を利用するのに必要なこと
大きく分けて３つ必要です
1. 準備(依存関係の記載)
1. レイアウトファイルにナビゲーションを使うことを記載する
3. ナビゲーション グラフのファイルを作る

# 準備

## 依存関係の記載

* 公式によると、`build.gradle`に以下の４つの依存関係を記載する必要があります。
    * Java language implementation
    * Kotlin
    * Dynamic Feature Module Support
    * Testing Navigation  

### 公式の`build.gradle`のサンプル

```gradle:build.gradle
    dependencies {
      def nav_version = "2.3.0-alpha01"

      // Java language implementation
      implementation "androidx.navigation:navigation-fragment:$nav_version"
      implementation "androidx.navigation:navigation-ui:$nav_version"

      // Kotlin
      implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
      implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

      // Dynamic Feature Module Support
      implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

      // Testing Navigation
      androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
    }
    
```
<blockquote>
<p style="text-align: right;"><cite>引用元：<a href="https://developer.android.com/guide/navigation/navigation-getting-started?hl=ja#Set-up">Navigation コンポーネント スタートガイド | 環境をセットアップする</a></cite></p>
</blockquote>

## gradleファイル

それでは、Sunflowerの`build.gradle`を見てみましょう。
`Navigation`と関係ないところは省略
### build.gradle(sunflower)
```gradle:build.gradle
buildscript {
    // Define versions in a single place
    ext {
               :
        navigationVersion = '2.2.0'
               :
        }
    dependencies {
               :
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}

```
公式のチュートリアルにはないものが含まれています！
こちらは `safe args`の機能を利用する際に記載の必要があるものです。
Navgation利用時に安全にパラメータを渡す機能です。
こちらは`root`の`build.gradle`に記載する必要があるとのこと。
https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args


#### build.gradle(:app)
```gradle:app/build.gradle
dependencies {
              :
    implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
    implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
              :
```
`$rootProject.navigationVersion`は`build.gradle(sunflower)`で定義している`navigationVersion = '2.2.0'`ですね

#### ここからわかること
* Sunflowerリポジトリでは、下記のものは利用していないので、依存関係の記載がない。
    * `Java`
    * `Dynamic Feature Module Support`、
    * `Navigationのテスト`
* `kotlin`は使っているので、依存関係の記載がある。

# layout
次にレイアウトファイルを見てみましょう。
最初に表示される画面から詳細画面に遷移するところで使っているはずなので、
最初に表示する画面を見てみます。
起動時の`Activity`は`GardenActivity`でそこで`setContentView`しているxmlファイルは`activity_garden.xml`です
レイアウトはこんな感じ

### activity_garden.xml  

```XML:activity_garden.xml  
<fragment
    android:id="@+id/nav_host"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_garden"/>
</FrameLayout>
```
お、やっぱりここに`Navgation`関連の設定をしています。

#### 必要なこと

1. `android:name` には `androidx.navigation.fragment.NavHostFragment`を設定する
    - **この`fragment`は`Navigation`を使うよ！**
1. `app:navGraph` には 利用するNavigationのXMLファイルを記載します。ここで`NavGraph`と関連付けます。
    - **ナビゲーショングラフはこのファイルを使うよ！**
   
#### その他

- `app:defaultNavHost="true"`を設定すると、戻るボタンで戻るようになります。
    - 1つの画面に複数の`Fragment`があり、両方で`Navgation`を利用している時などに、戻るボタンを効かなくする、
とかの時には`false`にする模様

# ナビゲーショングラフ
レイアウトで指定されていた`nav_garden`のファイルを見てみましょう

## Navigation Editor
<img width="1058" alt="NavEditor.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/615702/074dcc0c-d101-de0f-c5da-0ed631330666.png">

こんな感じで、画面が複数と、その間を結ぶ矢印が表示されています。

## XMLファイル

```XML:nav_garden.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"
    app:startDestination="@id/view_pager_fragment">

    <fragment
        android:id="@+id/view_pager_fragment"
        android:name="com.google.samples.apps.sunflower.HomeViewPagerFragment"
        tools:layout="@layout/fragment_view_pager">

        <action
                android:id="@+id/action_view_pager_fragment_to_plant_detail_fragment"
                app:destination="@id/plant_detail_fragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right" />

    </fragment>

    <fragment
        android:id="@+id/plant_detail_fragment"
        android:name="com.google.samples.apps.sunflower.PlantDetailFragment"
        android:label="@string/plant_details_title"
        tools:layout="@layout/fragment_plant_detail">
        <argument
            android:name="plantId"
            app:argType="string" />
    </fragment>

</navigation>
```

### ポイント
1. 最初に表示する画面が`<navigation>`タグの`app:startDestination`に記載されている。
1. 画面遷移に関連する画面が`<fragment>タグ`で記載されている。
1. 画面遷移の処理詳細が、遷移元の画面に`<action>`タグで記載されている。
1. 遷移先画面に渡されるパラメータが遷移先画面に`<argment>`タグで記載されている。

※この辺りは、`Navigation Editor`で画面をクリックしてグイーンとやって・・・みたいな感じで設定できる部分もあるようです！
（`iOS`でいう`Storyboard`みたいな感じでしょうか？）
https://developer.android.com/guide/navigation/navigation-getting-started?hl=ja#Designate-start

### actionタグ設定値の記載方法について
特有の設定値については下記となっています。
※アニメーション関連の設定値が分かりづらかったので
こちらを参照しました
  https://stackoverflow.com/questions/56285197/what-is-the-difference-between-enteranim-popenteranim-exitanim-popexitanim
https://developer.android.com/guide/navigation/navigation-animate-transitions

つまるところ、アニメーションはPUSH時とPOP時で分けて記述が可能ということのようです。

| 名称                | 内容　　　|    設定されている値  　　　　　  |
|:-------------------|:-------------------------------|:----------------------------|
| `android:id` 　　　 | 遷移アクションの名称（これをソース内に記載する）| `@id/action_view_pager_fragment_to_plant_detail_fragment` |
| `app:destination`  | 遷移先の画面（宛先）                | `@id/plant_detail_fragment` |
| `app:enterAnim`    | 宛先に遷移する際のアニメーション       | `@anim/slide_in_right`      |
| `app:exitAnim`     | 宛先からに出て行く時のアニメーション    | `@anim/slide_out_left`      |
| `app:popEnterAnim` | 宛先にPOPで遷移する時のアニメーション  | `@anim/slide_in_left`       |
| `app:popExitAnim`  | 宛先からPOPで出て行く時のアニメーション | `@anim/slide_out_right`     |

### 使用箇所について
- RecyclerViewの項目をタップした時に、`navigatioToPlant`で画面遷移を実施している。
- `navigatioToPlant`の`actionViewPagerFragmentToPlantDetailFragment（plantId)`で遷移先を決定する`NavDirections`を取得している。
- `NavDirections`の取得には`NavDirections`インターフェースを継承したクラス`ActionViewPagerFragmentToPlantDetailFragment`のコンストラクタ`ActionViewPagerFragmentToPlantDetailFragment(plantId)`使用している。
- `ActionViewPagerFragmentToPlantDetailFragment`クラスでは、`NavDirections`インターフェースの`getActionId()`メソッドをオーバーライドしている。
- `getActionId()`メソッドは <font color="Red">**action_view_pager_fragment_to_plant_detail_fragment**</font>から取得している。  
⬆︎先ほどナビゲーショングラフのXMLファイルで定義していた`action`のIDですね！(やっと出てきた！(^^;;)


```kotlin:GardenPlantingAdapter.kt
    class ViewHolder(
        private val binding: ListItemGardenPlantingBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        init {
            binding.setClickListener { view ->
                binding.viewModel?.plantId?.let { plantId ->
                    navigateToPlant(plantId, view)
                }
            }
        }

        private fun navigateToPlant(plantId: String, view: View) {
            val direction = HomeViewPagerFragmentDirections
                .actionViewPagerFragmentToPlantDetailFragment(plantId)
            view.findNavController().navigate(direction)
        }
```

```kotlin:HomeViewPagerFragmentDirections.kt
class HomeViewPagerFragmentDirections private constructor() {
  private data class ActionViewPagerFragmentToPlantDetailFragment(
    val plantId: String
  ) : NavDirections {
    override fun getActionId(): Int = R.id.action_view_pager_fragment_to_plant_detail_fragment

    override fun getArguments(): Bundle {
      val result = Bundle()
      result.putString("plantId", this.plantId)
      return result
    }
  }

  companion object {
    fun actionViewPagerFragmentToPlantDetailFragment(plantId: String): NavDirections =
        ActionViewPagerFragmentToPlantDetailFragment(plantId)
  }
}
```


## 引数の設定
遷移先の画面の<Fragment>タグ内の`<argument>`タグ内に、
遷移先画面で受け取る引数を設定可能

| 名称                | 内容　　　|    設定されている値  　　　　　  |
|:-------------------|:-------------------------------|:----------------------------|
| `android:name`  | 引数の名前 | `plantId` |
| `app:argType`    | 引数の型      | `string`      |

### 遷移先での引数の取得
遷移先のコードは下記です。

```kotlin:PlantDetailFragment.kt
private val args: PlantDetailFragmentArgs by navArgs()

private val plantDetailViewModel: PlantDetailViewModel by viewModels {
    InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
}
```
公式のドキュメント
https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja#Safe-args
によると、

```
受信側デスティネーションのコード内で、getArguments() メソッドを使用して、バンドルを取得し、そのコンテンツを使用します。-ktx 依存関係を使用している場合、Kotlin ユーザーは、by navArgs() プロパティ デリゲートを使用して引数にアクセスすることもできます。
```
なるほど-
ここでは`safe args`という機能を利用して、遷移元から渡されたデータを遷移先で`args`という変数で取得し、
それを`ViewModel`に入れてますねー。
で、画面ではその`ViewModel`を介してデータを使用するようになっています。

# まとめ

Sunflowerのリポジトリでは

* `Kotlin`で`Navigation`を利用している
* `Navigation`を利用する時ために、以下のところを追加・変更している
    * レイアウト
    * ナビゲーショングラフ
    * 呼び出し元
    * 呼び出し先で引数の取得
* `Safe Args`という機能で遷移元から遷移先へデータを渡している

以上です！

# 参考サイト
* [Sunflowerリポジトリ](https://github.com/android/sunflower)
* [Navigation](https://developer.android.com/guide/navigation?hl=ja): 公式ドキュメント