フラグメントと通信する

フラグメントを再利用するには、各フラグメントを、独自のレイアウトと動作を定義する完全に自己完結型のコンポーネントとして作成します。再利用可能なフラグメントをいったん定義すると、それらをアクティビティに関連付けるかアプリロジックに接続することにより、総合的な複合 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 のスコープを ListFragmentNavBackStackEntry に設定します。

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-alpha04 以降では、個々の FragmentManagerFragmentResultOwner が実装されています。これは、FragmentManager がフラグメントの結果の中央倉庫として機能できることを意味します。この変更によって、コンポーネントがお互いを直接参照しなくても、フラグメントの結果を設定してその結果をリッスンすることにより、コンポーネントの相互通信が可能になります。

フラグメント間で結果を受け渡す

フラグメント B からフラグメント A にデータを戻すには、まず、フラグメント A(結果を受け取るフラグメント)に結果リスナーを設定します。次の例に示すように、フラグメント A の FragmentManagersetFragmentResultListener() を呼び出します。

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
        }
    });
}
FragmentManager を使用してフラグメント B がフラグメント A にデータを送信する
図 1. FragmentManager を使用してフラグメント B がフラグメント A にデータを送信する

フラグメント 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)
    // We 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);
    // We 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 を使用して子フラグメントが親に結果を送信する
図 2. FragmentManager を使用して子フラグメントが親に結果を送信する

子フラグメントは 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
            }
        });
    }
}