与 Fragment 通信

要重用 Fragment,请将其构建为完全独立的组件,这些组件定义其自身的布局和行为。定义这些可重用 Fragment 后,您可以将其与 Activity 关联,并将其与应用逻辑连接起来,以实现整体复合 UI。

为了正确响应用户事件并共享状态信息,您通常需要在 Activity 及其 Fragment 之间或两个或更多 Fragment 之间建立通信通道。为使 Fragment 保持独立性,请 让 Fragment 直接与其他 Fragment 或其宿主 Activity 通信。

Fragment 库提供了两种通信选项:共享 ViewModel 和 Fragment Result API。建议的选项取决于用例。要通过自定义 API 共享持久性数据,请使用 ViewModel。对于可放入 Bundle 中的一次性结果数据,请使用 Fragment Result API。

以下部分将向您展示如何使用 ViewModel 和 Fragment Result API 在您的 Fragment 和 Activity 之间进行通信。

使用 ViewModel 共享数据

当您需要在多个 Fragment 之间或 Fragment 及其宿主 Activity 之间共享数据时,ViewModel 是理想的选择。ViewModel 对象存储和管理 UI 数据。如需详细了解 ViewModel,请参阅ViewModel 概览

与宿主 Activity 共享数据

在某些情况下,您可能需要在 Fragment 及其宿主 Activity 之间共享数据。例如,您可能希望根据 Fragment 中的交互来切换全局 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 概览

您的 Fragment 及其宿主 Activity 都可以通过将 Activity 传入 ViewModelProvider 构造函数,来检索具有 Activity 作用域的 ViewModel 共享实例。ViewModelProvider 负责实例化 ViewModel 或在 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);
        });
    }
}

在 Fragment 之间共享数据

同一个 Activity 中的两个或更多 Fragment 通常需要相互通信。例如,假设一个 Fragment 显示一个列表,而另一个 Fragment 允许用户对该列表应用各种过滤器。如果不让 Fragment 直接通信,实现此用例并非易事,但那样它们就不再是独立的了。此外,两个 Fragment 都必须处理另一个 Fragment 尚未创建或不可见的情况。

这些 Fragment 可以使用其 Activity 作用域共享 ViewModel 来处理这种通信。通过这种方式共享 ViewModel,Fragment 之间无需相互了解,Activity 也无需执行任何操作来促进通信。

以下示例展示了两个 Fragment 如何使用共享 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);
    }
}

两个 Fragment 都将其宿主 Activity 用作 ViewModelProvider 的作用域。由于这些 Fragment 使用相同的作用域,因此它们会收到 ViewModel 的相同实例,从而使它们能够相互通信。

在父 Fragment 和子 Fragment 之间共享数据

使用子 Fragment 时,您的父 Fragment 及其子 Fragment 可能需要相互共享数据。要在这些 Fragment 之间共享数据,请使用父 Fragment 作为 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,请参阅以编程方式与导航组件交互

使用 Fragment Result API 获取结果

在某些情况下,您可能希望在两个 Fragment 之间或 Fragment 及其宿主 Activity 之间传递一次性值。例如,您可能有一个读取 QR 码的 Fragment,并将数据传回给前一个 Fragment。

在 Fragment 1.3.0 及更高版本中,每个 FragmentManager 都实现了 FragmentResultOwner。这意味着 FragmentManager 可以充当 Fragment 结果的中央存储。此更改允许组件通过设置 Fragment 结果和监听这些结果进行通信,而无需这些组件直接相互引用。

在 Fragment 之间传递结果

要将数据从 Fragment B 传回给 Fragment A,首先在 Fragment A(接收结果的 Fragment)上设置结果监听器。在 Fragment 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. Fragment B 使用 FragmentManager 将数据发送到 Fragment A。

在 Fragment B(生成结果的 Fragment)中,使用相同的 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);
    }
});

Fragment A 然后接收结果,并在 Fragment 处于 STARTED 状态时执行监听器回调。

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

如果您设置了结果但没有相应的监听器接收它,则结果会存储在 FragmentManager 中,直到您使用相同的键设置监听器。一旦监听器收到结果并触发 onFragmentResult() 回调,结果就会被清除。此行为有两个主要影响:

  • 返回堆栈上的 Fragment 不会接收结果,直到它们被弹出并处于 STARTED 状态。
  • 如果监听结果的 Fragment 在结果设置时处于 STARTED 状态,则监听器的回调会立即触发。

测试 Fragment 结果

使用 FragmentScenario 测试对 setFragmentResult()setFragmentResultListener() 的调用。通过使用 launchFragmentInContainerlaunchFragment 为待测试的 Fragment 创建一个场景,然后手动调用未被测试的方法。

要测试 setFragmentResultListener(),请为调用 setFragmentResultListener() 的 Fragment 创建一个场景。接下来,直接调用 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() 的 Fragment 创建一个场景。接下来,直接调用 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))
        }
    }
}

在父 Fragment 和子 Fragment 之间传递结果

要将结果从子 Fragment 传递给父 Fragment,在调用 setFragmentResultListener() 时,请使用父 Fragment 的 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 子 Fragment 可以使用 FragmentManager 将结果发送给其父 Fragment。

子 Fragment 在其 FragmentManager 上设置结果。父 Fragment 然后在 Fragment 处于 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);
    }
});

在宿主 Activity 中接收结果

要在宿主 Activity 中接收 Fragment 结果,请使用 getSupportFragmentManager() 在 Fragment 管理器上设置结果监听器。

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.
            }
        });
    }
}