与片段通信

要重复使用片段,请将它们构建为完全独立的组件,这些组件定义自己的布局和行为。定义好这些可重复使用的片段后,您可以将它们与活动关联,并将其与应用程序逻辑连接起来,以实现整体的组合 UI。

为了适当地响应用户事件并共享状态信息,您通常需要在活动与其片段之间,或两个或多个片段之间建立通信渠道。为了保持片段的独立性,不要让片段直接与其他片段或其宿主活动进行通信。

Fragment 库提供了两种通信选项:共享的 ViewModel 和片段结果 API。推荐使用哪种选项取决于用例。若要使用自定义 API 共享持久数据,请使用 ViewModel。对于可以放置在 Bundle 中的一次性结果数据,请使用片段结果 API。

以下部分将向您展示如何使用 ViewModel 和片段结果 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);
        });
    }
}

在片段之间共享数据

同一活动中的两个或多个片段通常需要彼此通信。例如,想象一个显示列表的片段和另一个允许用户对列表应用各种过滤器的片段。如果没有片段直接通信,实现这种情况并不容易,但那样一来,它们就不再是自包含的了。此外,两个片段都必须处理另一个片段尚未创建或可见的情况。

这些片段可以使用它们活动的作用域共享一个 ViewModel 来处理这种通信。通过这种方式共享 ViewModel,片段不需要知道彼此的存在,活动也不需要做任何事情来促进通信。

以下示例展示了两个片段如何使用共享的 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 作用域到导航图

如果您正在使用 导航库,您还可以将 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 的更多信息,请参阅 以编程方式与导航组件交互

使用片段结果 API 获取结果

在某些情况下,您可能希望在两个片段之间或在片段与其宿主活动之间传递一次性值。例如,您可能有一个读取二维码的片段,并将数据传回前一个片段。

在 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.
        }
    });
}
fragment b sends data to fragment a using a FragmentManager
图 1. 片段 B 使用 FragmentManager 将数据发送到片段 A。

在片段 B(生成结果的片段)中,在相同的 FragmentManager 上设置结果,方法是使用相同的 requestKey。您可以使用 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 状态后执行监听器回调。

对于给定的键,您只能拥有一个监听器和结果。如果您多次为同一个键调用 setFragmentResult(),并且监听器没有处于 STARTED 状态,系统会将任何挂起的結果替换为您的更新结果。

如果您在没有相应的监听器来接收结果的情况下设置了结果,则该结果将存储在 FragmentManager 中,直到您使用相同的键设置监听器为止。一旦监听器接收到结果并触发 onFragmentResult() 回调,结果将被清除。这种行为有两个主要含义

  • 回退栈中的片段只有在被弹出并处于 STARTED 状态后才会接收到结果。
  • 如果监听结果的片段在设置结果时处于 STARTED 状态,那么监听器的回调将立即触发。

测试片段结果

使用 FragmentScenario 来测试对 setFragmentResult()setFragmentResultListener() 的调用。使用 launchFragmentInContainerlaunchFragment 为正在测试的片段创建场景,然后手动调用未被测试的方法。

要测试 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() 时,从父片段使用 getChildFragmentManager() 而不是 getParentFragmentManager()

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.
            }
        });
}
a child fragment can use FragmentManager to send a result
            to its parent
图 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.
            }
        });
    }
}