【Android】Fragment間で値をやりとりする

# はじめに
あるFragmentで取得した値を、他のFragmentでも使いたい場面は多々あるかと思います。
この記事では、Fragment間で値をやりとりする方法をまとめました。

# 方法一覧
Fragment間で値をやりとりする方法は、ざっくり分けて次のとおりです。
**2020/04/30 `setFragmentResult` について追記しました**

- Bundleを使う
  - 【応用】SafeArgsを使う
- ActivityのスコープでViewModelを使う
- navGraphViewModelsを使う
- setFragmentResultを使う  ← New!!

抜けがあったら教えてください。

次に、それぞれのやり方について解説していきます。
※番外編としてViewPagerやBottomNavigationのページ間で値をやりとりする方法についても解説します。

## Bundleを使う
一番オーソドックスなやり方です。遷移先のFragmentを生成する際に値をargumentsとして渡します。

### やり方

以下のようにして遷移先のFragmentに値を渡します。

```kotlin
val title = "タイトル"
// Bundleインスタンスを作成
val bundle = Bundle()
// putXXXXで値をセットする
bundle.putString("BUNDLE_KEY_TITLE", title)
// Fragmentに値をセットする
val fragment = SecondFragment()
fragment.arguments = bundle
// 遷移処理
parentFragmentManager.beginTransaction()
        .add(R.id.container, fragment)
        .commit()
```

値を受け取る側はgetArguments(arguments)で値を取得します。

```kotlin:SecondFragment
// putXXXXに対応するgetXXXXで値を取得
val args = arguments?.getString("BUNDLE_KEY_TITLE") // "タイトル"
```

### こういう時に使える
Bundleを使った方法は、単方向の値のやりとりに向いています。

<img width="495" alt="スクリーンショット 2020-03-23 10.23.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309569/9d83ded4-9839-e7b5-3863-c5e51148af07.png">


Bundleで渡した値は読み取り専用のため、受け取った値を書き換えたり、加工した値をまたFragmentAで使いたい場合には向いていません。
（varで値を取得して書き換えてまたBundleに渡して…というやり方も出来なくはないですが、煩雑になるのでオススメは出来ません。おとなしくViewModelを使いましょう）

## 【応用】SafeArgsを使う
Bundleで渡した値を型安全に使える[SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja)というものが登場しました。

`bundle.putXXXX` や `arguments.getXXXX` で値をやりとりするのは便利ではありますが、渡すキー名を間違えると `NULL` が返ってくるため、簡単にクラッシュしてしまいます。
それをプロパティを介したアクセスにする事で、型安全にアクセス可能になります。

### やり方

Gradleに依存関係は追加済みとします。

