フラグメントを再利用するには、それ自体のレイアウトと動作を定義する完全に自己完結型のコンポーネントとしてフラグメントを作成します。再利用可能なフラグメントをいったん定義すると、それらをアクティビティに関連付けるかアプリロジックに接続することにより、総合的な複合 UI を実現できます。
ユーザー イベントに適切に対応したり、状態情報を共有したりするには、多くの場合、アクティビティとそのフラグメント間、または 2 つ以上のフラグメント間に、通信チャネルが必要です。フラグメントを自己完結型のまま維持するには、フラグメントが他のフラグメントまたは自身のホスト アクティビティと直接通信しないようにしてください。
Fragment
ライブラリは、共有 ViewModel
と Fragment Result API の 2 つの通信オプションを提供します。推奨されるオプションは、ユースケースによって異なります。永続データをカスタム API と共有するには、ViewModel
を使用します。Bundle
に配置できるデータを含む 1 回限りの結果を取得するには、Fragment Result API を使用します。
以下のセクションでは、ViewModel
と Fragment Result API を使用して、フラグメントとアクティビティ間で通信を行う方法について説明します。
ViewModel を使用してデータを共有する
複数のフラグメント間、またはフラグメントとそのホスト アクティビティ間でデータを共有する必要がある場合は、ViewModel
が最適です。ViewModel
オブジェクトは、UI データを格納し、管理します。ViewModel
の詳細については、ViewModel の概要をご覧ください。
ホスト アクティビティとデータを共有する
場合によっては、フラグメントとそのホスト アクティビティ間でデータを共有する必要があります。たとえば、フラグメント内のインタラクションに基づいて、グローバル UI コンポーネントを切り替える場合などです。
次の ItemViewModel
について考えてみましょう。
Kotlin
class ItemViewModel : ViewModel() { private val mutableSelectedItem = MutableLiveData<Item>() val selectedItem: LiveData<Item> get() = mutableSelectedItem fun selectItem(item: Item) { mutableSelectedItem.value = item } }
Java
public class ItemViewModel extends ViewModel { private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>(); public void selectItem(Item item) { selectedItem.setValue(item); } public LiveData<Item> getSelectedItem() { return selectedItem; } }
この例では、保存されたデータは MutableLiveData
クラスにラップされています。LiveData
は、ライフサイクル対応の監視可能なデータホルダー クラスです。MutableLiveData
を使用すると、値を変更できます。LiveData
について詳しくは、LiveData の概要をご覧ください。
フラグメントとそのホスト アクティビティは、どちらもアクティビティを ViewModelProvider
コンストラクタに渡すことにより、アクティビティ スコープを持つ ViewModel
の共有インスタンスを取得できます。ViewModelProvider
は、ViewModel
のインスタンス化を処理(存在する場合は、取得)します。どちらのコンポーネントもこのデータを監視し、変更できます。
Kotlin
class MainActivity : AppCompatActivity() { // Using the viewModels() Kotlin property delegate from the activity-ktx // artifact to retrieve the ViewModel in the activity scope. private val viewModel: ItemViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.selectedItem.observe(this, Observer { item -> // Perform an action with the latest item data. }) } } class ListFragment : Fragment() { // Using the activityViewModels() Kotlin property delegate from the // fragment-ktx artifact to retrieve the ViewModel in the activity scope. private val viewModel: ItemViewModel by activityViewModels() // Called when the item is clicked. fun onItemClicked(item: Item) { // Set a new item. viewModel.selectItem(item) } }
Java
public class MainActivity extends AppCompatActivity { private ItemViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ItemViewModel.class); viewModel.getSelectedItem().observe(this, item -> { // Perform an action with the latest item data. }); } } public class ListFragment extends Fragment { private ItemViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class); ... items.setOnClickListener(item -> { // Set a new item. viewModel.select(item); }); } }
フラグメント間でデータを共有する
同じアクティビティ内の 2 つ以上のフラグメントが相互に通信する必要が生じることはよくあります。たとえば、1 つのリストを表示するフラグメントと、ユーザーがそのリストにさまざまなフィルタを適用するためのフラグメントがあるとします。このケースの実装は、フラグメント同士が直接通信しなければ簡単ではありませんが、その場合自己完結型ではなくなります。さらに、どちらのフラグメントも、もう一方のフラグメントがまだ作成または表示されていない状況に対応する必要があります。
この場合、フラグメントは、各自のアクティビティ スコープを使用して ViewModel
を共有することにより、通信を処理できます。この方法で ViewModel
を共有すれば、フラグメントはお互いを認識する必要がなくなり、アクティビティは通信を容易にするための処理をしなくてもよくなります。
次の例は、2 つのフラグメントが共有 ViewModel
を使用して通信する方法を示しています。
Kotlin
class ListViewModel : ViewModel() { val filters = MutableLiveData<Set<Filter>>() private val originalList: LiveData<List<Item>>() = ... val filteredList: LiveData<List<Item>> = ... fun addFilter(filter: Filter) { ... } fun removeFilter(filter: Filter) { ... } } class ListFragment : Fragment() { // Using the activityViewModels() Kotlin property delegate from the // fragment-ktx artifact to retrieve the ViewModel in the activity scope. private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI. } } } class FilterFragment : Fragment() { private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filters.observe(viewLifecycleOwner, Observer { set -> // Update the selected filters UI. } } fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter) fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter) }
Java
public class ListViewModel extends ViewModel { private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>(); private final LiveData<List<Item>> originalList = ...; private final LiveData<List<Item>> filteredList = ...; public LiveData<List<Item>> getFilteredList() { return filteredList; } public LiveData<Set<Filter>> getFilters() { return filters; } public void addFilter(Filter filter) { ... } public void removeFilter(Filter filter) { ... } } public class ListFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class); viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> { // Update the list UI. }); } } public class FilterFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class); viewModel.getFilters().observe(getViewLifecycleOwner(), set -> { // Update the selected filters UI. }); } public void onFilterSelected(Filter filter) { viewModel.addFilter(filter); } public void onFilterDeselected(Filter filter) { viewModel.removeFilter(filter); } }
どちらのフラグメントも各自のホスト アクティビティを ViewModelProvider
のスコープとして使用します。これらのフラグメントは同じスコープを使用するため、ViewModel
の同じインスタンスを受け取ります。これにより、フラグメントの相互通信が可能になります。
親フラグメントと子フラグメント間でデータを共有する
子フラグメントを使用する場合、親フラグメントとその子フラグメントが相互にデータを共有する必要が生じることがあります。これらのフラグメント間でデータを共有するには、以下の例のように、親フラグメントを ViewModel
スコープとして使用します。
Kotlin
class ListFragment: Fragment() { // Using the viewModels() Kotlin property delegate from the fragment-ktx // artifact to retrieve the ViewModel. private val viewModel: ListViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI. } } } class ChildFragment: Fragment() { // Using the viewModels() Kotlin property delegate from the fragment-ktx // artifact to retrieve the ViewModel using the parent fragment's scope private val viewModel: ListViewModel by viewModels({requireParentFragment()}) ... }
Java
public class ListFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { viewModel = new ViewModelProvider(this).get(ListViewModel.class); viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> { // Update the list UI. } } } public class ChildFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class); ... } }
ViewModel のスコープをナビゲーション グラフに設定する
Navigation ライブラリを使用している場合は、ViewModel
のスコープをデスティネーションの NavBackStackEntry
のライフサイクルに設定することもできます。たとえば、ViewModel
のスコープを ListFragment
の NavBackStackEntry
に設定します。
Kotlin
class ListFragment: Fragment() { // Using the navGraphViewModels() Kotlin property delegate from the fragment-ktx // artifact to retrieve the ViewModel using the NavBackStackEntry scope. // R.id.list_fragment == the destination id of the ListFragment destination private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { item -> // Update the list UI. } } }
Java
public class ListFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { NavController navController = NavHostFragment.findNavController(this); NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.list_fragment) viewModel = new ViewModelProvider(backStackEntry).get(ListViewModel.class); viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> { // Update the list UI. } } }
ViewModel
のスコープを NavBackStackEntry
に設定する方法の詳細については、プログラムで Navigation コンポーネントを操作するをご覧ください。
Fragment Result API を使用して結果を取得する
2 つのフラグメント間、またはフラグメントとそのホスト アクティビティ間で、1 回限りの値を受け渡したい場合があります。たとえば、QR コードを読み取るフラグメントでは、先行するフラグメントにデータを戻す状況が考えられます。
Fragment バージョン 1.3.0 以降では、個々の FragmentManager
に FragmentResultOwner
が実装されています。これは、FragmentManager
がフラグメントの結果の中央倉庫として機能できることを意味します。この変更によって、コンポーネントがお互いを直接参照しなくても、フラグメントの結果を設定してその結果をリッスンすることにより、コンポーネントの相互通信が可能になります。
フラグメント間で結果を受け渡す
フラグメント B からフラグメント A にデータを戻すには、まず、フラグメント A(結果を受け取るフラグメント)に結果リスナーを設定します。次の例に示すように、フラグメント A の FragmentManager
で setFragmentResultListener()
を呼び出します。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Use the Kotlin extension in the fragment-ktx artifact. setFragmentResultListener("requestKey") { requestKey, bundle -> // We use a String here, but any type that can be put in a Bundle is supported. val result = bundle.getString("bundleKey") // Do something with the result. } }
Java
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() { @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { // We use a String here, but any type that can be put in a Bundle is supported. String result = bundle.getString("bundleKey"); // Do something with the result. } }); }
フラグメント B(結果を生成するフラグメント)では、同じ requestKey
を使用して、同じ FragmentManager
に結果を設定します。そのためには、setFragmentResult()
API を使用します。
Kotlin
button.setOnClickListener { val result = "result" // Use the Kotlin extension in the fragment-ktx artifact. setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
Java
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle result = new Bundle(); result.putString("bundleKey", "result"); getParentFragmentManager().setFragmentResult("requestKey", result); } });
フラグメント A は結果を受け取り、フラグメントが STARTED
になったら、リスナー コールバックを実行します。
特定のキーに対してリスナーと結果を 1 つだけ設定できます。同じキーで setFragmentResult()
を複数回呼び出した場合、リスナーが STARTED
でなければ、システムは保留中の結果を更新された結果に置き換えます。
結果を受け取る側の対応するリスナーなしで結果を設定した場合は、同じキーでリスナーを設定するまで、結果は FragmentManager
に格納されます。リスナーが結果を受け取って onFragmentResult()
コールバックを起動すると、結果はクリアされます。この動作には主に 2 つの意味があります。
- バックスタックのフラグメントは、ポップされて
STARTED
になるまでは結果を受け取りません。 - 結果が設定されたときに、結果をリッスンするフラグメントが
STARTED
だった場合、リスナーのコールバックが直ちに起動されます。
フラグメントの結果をテストする
setFragmentResult()
と setFragmentResultListener()
の呼び出しをテストするには、FragmentScenario
を使用します。テスト対象のフラグメントのシナリオを作成するには、launchFragmentInContainer
または launchFragment
を使用して、テスト対象でないメソッドを手動で呼び出します。
setFragmentResultListener()
をテストするには、setFragmentResultListener()
を呼び出すフラグメントを使用するシナリオを作成します。次に、setFragmentResult()
を直接呼び出して結果を確認します。
@Test
fun testFragmentResultListener() {
val scenario = launchFragmentInContainer<ResultListenerFragment>()
scenario.onFragment { fragment ->
val expectedResult = "result"
fragment.parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to expectedResult))
assertThat(fragment.result).isEqualTo(expectedResult)
}
}
class ResultListenerFragment : Fragment() {
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact.
setFragmentResultListener("requestKey") { requestKey, bundle ->
result = bundle.getString("bundleKey")
}
}
}
setFragmentResult()
をテストするには、setFragmentResult()
を呼び出すフラグメントを使用するシナリオを作成します。次に、setFragmentResultListener()
を直接呼び出して結果を確認します。
@Test
fun testFragmentResult() {
val scenario = launchFragmentInContainer<ResultFragment>()
lateinit var actualResult: String?
scenario.onFragment { fragment ->
fragment.parentFragmentManager
.setFragmentResultListener("requestKey") { requestKey, bundle ->
actualResult = bundle.getString("bundleKey")
}
}
onView(withId(R.id.result_button)).perform(click())
assertThat(actualResult).isEqualTo("result")
}
class ResultFragment : Fragment(R.layout.fragment_result) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById(R.id.result_button).setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact.
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
}
親フラグメントと子フラグメント間で結果を受け渡す
子フラグメントから親に結果を渡すには、親フラグメントが setFragmentResultListener()
を呼び出すときに getParentFragmentManager()
ではなく getChildFragmentManager()
を使用します。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Set the listener on the child fragmentManager. childFragmentManager.setFragmentResultListener("requestKey") { key, bundle -> val result = bundle.getString("bundleKey") // Do something with the result. } }
Java
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set the listener on the child fragmentManager. getChildFragmentManager() .setFragmentResultListener("requestKey", this, new FragmentResultListener() { @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { String result = bundle.getString("bundleKey"); // Do something with the result. } }); }
子フラグメントは FragmentManager
に結果を設定します。フラグメントが STARTED
になると、親は結果を受け取ります。
Kotlin
button.setOnClickListener { val result = "result" // Use the Kotlin extension in the fragment-ktx artifact. setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
Java
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle result = new Bundle(); result.putString("bundleKey", "result"); // The child fragment needs to still set the result on its parent fragment manager. getParentFragmentManager().setFragmentResult("requestKey", result); } });
ホスト アクティビティ内で結果を受け取る
ホスト アクティビティ内でフラグメントの結果を受け取るには、getSupportFragmentManager()
を使用して、フラグメント マネージャーに結果リスナーを設定します。
Kotlin
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager .setFragmentResultListener("requestKey", this) { requestKey, bundle -> // We use a String here, but any type that can be put in a Bundle is supported. val result = bundle.getString("bundleKey") // Do something with the result. } } }
Java
class MainActivity extends AppCompatActivity { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() { @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { // We use a String here, but any type that can be put in a Bundle is supported. String result = bundle.getString("bundleKey"); // Do something with the result. } }); } }