はじめに
あるFragmentで取得した値を、他のFragmentでも使いたい場面は多々あるかと思います。
この記事では、Fragment間で値をやりとりする方法をまとめました。
方法一覧
Fragment間で値をやりとりする方法は、ざっくり分けて次のとおりです。
2020/04/30 setFragmentResult
について追記しました
- Bundleを使う
- 【応用】SafeArgsを使う
- ActivityのスコープでViewModelを使う
- navGraphViewModelsを使う
- setFragmentResultを使う ← New!!
抜けがあったら教えてください。
次に、それぞれのやり方について解説していきます。
※番外編としてViewPagerやBottomNavigationのページ間で値をやりとりする方法についても解説します。
Bundleを使う
一番オーソドックスなやり方です。遷移先のFragmentを生成する際に値をargumentsとして渡します。
やり方
以下のようにして遷移先のFragmentに値を渡します。
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)で値を取得します。
// putXXXXに対応するgetXXXXで値を取得
val args = arguments?.getString("BUNDLE_KEY_TITLE") // "タイトル"
こういう時に使える
Bundleを使った方法は、単方向の値のやりとりに向いています。
Bundleで渡した値は読み取り専用のため、受け取った値を書き換えたり、加工した値をまたFragmentAで使いたい場合には向いていません。
(varで値を取得して書き換えてまたBundleに渡して…というやり方も出来なくはないですが、煩雑になるのでオススメは出来ません。おとなしくViewModelを使いましょう)
【応用】SafeArgsを使う
Bundleで渡した値を型安全に使えるSafeArgsというものが登場しました。
bundle.putXXXX
や arguments.getXXXX
で値をやりとりするのは便利ではありますが、渡すキー名を間違えると NULL
が返ってくるため、簡単にクラッシュしてしまいます。
それをプロパティを介したアクセスにする事で、型安全にアクセス可能になります。
やり方
Gradleに依存関係は追加済みとします。
SafeArgsはNavigation ComponentのNavigation Graphを使用します。
Navigation Graphに argument
タグを追加します。これは遷移先のFragmentタグ内に追加します。
<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遷移時に以下のように引数を渡します。
val title = "タイトル"
val action = FirstFragmentDirections.actionFirstToSecond(title)
findNavController().navigate(action)
遷移後のFragmentで値を取り出すには navArgs()
を使います。
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への定義までは同じで、そのあとは以下のように使えます。
val bundle = SecondFragmentArgs.Builder(title)
.build()
.toBunble()
val fragment = SecondFragment()
fragment.argument = bundle
// 遷移処理はBundleと同じなので省略
値の取り出し方はNavigation Componentを使った場合と同じです。
private val args: SecondFragmentArgs by navArgs()
ActivityのスコープでViewModelを使う
読んで字のとおりですが、ActivityのスコープでViewModelを定義し、それをFragment間で使いまわします。公式でも紹介されているやり方です。
やり方
例えばMainViewModelがあったとします。
class MainViewModel: ViewModel()
これをFragmentで取得する際、Activityを引数に渡します。
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を利用すれば、 activityViewModels
でもViewModelのスコープをActivityにできます。
private val viewModel: MainViewModel by activityViewModels()
(独自のViewModelStoreOwnerを定義して利用する方法もあるようですが、ここでは割愛します。こちらの記事が参考になりそうです)
こういう時に使える
ViewModelを使った方法は、双方向の値のやりとりに向いています。
FragmentAでもFragmentBでも値を参照し、かつ、どちらのFragmentでも値の変更が行われる可能性がある場合などは、BundleよりもViewModelを使った方が処理がスマートになると思います。
navGraphViewModelsを使う
Navigation ComponentとFragment-KTXを組み合わせることによって、Navigation Graph単位のスコープを持ったViewModelを使うことが出来ます。
(ネストしたNavGraphでしか使えず、メインのNavGraphでは使えない事に注意が必要です)
やり方
NavigationGraphの中に、以下のようなネストしたNavGraphとFragmentを定義します。
(説明のために色々端折っています)
<navigation android:id="@+id/nested_nav_graph"
app:startDestination="@id/secondFragment">
<fragment android:id="@+id/secondFragment">
...
<fragment android:id="@+id/thirdFragment">
</navigation>
Fragment上で以下のようにViewModelを取得します。
class SecondFragment: Fragment() {
private val viewModel: MainViewModel by navGraphViewModels<R.id.nested_nav_graph>()
}
class ThirdFragment: Fragment() {
private val viewModel: MainViewModel by navGraphViewModels<R.id.nested_nav_graph>()
}
これでSecondFragmentとThirdFragmentは共通のViewModelインスタンスを使えるようになります。
このViewModelはNavGraph単位のスコープを持つので、別のNavGraphに遷移した場合は値が破棄されます。
もっと詳しい情報はSTAR-ZEROさんのこちらの記事が参考になります。
こういう時に使える
ActivityスコープのViewModelは、SingleActivityのアプリだと、実質どこのFragmentからでもアクセス出来てしまいます。
それではスコープが大きすぎる、という時はnavGraphViewModelsを使ってスコープ範囲を分割していくと良いかと思います。
番外編: ViewPagerやBottomNavigationViewの子ページ同士で値をやりとりする
ViewPagerやBottomNavigationViewを使っている時、ページ間で値のやりとりをしたい場合は、BundleやnavGraphViewModelsは使えません(たぶん)。
方法はいくつかあるかと思いますが、個人的にはViewModelProviderに parentFragment
を渡して、ViewModelを共有するのが一番やりやすいと思います。
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以降が必要です。
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha04"
やり方
結果を渡す側は、以下のように requestKey と値を渡します。
setFragmentResult("request_key", bundleOf(
"result_key1" to "result1",
"result_key2" to 222
))
結果を受け取る側は、 setFragmentResultListener
を使って以下のように受け取ります。
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間の値渡し
・[Qiita] [Android] NavigationでSafeArgsを使って引数付き画面遷移をする
・ViewModel、ViewModelProviderについて調べてみた(Android)
・[Qiita] Activity, Fragmentを跨いでViewModelを共有する
・Navigation GraphスコープのViewModel
・setFragmentResultを使ったFragment間のデータ受け渡し