SafeArgsは[Navigation Component](https://developer.android.com/guide/navigation/navigation-getting-started)のNavigation Graphを使用します。
Navigation Graphに `argument` タグを追加します。これは**遷移先のFragmentタグ内**に追加します。

```xml:nav_graph.xml
    <fragment android:id="@+id/firstFragment"
        ...>
       <action
            android:id="@+id/action_first_to_second"
            app:destination="@id/secondFragment"/>
    </fragment>
    <fragment android:id="@+id/secondFragment"
        ...>

        <argument
                android:name="title"
                app:argType="string"/>

    </fragment>
```

`argument` タグ追加後は一度ビルドしておきましょう。

SecondFragment遷移時に以下のように引数を渡します。

```kotlin:FirstFragment
val title = "タイトル"
val action = FirstFragmentDirections.actionFirstToSecond(title)
findNavController().navigate(action)
```

遷移後のFragmentで値を取り出すには `navArgs()` を使います。

```kotlin:SecondFragment
class SecondFragment : Fragment() {
    private val args: SecondFragmentArgs by navArgs()
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        textView.text = args.title // プロパティでアクセス可能
    }
}
```

Navigation Componentを使った遷移を使わない場合でも、SafeArgsだけを使用することが可能です。
Navigation Graphへの定義までは同じで、そのあとは以下のように使えます。


```kotlin:FirstFragment
val bundle = SecondFragmentArgs.Builder(title)
    .build()
    .toBunble()
val fragment = SecondFragment()
fragment.argument = bundle
// 遷移処理はBundleと同じなので省略
```

値の取り出し方はNavigation Componentを使った場合と同じです。

```kotlin:SecondFragment
private val args: SecondFragmentArgs by navArgs()
```

## ActivityのスコープでViewModelを使う
読んで字のとおりですが、ActivityのスコープでViewModelを定義し、それをFragment間で使いまわします。[公式でも紹介されているやり方](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=JA#sharing)です。

### やり方
例えばMainViewModelがあったとします。

```kotlin:MainViewModel
class MainViewModel: ViewModel()
```

これをFragmentで取得する際、Activityを引数に渡します。

```kotlin:FirstFragment
class FirstFragment: Fragment() {

    lateinit var viewModel: MainViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
    activity?.run {
            viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        }
    }
}
```

これでViewModelがActivityのViewModelStore(※)に保持されるので、他のFragmentでも同じViewModelのインスタンスを利用できます。

(※)ViewModelStore：ViewModelを保持するクラス

もしくは[Fragment-KTX](https://developer.android.com/kotlin/ktx?hl=ja#fragment)を利用すれば、 `activityViewModels` でもViewModelのスコープをActivityにできます。

```kotlin:FirstFragment
private val viewModel: MainViewModel by activityViewModels()
```

（独自のViewModelStoreOwnerを定義して利用する方法もあるようですが、ここでは割愛します。[こちら](https://qiita.com/hatt0519/items/4c48be589579f2b7e159)の記事が参考になりそうです）

### こういう時に使える
ViewModelを使った方法は、双方向の値のやりとりに向いています。
<img width="476" alt="スクリーンショット 2020-03-23 10.39.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309569/38de3297-dec4-69ba-c18a-eb948b12cc01.png">



FragmentAでもFragmentBでも値を参照し、かつ、どちらのFragmentでも値の変更が行われる可能性がある場合などは、BundleよりもViewModelを使った方が処理がスマートになると思います。

## navGraphViewModelsを使う
[Navigation Component](https://developer.android.com/guide/navigation/navigation-getting-started)と[Fragment-KTX](https://developer.android.com/kotlin/ktx?hl=ja#fragment)を組み合わせることによって、Navigation Graph単位のスコープを持ったViewModelを使うことが出来ます。
(ネストしたNavGraphでしか使えず、メインのNavGraphでは使えない事に注意が必要です)


### やり方
NavigationGraphの中に、以下のようなネストしたNavGraphとFragmentを定義します。
(説明のために色々端折っています)

```xml:nav_graph.xml
    <navigation android:id="@+id/nested_nav_graph"
        app:startDestination="@id/secondFragment">
        <fragment android:id="@+id/secondFragment">
        ...
        <fragment android:id="@+id/thirdFragment">
    </navigation>
```

Fragment上で以下のようにViewModelを取得します。

```kotlin:SecondFragment
class SecondFragment: Fragment() {
    private val viewModel: MainViewModel by navGraphViewModels<R.id.nested_nav_graph>()
}
```

```kotlin:SecondFragment
class ThirdFragment: Fragment() {
    private val viewModel: MainViewModel by navGraphViewModels<R.id.nested_nav_graph>()
}
```

これでSecondFragmentとThirdFragmentは共通のViewModelインスタンスを使えるようになります。
このViewModelはNavGraph単位のスコープを持つので、別のNavGraphに遷移した場合は値が破棄されます。

もっと詳しい情報はSTAR-ZEROさんの[こちら](https://medium.com/@star_zero/navigation-graph%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%AEviewmodel-8bcfebca8c13)の記事が参考になります。

### こういう時に使える
ActivityスコープのViewModelは、SingleActivityのアプリだと、実質どこのFragmentからでもアクセス出来てしまいます。
それではスコープが大きすぎる、という時はnavGraphViewModelsを使ってスコープ範囲を分割していくと良いかと思います。

## 番外編: ViewPagerやBottomNavigationViewの子ページ同士で値をやりとりする
ViewPagerやBottomNavigationViewを使っている時、ページ間で値のやりとりをしたい場合は、BundleやnavGraphViewModelsは使えません（たぶん）。

方法はいくつかあるかと思いますが、個人的にはViewModelProviderに `parentFragment` を渡して、ViewModelを共有するのが一番やりやすいと思います。

```kotlin:ChildFragment
    lateinit var viewModel: MainViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // parentFragmentを渡す
        viewModel = ViewModelProvider(parentFragment).get(MainViewModel::class.java)
        
    }
```

こうすることでViewModelのインスタンスが親FragmentのViewModelStoreに保持されるため、ページ間で共通のViewModelインスタンスを利用することが可能になります。


# setFragmentResultを使う
Fragment間の値のやり取りの方法として、 `setFragmentResult` が追加されました。

使用するにはFragment-KTXの1.3.0-alpha04以降が必要です。

```groovy:build.gradle
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha04"
```

### やり方

結果を渡す側は、以下のように requestKey と値を渡します。

```kotlin
setFragmentResult("request_key", bundleOf(
    "result_key1" to "result1",
    "result_key2" to 222
))
```

結果を受け取る側は、 `setFragmentResultListener` を使って以下のように受け取ります。

```kotlin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
    // 設定した requestKey を元にbundleを受け取る
    setFragmentResultListener("request_key") { requestKey, bundle ->
        val result1 = bundle.getString("result_key1") // "result1"
        val result2 = bundle.getInt("result_key2")    // 222
    }
}
```

# おわりに
本記事を書くにあたって、以下のリンク先を参考にさせて頂きました。
・[[Qiita] 【Kotlin】Bundleを使ったFragment間の値渡し](https://qiita.com/orimomo/items/0472a52f90d14bef0c89)
・[[Qiita] [Android] NavigationでSafeArgsを使って引数付き画面遷移をする](https://qiita.com/tktktks10/items/c78fe6f2083611676fbb)
・[ViewModel、ViewModelProviderについて調べてみた（Android）](https://medium.com/url-memo/viewmodel-viewmodelprovider-c95a19412805)
・[[Qiita] Activity, Fragmentを跨いでViewModelを共有する](https://qiita.com/hatt0519/items/4c48be589579f2b7e159)
・[Navigation GraphスコープのViewModel](https://medium.com/@star_zero/navigation-graph%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%AEviewmodel-8bcfebca8c13)
・[setFragmentResultを使ったFragment間のデータ受け渡し](https://medium.com/@star_zero/setfragmentresult%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Ffragment%E9%96%93%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97-9223ddc1ca38)