LoginSignup
62
39

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-22

はじめに

ある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)で値を取得します。

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

こういう時に使える

Bundleを使った方法は、単方向の値のやりとりに向いています。

スクリーンショット 2020-03-23 10.23.40.png

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

【応用】SafeArgsを使う

Bundleで渡した値を型安全に使えるSafeArgsというものが登場しました。

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

やり方

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

SafeArgsはNavigation ComponentのNavigation Graphを使用します。
Navigation Graphに argument タグを追加します。これは遷移先のFragmentタグ内に追加します。

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遷移時に以下のように引数を渡します。

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

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

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への定義までは同じで、そのあとは以下のように使えます。

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

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

SecondFragment
private val args: SecondFragmentArgs by navArgs()

ActivityのスコープでViewModelを使う

読んで字のとおりですが、ActivityのスコープでViewModelを定義し、それをFragment間で使いまわします。公式でも紹介されているやり方です。

やり方

例えばMainViewModelがあったとします。

MainViewModel
class MainViewModel: ViewModel()

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

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を利用すれば、 activityViewModels でもViewModelのスコープをActivityにできます。

FirstFragment
private val viewModel: MainViewModel by activityViewModels()

(独自のViewModelStoreOwnerを定義して利用する方法もあるようですが、ここでは割愛します。こちらの記事が参考になりそうです)

こういう時に使える

ViewModelを使った方法は、双方向の値のやりとりに向いています。
スクリーンショット 2020-03-23 10.39.07.png

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

navGraphViewModelsを使う

Navigation ComponentFragment-KTXを組み合わせることによって、Navigation Graph単位のスコープを持ったViewModelを使うことが出来ます。
(ネストしたNavGraphでしか使えず、メインのNavGraphでは使えない事に注意が必要です)

やり方

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

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を取得します。

SecondFragment
class SecondFragment: Fragment() {
    private val viewModel: MainViewModel by navGraphViewModels<R.id.nested_nav_graph>()
}
SecondFragment
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を共有するのが一番やりやすいと思います。

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以降が必要です。

build.gradle
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間のデータ受け渡し

62
39
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
39