与 fragment 通信

如需重复使用 fragment,请将其构建为完全独立的组件,可以自定义布局和行为。定义这些可重复使用的 fragment 后,您可以将其与 activity 关联,并将其与应用逻辑联系起来,以实现整体复合界面。

为了正确响应用户事件和共享状态信息,通常需要在 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 对象可存储和管理界面数据。如需详细了解 ViewModel,请参阅 ViewModel 概览

与宿主 activity 共享数据

在某些情况下,您可能需要在 fragment 与其宿主 activity 之间共享数据。例如,您可能想要根据 fragment 中的交互切换一个全局界面组件。

我们来考虑以下 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 或检索它(如果已存在)。这两个组件都可以观察和修改此数据。

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 通常需要相互通信。例如,假设有一个 ragment 显示一个列表,另一个 ragment 允许用户对该列表应用各种过滤器。如果没有 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 的范围限定为导航图

如果您使用的是 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 获取结果

在某些情况下,您可能要在 fragment 之间或 fragment 与其宿主 activity 之间传递一次性值。例如,您可能有一个 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 使用 FragmentManager 将数据发送到 fragment A
图 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 处于 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.
            }
        });
}
子 fragment 可以使用 FragmentManager 将结果发送到其父 fragment
图 2 子 fragment 可以使用 FragmentManager 将结果发送到其父 fragment。

子 fragment 在其 FragmentManager 上设置结果。然后,一旦父 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.
            }
        });
    }
}