与 Fragment 通信

为了重复使用 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

KotlinJava

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

KotlinJava

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

注意:务必将适当的范围与 ViewModelProvider 一起使用。在前面的示例中,MainActivity 用作 MainActivity 和 ListFragment 中的范围,因此为它们提供了相同的 ViewModel。如果 ListFragment 将自身用作范围,会为其提供与 MainActivity 不同的 ViewModel

在 Fragment 之间共享数据

同一 Activity 中的两个或更多 Fragment 通常需要相互通信。例如,假设有一个 Fragment 显示一个列表,另一个 Fragment 允许用户对该列表应用各种过滤器。如果 Fragment 不直接通信(这意味着,它们不再独立),这种情况可能不容易实现。此外,这两个 Fragment 还必须处理另一个 Fragment 尚未创建或不可见的情况。

这两个 Fragment 可以使用其 Activity 范围共享 ViewModel 来处理这种通信。通过以这种方式共享 ViewModel,Fragment 不需要相互了解,Activity 也不需要执行任何操作来促进通信。

以下示例展示了两个 Fragment 如何使用共享的 ViewModel 进行通信:

KotlinJava

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 的同一实例,这使它们能够来回通信。

注意ViewModel 会一直在内存中,直到其范围限定到的 ViewModelStoreOwner 永久消失。在一个 Activity 架构中,如果 ViewModel 的范围限定为 Activity,那么它本质上是单例。首次实例化 ViewModel 之后,使用 Activity 范围检索 ViewModel 的后续调用始终返回相同的现有 ViewModel 以及现有数据,直到 Activity 的生命周期永久结束。

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

使用子 Fragment 时,父 Fragment 及其子 Fragment 可能需要相互共享数据。如需在这些 Fragment 之间共享数据,请将父 Fragment 用作 ViewModel 范围。

KotlinJava

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

KotlinJava

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-alpha04 开始,每个 FragmentManager 都会实现 FragmentResultOwner。这意味着,FragmentManager 可以充当 Fragment 结果的集中存储区。此更改通过设置 Fragment 结果并监听这些结果而不要求组件直接相互引用,让这些组件能够相互通信。

在 Fragment 之间传递结果

如需将数据从 Fragment B 传回 Fragment A,请先在 Fragment A(即接收结果的 Fragment)上设置结果监听器。对 Fragment A 的 FragmentManager 调用 setFragmentResultListener(),如以下示例所示:

KotlinJava

@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 来完成此操作:

KotlinJava

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 结果存储在 FragmentManager 级别,因此 Fragment 必须随父 FragmentManager 一起附加到对 setFragmentResultListener() 或 setFragmentResult() 的调用。

测试 Fragment 结果

使用 FragmentScenario 测试对 setFragmentResult() 和 setFragmentResultListener() 的调用。使用 launchFragmentInContainer 或 launchFragment 为被测 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,父 Fragment 在调用 setFragmentResultListener() 时应使用 getChildFragmentManager() 而不是 getParentFragmentManager()

KotlinJava

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

子 Fragment 可以使用 FragmentManager 将结果发送到其父 Fragment

图 2 子 Fragment 可以使用 FragmentManager 将结果发送到其父 Fragment。

子 Fragment 在其 FragmentManager 上设置结果。然后,一旦父 Fragment 处于 STARTED 状态,它就会收到结果:

KotlinJava

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 管理器上设置结果监听器。

KotlinJava

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
            }
        });
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值