前言
很高兴见到你 👋,我是 Flywith24 。
最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。
Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档的极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文,喜欢一手信息的小伙伴可直奔 官方原文。如果只想关注新文档中的变化,可 点此直达。限于篇幅原因,该文档分上下两部分。
【译】2020 年 Fragment 最新文档(上),该更新知识库啦
【译】2020 年 Fragment 最新文档(下),该更新知识库啦
本文为下半部分,将介绍以下内容:
- Fragment 的状态保存
- Fragment 间通信
- Fragment 于 AppBar 共同使用
- 使用 DialogFragment 显示 Dialog
- Fragment 测试
上半部分介绍:
- Fragment 的创建
- Fragment manager
- Fragment 事务
- Fragment 动画
- Fragment 生命周期
点击查看彩蛋😉
😝 欢迎来到彩蛋部分,您一定是个好奇心很强的小伙伴呢。我是一个「强迫症晚期患者」,为了移动端更好阅读的体验,我经常将代码以图片的形式插入到文内。但随之而来出现一个问题:没办法 copy 代码(这对 cv 开发者很重要的 🤣)。
前些天,我在 github 某个项目的 README 文档中看到一个技巧,便是把较长且有些影响阅读的内容折叠,读者可以自由地选择展开。
这也是这个「彩蛋」的显示方式。后文中关于代码的部分我都会提供图片和可复制的源码两部分,其中后者处于折叠状态。您可以点击 「点击查看代码详情」以展开源码。
彩蛋结束。🥳
状态保存
各种 Android 系统操作可能会影响 fragment 的状态。 为了确保用户状态得到保存,Android 会自动保存并还原 fragment 的返回栈。因此,您需要确保 fragment 中的所有数据也被保存和还原。
下表罗列了导致 fragment 丢失状态的操作,以及各种状态是否被保存。表中提到的状态类型如下:
- Variables:fragment 的本地变量
- View State:fragment 中 一个或多个 view 拥有 的所有数据
- SavedState:该 fragment 实例固有的数据,应保存在
onSaveInstanceState()中 - NonConfig:从外部源(例如服务器或本地存储库)提取的数据,或由用户创建一旦提交就发送到服务器的数据。
通常,Variables 与 SavedState 的处理方式相同,但下表将两者进行了区分,以展示各种操作对它们的影响:
*NonConfig state 在进程死亡时可以使用 Saved State module for ViewModel 保存状态。
让我们看一个具体的例子。我们生成一个随机字符串将其显示在 TextView 中,并提供一个发送给朋友之前编辑该字符串的选项:
用户按下编辑按钮后,将显示一个 EditText 视图,用户可以在其中编辑消息。如果用户点击CANCEL,则应清除 EditText 视图,并将其可见性设置为 View.GONE。为了保持良好的体验,该示例需要管理 4 个数据:
以下各节介绍如何正确管理数据状态。
View State
View 负责管理自己的状态。例如,当 view 接受用户输入时,view 负责保存该输入以确保配置变化时能够恢复状态。所有 Android 官方提供的 view 均重写了 onSaveInstanceState() 和 onRestoreInstanceState() 方法,因此您不必管理 fragment 中的 View State。
🌟 注意:为了确保配置更改是能够正确处理状态,您的自定义 View 应该重写
onSaveInstanceState()和onRestoreInstanceState()方法
例如,在前面的场景中,已编辑的字符串保存在 EditText 中。EditText 知道其显示的文本的值以及其它详细信息(如选定文本的开头和结尾)。
View 需要一个 ID 来恢复状态。这个 ID 必须在其所在的 fragment 视图树中唯一。没有 ID 的 View 不能恢复状态。
如表 1 所示,除了 fragment 被移除且没加入到返回栈和宿主销毁这两种情况,view 可以保存和恢复其 ViewState。
SavedState
您的 fragment 负责管理少量动态状态,这些动态状态对于 fragment 的功能至关重要。 您可以使用 Fragment.onSaveInstanceState(Bundle) 保存便于序列化的数据。与 Activity.onSaveInstanceState(Bundle) 相似,Bundle 中的数据将在 配置发生变化和系统资源回收 时保存,并且该 Bundle 在 fragment 的 onCreate(Bundle),onCreateView(LayoutInflater, ViewGroup, Bundle) 和 onViewCreated(View, Bundle) 方法中可用。
⚠️ 注意:fragment 的
onSaveInstanceState(Bundle)仅在其宿主 activity 的onSaveInstanceState(Bundle)调用时调用。
Tips:当使用 ViewModel 时,可用直接使用 SavedStateHandle 保存数据。更多的信息请参考:Saved State module for ViewModel(译者注:也可参考译者文章 绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析)。
继续前面的示例,randomGoodDeed 是显示给用户的数据,isEditing 是确定 fragment 显示或隐藏 EditText 的标志。这种 save state 应使用 onSaveInstanceState(Bundle) 保存,如下所示:
点击查看代码详情
// 👇 Kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(IS_EDITING_KEY, isEditing)
outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed)
}
// 👇 Java
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(IS_EDITING_KEY, isEditing);
outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed);
}
要在 onCreate(Bundle) 中恢复状态,可用从 Bundle 中取值:
点击查看代码详情
// 👇 Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isEditing = savedInstanceState?.getBoolean(IS_EDITING_KEY, false)
randomGoodDeed = savedInstanceState?.getString(RANDOM_GOOD_DEED_KEY)
?: viewModel.generateRandomGoodDeed()
}
// 👇 Java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
isEditing = savedInstanceState.getBoolean(IS_EDITING_KEY, false);
randomGoodDeed = savedInstanceState.getString(RANDOM_GOOD_DEED_KEY);
} else {
randomGoodDeed = viewModel.generateRandomGoodDeed();
}
}
如表 1 所示,请注意,当 fragment 被加入到返回栈时 Variables 会被保存,将 Variables 看作 成 SavedState 来处理可以确保在所有场景下都能保存这些变量。
NonConfig
NonConfig 数据应放在 fragment 之外,例如在 ViewModel 中。在上面的示例中,seed(NonConfig sate)在 ViewModel 中生成,由 ViewModel 负责保存其状态。
点击查看代码详情
// 👇 Kotlin
public class RandomGoodDeedViewModel : ViewModel() {
private val seed = ... // 生成 seed(种子)
private fun generateRandomGoodDeed(): String {
val goodDeed = ... // 使用 seed 生成 goodDeed
return goodDeed
}
}
// 👇 Java
public class RandomGoodDeedViewModel extends ViewModel {
private Long seed = ... // 生成 seed(种子)
private String generateRandomGoodDeed() {
String goodDeed = ... // 使用 seed 生成 goodDeed
return goodDeed;
}
}
ViewModel 类本质上允许数据在配置发生变化(例如屏幕旋转)中幸存下来,并且在将 fragment 放回返回栈中时仍保留在内存中。在系统资源回收(进程死亡并重新创建)之后,将重新创建 ViewModel,并生成一个新种子。 在 ViewModel 中添加 SavedState 模块可以使 ViewModel 在系统资源回收的场景下保留其内部数据。
额外资源
通信
为了复用 fragment,需要将每个 fragment 构建为完全独立的组件并定义自己的布局和行为。定义可复用的 fragment 并将它们与 activity 关联便可为 app 建立复合型 UI。
为了正确响应用户事件或共享状态信息,开发者通常需要在 activity 和它的 fragment 之间或两个到多个 fragment 之间建立通信。为了保证 fragment 的独立性,您 不应 让 fragment 与其它 fragment 或其宿主直接通信。
Fragment 库提供了两个通信选项:共享 ViewModel 和 Fragment Result API。如何选择应视场景而定: 要与任何自定义 API 共享持久数据,应使用 ViewModel。对于可以放入 Bundle 的一次性的结果类数据,应使用 Fragment Result API。
下文介绍如何使用 ViewModel 和 Fragment Result API 在 fragment 和 activity 之间通信。
使用 ViewModel 共享数据
ViewModel 是多个 fragment 或 fragment 与其宿主之间共享数据的理想选择。ViewModel 对象存储并管理 UI 数据。关于 ViewModel 的更多信息,请参考 ViewModel overview(译者注:也可参考译者文章 即使您不使用 MVVM 也要了解 ViewModel)。
与宿主 activity 共享数据
在某些场景下,您可能需要在 fragment 及其宿主 activity 之间共享数据。例如,您可能在 fragment 中操作全局的 UI 组件。
看看下面的 ItemViewViewModel:
点击查看代码详情
// 👇 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 overview(译者注:也可参考译者文章 ViewModel 的左膀右臂 数据驱动真的香)。
通过将 activity 传递给 ViewModelProvider 构造器,您的 fragment 及其宿主 activity 都可以获取 activity 范围内共享的 ViewModel 实例,ViewModelProvider 负责实例化 ViewModel 或获取它(如果已经存在)。activity 和 fragment 都可以观察和修改该数据:
点击查看代码详情
// 👇 Kotlin
class MainActivity : AppCompatActivity() {
// activity-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ItemViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.selectedItem.observe(this) { item ->
// 根据最新的数据执行操作
})
}
}
class ListFragment : Fragment() {
// fragment-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ItemViewModel by activityViewModels()
// 当 item 点击时调用
fun onItemClicked(item: Item) {
// 设置新的 item
viewModel.selectItem(item)
}
}
点击查看代码详情
// 👇 Java
public class MainActivity extends AppCompatActivity {
private ItemViewModel viewModel;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 👇 传入 activity,得到 activity 范围共享的 ViewMoel
viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
viewModel.getSelectedItem().observe(this, item -> {
// 根据最新的数据执行操作
});
}
}
public class ListFragment extends Fragment {
private ItemViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 👇 传入 activity,得到 activity 范围共享的 ViewMoel
viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class);
...
items.setOnClickListener(item -> {
// 设置新的 item
viewModel.select(item);
});
}
}
⚠️ 警告:请确保在
ViewModelProvider中 使用合适的作用域。在上面的示例中,MainActivity是MainActivity和ListFragment的作用域,因此它们能够获得相同的ViewModel对象。如果ListFragment改用自身的作用域,则将获得与MainActivity不同的ViewModel对象。
在 fragment 之间共享数据
同一 activity 中的两个或多个 fragment 通常需要相互通信。例如,一个 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() {
// fragment-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// 👇 注意这里的 lifecycleOwner 是 view 的
viewModel.filteredList.observe(viewLifecycleOwner) { list ->
// 更新 list UI
}
}
}
class FilterFragment : Fragment() {
// fragment-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// 👇 注意这里的 lifecycleOwner 是 view 的
viewModel.filters.observe(viewLifecycleOwner) { set ->
// 根据选中的过滤条件更新 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 -> {
// 更新 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 -> {
// 根据选中的过滤条件更新 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 的作用域。
点击查看代码详情
// 👇 Kotlin
class ListFragment: Fragment() {
// 使用 fragment-ktx 提供的属性代理获取 ViewModel(作用域为当前 fragment)
private val viewModel: ListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner) { list ->
// 更新 list UI
}
}
}
class ChildFragment: Fragment() {
// 使用 fragment-ktx 提供的属性代理获取 ViewModel(作用域为父 fragment)
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) {
// 👇 作用域为当前 fragment
viewModel = new ViewModelProvider(this).get(ListViewModel.class);
viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
// 更新 list UI
}
}
}
public class ChildFragment extends Fragment {
private ListViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
// 👇 作用域为父 fragment
viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class);
...
}
}
Navigation Graph 范围内共享 ViewModel
如果您正在使用 Navigation library,还可以将 ViewModel 的作用域限定为目的地的 NavBackStackEntry 的生命周期。例如,可以将 ViewModel 的作用域限定为 ListFragment 的 NavBackStackEntry:
点击查看代码详情
// 👇 Kotlin
class ListFragment: Fragment() {
// 使用 fragment-ktx 提供的 NavBackStackEntry 范围内共享的 ViewMoel
private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner) { item ->
// 更新 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 -> {
// 更新 list UI
}
}
}
有关将 ViewModel 作用域限定在 NavBackStackEntry 的更多信息,请参考 Interact programmatically with the Navigation component(译者注:也可以参考译者文章 想去哪就去哪,Android 世界的指南针)。
使用 Fragment Result API 获得结果
在某些情况下,您可能希望在两个 fragment 之间或 fragment 与其宿主 activity 之间传递一次性值。例如,您可能有一个读取二维码的 fragment,将数据传递回前一个 fragment。从 Fragment 1.3.0-alpha04 开始,每个 FragmentManager 都实现 FragmentResultOwner。这意味着 FragmentManager 可以充当 fragment 结果的中央存储。此更改允许组件通过设置 fragment 结果并监听那些结果进而彼此通信,而无需那些组件彼此直接引用(译者注:Fragment Result API 引入的原因以及源码分析可参考 1.3.0-alpha04 来袭,Fragment 间通信的新姿势)。
在 fragment 之间传递结果
要将数据从 fragment B 传递回 fragment A,请首先在 fragment A 上设置一个结果 listener,该 fragment 将接收结果。在 fragment A 的 FragmentManager 上调用 setFragmentResultListener() ,如下所示:
点击查看代码详情
// 👇 Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 fragment-ktx 提供的扩展函数
setFragmentResultListener("requestKey") { requestKey, bundle ->
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
val result = bundle.getString("bundleKey")
// 根据结果执行后续逻辑
}
}
// 👇 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) {
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
String result = bundle.getString("bundleKey");
// 根据结果执行后续逻辑
}
});
}
在 fragment B(产生结果的 fragment)中,必须使用相同的 requestKey 在相同的 FragmentManager 上设置结果。您可以使用 setFragmentResult() API 来做到这一点:
点击查看代码详情
// 👇 Kotlin
button.setOnClickListener {
val result = "result"
// 使用 fragment-ktx 提供的扩展函数
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 后执行 listener 回调。
您只能有一个 listener 和给定 key 的结果。如果为同一 key 多次调用 setFragmentResult(),并且 listener 未启动,则系统会将所有待处理的结果替换为更新的结果。如果设置的结果没有相应的 listener 接收,则结果将存储在 FragmentManager 中,直到您使用相同的 key 设置 listener 为止。listener 收到结果并触发 onFragmentResult() 回调后,该结果将被清除。此行为有两个主要含义:
- 返回栈上的 fragment 只有弹出并处于
STARTED才能接收结果 - 当 fragment 正在监听一个
STARTED状态的结果,当结果被设置则立即触发 listener 的回调
🌟 注意:由于 fragment 结果存储在
FragmentManager层级上,因此必须将 fragment attach 到父FragmentManager来调用setFragmentResultListener()或setFragmentResult()。
测试 fragment 结果
使用 FragmentScenario 测试 setFragmentResult() 和 setFragmentResultListener() 的调用。使用 launchFragmentInContainer 或 launchFragment 为被测 fragment 创建一个场景,然后手动调用待测试的方法。
要测试 setFragmentResultListener() ,请创建带有 fragment 的场景,该 fragment 将调用 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)
// 使用 fragment-ktx 提供但扩展函数
setFragmentResultListener("requestKey") { requestKey, bundle ->
result = bundle.getString("bundleKey")
}
}
}
在父 fragment 和子 fragment 间传递结果
要将结果从子 fragment 传递给父 fragment,在调用 setFragmentResultListener() 时,父 fragment 应使用 getChildFragmentManager() 而不是 getParentFragmentManager()。
点击查看代码详情
// 👇 Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在 child fragmentManager 设置 listener
childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
val result = bundle.getString("bundleKey")
// 处理结果
}
}
// 👇 Java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在 child fragmentManager 设置 listener
getChildFragmentManager()
.setFragmentResultListener("requestKey", this, new FragmentResultListener() {
@Override
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
String result = bundle.getString("bundleKey");
// 处理结果
}
});
}
接收宿主 activity 的结果
要在宿主 activity 中接收 fragment 结果,请使用 getSupportFragmentManager() 在 FragmentManager 上设置结果 listener。
点击查看代码详情
// 👇 Kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.setFragmentResultListener("requestKey", this) { requestKey, bundle ->
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
val result = bundle.getString("bundleKey")
// 处理结果
}
}
}
// 👇 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) {
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
String result = bundle.getString("bundleKey");
// 处理结果
}
});
}
}
与 AppBar 共同使用
顶部 app bar 在 app 窗口顶部提供了统一的界面,用于显示当前屏幕上的信息和操作。
使用 fragment 时,app bar 可以作为宿主 activity 的 ActionBar 或 fragment 布局中的 toolbar。app bar 的所属权取决于您的应用需求。
如果所有屏幕都使用始终位于顶部并填满屏幕宽度的同一 app bar,则应使用由该 activity 托管的主题提供的 action bar。使用主题 app bar 有助于保持一致的外观,并提供了一个存放选项菜单和返回按钮的地方。
如果要在多个屏幕上对 ap bar 的大小,位置和动画进行更多控制,请使用由 fragment 托管的 toolbar。例如,您可能需要折叠的 app bar 或宽度为屏幕一半且垂直居中的 app bar。
了解不同的方式并采用正确的方法可以节省您的时间,并助于确保您的 app 正常运行。对于加载菜单和响应用户交互的操作要根据不同场景使用不同的方法处理。
下面的示例包含可编辑配置文件的 ExampleFragment。该 fragment 在其 app bar 中加载了以下 XML-defined menu:
点击查看代码详情
<!-- sample_menu.xml -->
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_done"
android:icon="@drawable/ic_done"
android:title="@string/done"
app:showAsAction="ifRoom|withText"/>
</menu>
该菜单包含两个选项:一个用于导航到配置文件界面,另一个用于保存对配置文件所做的所有更改。
Activity 拥有的 app bar
app bar 通常由宿主 activity 持有。当 activity 持有 app bar 时,fragment 可以通过重写在 fragment 创建期间调用的 framework 方法来与 app bar 进行交互。
🌟 注意:本节内容仅在 activity 持有 app bar 时才适用。 如果您的 app bar 是 fragment 布局中包含的 toolbar,请参见 Fragment 拥有的 app bar 一节。
注册 activity
您必须通知系统您的 app bar fragment 正在参与选项菜单的加载。为此,请在 fragment 的 onCreate(Bundle) 方法中调用 setHasOptionsMenu(true),如下所示:
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
}
setHasOptionsMenu(true) 告诉系统您的 fragment 想接收菜单相关的回调。当发生与菜单相关的事件(创建,点击等)时,首先在 activity 上调用事件处理方法,然后再在 fragment 上调用该事件处理方法。请注意,您的应用程序逻辑不应依赖于此顺序。如果同一 activity 托管多个 fragment,则每个 fragment 都可以提供菜单选项。在这种情况下,回调顺序取决于 fragment 的添加顺序。
加载 menu
要将菜单合并到 app bar 的选项菜单中,请在 fragment 中重写 onCreateOptionsMenu()。此方法接收当前 app bar 菜单和 MenuInflater 作为参数。使用 menu inflater 创建 fragment 菜单的实例,然后将其合并到当前菜单中,如下所示:
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.sample_menu, menu)
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.sample_menu, menu);
}
}
处理点击事件
参与选项菜单的每个 activity 和 fragment都能够响应触摸事件。Fragment的 onOptionsItemSelected() 接收选定的菜单 item 作为参数,并返回一个布尔值以指示是否已消费了触摸。一旦 activity 或 fragment 从 onOptionsItemSelected() 返回 true,其它任何参与的 fragment 将不会收到回调。
在 onOptionsItemSelected() 的实现中,在菜单 item 的 itemId 上使用 switch 语句(Kotlin 使用 when 关键字)。如果所选 item 属于您,则处理触摸并返回 true 表示已处理 click 事件。 如果所选项目不是您的,请调用 super 方法。默认情况下,super 方法返回 false 以允许菜单被后续处理。
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
// 跳转到设置界面
true
}
R.id.action_done -> {
// 保存配置文件更改
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings: {
// 跳转到设置界面
return true;
}
case R.id.action_done: {
// 保存配置文件更改
return true;
}
default:
return super.onOptionsItemSelected(item);
}
}
}
🌟 注意:fragment 只能处理通过
onCreateOptionsMenu()调用添加的菜单 item 。使用 activity 拥有的 app bar 时,activity 应处理返回上一级按钮和未被 fragment 添加的菜单 item 的点击事件。
动态修改菜单
隐藏/显示按钮或更改图标的逻辑应放在 onPrepareOptionsMenu() 中。在显示菜单的每个实例之前立即调用此方法。
继续前面的示例,在用户开始编辑之前,保存按钮应该是不可见的,并且在用户保存后应该消失。将此逻辑添加到 onPrepareOptionsMenu() 可以确保始终正确显示菜单:
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onPrepareOptionsMenu(menu: Menu){
super.onPrepareOptionsMenu(menu)
val item = menu.findItem(R.id.action_done)
item.isVisible = isEditing
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem item = menu.findItem(R.id.action_done);
item.setVisible(isEditing);
}
}
当您需要更新菜单时(例如,当用户按下编辑按钮以编辑配置文件信息时),您必须在宿主 activity 上调用 invalidateOptionsMenu() 以请求系统调用 onCreateOptionsMenu()。无效时,您可以在 onCreateOptionsMenu() 中进行更新。菜单加载后,系统将调用 onPrepareOptionsMenu() 并更新菜单以响应 fragment 的当前状态。
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
fun updateOptionsMenu() {
isEditing = !isEditing
requireActivity().invalidateOptionsMenu()
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
public void updateOptionsMenu() {
isEditing = !isEditing;
requireActivity().invalidateOptionsMenu();
}
}
Fragment 拥有的 app bar
如果您的 app 中的大多数屏幕都不需要应用 app bar,或者一个屏幕可能需要一个截然不同的 app bar,则可以在 fragment 布局中添加 Toolbar。尽管您可以在 fragment 的视图树中的任何位置添加 Toolbar,但通常应将其放置在屏幕顶部。要在片段中使用 Toolbar,请提供一个 ID 并在 fragment 中获得对其的引用,就像在其他任何视图中一样。
使用 fragment 拥有的 app bar 时,强烈建议直接使用 Toolbar API。不要使用 setSupportActionBar() 和 Fragment menu API,它们仅适用于 activity 拥有的 app bar。
加载 menu
Toolbar 有一个便捷方法 inflateMenu(int),它需要菜单资源的 ID 作为参数。要将 XML 菜单资源加载到 toolbar 中,请将 resId 传递给此方法,如下所示:
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewBinding.myToolbar.inflateMenu(R.menu.sample_menu)
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.inflateMenu(R.menu.sample_menu);
}
}
要加载另一个 XML 菜单资源,请使用新菜单的 resId 再次调用该方法。新菜单 item 将添加到菜单,并且现有菜单 item 不会被修改或删除。
如果要替换现有菜单集,请在使用新菜单 ID 调用 inflateMenu(int) 之前清除菜单。
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
fun clearToolbarMenu() {
viewBinding.myToolbar.menu.clear()
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
public void clearToolbarMenu() {
viewBinding.myToolbar.getMenu().clear()
}
}
处理点击事件
您可以使用 setOnMenuItemClickListener() 方法将 OnMenuItemClickListener 直接传递到 toolbar。每当用户从 toolbar 操作菜单 item 时,就会调用此 listener。选定的 MenuItem 传递给 listener 的 onMenuItemClick() 方法,并消费这个事件,如下所示:
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewBinding.myToolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_settings -> {
// 跳转设置界面
true
}
R.id.action_done -> {
// 保存配置更改
true
}
else -> false
}
}
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) {
case R.id.action_settings:
// 跳转设置界面
return true;
case R.id.action_done:
// 保存配置更改
return true;
default:
return false;
}
});
}
}
动态修改菜单
当 fragment 拥有 app bar 时,您可以在运行时像操作其他 view 一样修改 Toolbar。
继续前面的示例,在用户开始编辑之前,保存菜单 item 应该是不可见的,并且在点击保存后应再次消失:
点击查看代码详情
/ 👇 Kotlin
class ExampleFragment : Fragment() {
...
fun updateToolbar() {
isEditing = !isEditing
val saveItem = viewBinding.myToolbar.menu.findItem(R.id.action_done)
saveItem.isVisible = isEditing
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
public void updateToolbar() {
isEditing = !isEditing;
MenuItem saveItem = viewBinding.myToolbar.getMenu().findItem(R.id.action_done);
saveItem.setVisible(isEditing);
}
}
添加导航图标
如果存在,导航按钮将出现在工具栏的起始位置,在 toolbar 上设置导航图标并使其可见。您还可以设置 navigation 特有的 onClickListener(),只要用户点击导航按钮,就会调用该 onClickListener,如下所示:
点击查看代码详情
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
myToolbar.setNavigationIcon(R.drawable.ic_back)
myToolbar.setNavigationOnClickListener { view ->
// 跳转到某个地方
}
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.setNavigationIcon(R.drawable.ic_back);
viewBinding.myToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 跳转到某个地方
}
});
}
}
🌟 注意:使用
ToolbarAPI 处理导航图标时,不会触发默认 activity 的行为。您可以使用requireActivity().onSupportNavigateUp()触发返回到 manifest 中定义的 父 activity 的行为。
使用 DialogFragment 显示 Dialog
DialogFragment 是专门用于创建和托管 dialog 的特殊 fragment 子类。严格来说,您不需要在 fragment 中托管 dialog,但是这样做可以使 FragmentManager 管理 dialog 的状态并在配置发生变化时自动还原 dialog 。
🌟 注意:本节假定您熟悉创建 dialog 。有关更多信息,请参见 dialog 指南。
创建 DialogFragment
要创建 DialogFragment,请首先创建一个继承 DialogFragment 的类,并重写 onCreateDialog(),如下所示:
点击查看代码详情
// 👇 Kotlin
class PurchaseConfirmationDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.order_confirmation))
.setPositiveButton(getString(R.string.ok)) { _,_ -> }
.create()
companion object {
const val TAG = "PurchaseConfirmationDialog"
}
}
// 👇 Java
public class PurchaseConfirmationDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.order_confirmation))
.setPositiveButton(getString(R.string.ok), (dialog, which) -> {} )
.create();
}
public static String TAG = "PurchaseConfirmationDialog";
}
与 onCreateView() 在普通 fragment 中创建根视图的方式类似,onCreateDialog() 应该创建一个 Dialog 来显示为 DialogFragment 的一部分。DialogFragment 可以在 fragment 的生命周期中的适当状态下显示 Dialog。
🌟 注意:
DialogFragment拥有Dialog.setOnCancelListener()和Dialog.setOnDismissListener()回调。您不能自己设置它们。要了解有关这些事件的信息,请重写onCancel()和onDismiss()。
就像 onCreateView() 一样,您可以从 onCreateDialog() 返回 Dialog 的任何子类,而不仅限于使用 AlertDialog。
显示 DialogFragment
无需手动创建 FragmentTransaction 即可显示 DialogFragment,使用 show() 方法显示 dialog 。您可以向该方法传递一个 FragmentManager 对象和 FragmentTransaction 的 tag(String 类型)。从 Fragment 中创建 DialogFragment 时,必须使用 Fragment 的子 FragmentManager 来确保在配置发生变化后正确恢复状态。非空标记允许您在以后使用 findFragmentByTag() 来获取 DialogFragment。
为了更好地控制 FragmentTransaction,可以使用 show() 的重载方法传入一个 FragmentTransaction。
🌟 注意:因为
DialogFragment是在配置发生变化后自动恢复的,所以请考虑仅根据用户操作或findFragmentByTag()返回 null(代表 dialog 不存在)时才调用show()。
DialogFragment 生命周期
DialogFragment 遵循标准的 fragment 生命周期。此外,DialogFragment 还有一些其它的生命周期回调。常见的如下:
onCreateDialog()- 重写此回调,为 fragment 提供一个管理和显示的 dialogonDismiss()- 如果在关闭 Dialog 时需要执行自定义逻辑(例如释放资源,取消订阅可观察的资源等),请重写此回调onCancel()- 如果在取消 Dialog 时需要执行自定义逻辑,则重写该方法
DialogFragment 还包含用于关闭或设置 DialogFragment 可取消的方法:
dismiss()- 关闭 fragment 及其 dialog 。如果该 fragment 加入到了返回栈,则弹出该 fragment 及其顶部的所有 entry。否则,将提交一个新的事务 remove 该 fragment。setCancellable()- 控制当前显示的 dialog 是否可以取消。应该使用DialogFragment的该方法而不是直接调用Dialog.setCancelable(boolean)。
请注意,在将 DialogFragment 与 Dialog 一起使用时,您不要重写 onCreateView() 或 onViewCreated()。dialog 不仅是 view,它还具有自己的 Windiow。因此,重写 onCreateView()是不行的。此外,除非您已重写 onCreateView() 并提供了非 null 的 view,否则永远不会在自定义 DialogFragment 上调用 onViewCreated()。
🌟 注意:订阅支持生命周期的组件(如 LiveData)时,切勿在使用
Dialog的DialogFragment中将viewLifecycleOwner用作LifecycleOwner。相反,请使用DialogFragment本身,或者如果您使用的是 Jetpack Navigation,请使用NavBackStackEntry。
使用自定义 View
您可以通过 重写 onCreateView() 来创建 DialogFragment 并显示 dialog ,可以像使用正常的 fragment 一样为其提供 layoutId,也可以使用 Fragment 1.3.0-alpha02 中引入的 DialogFragment 构造器。
由 onCreateView() 返回的 View 将自动添加到 dialog 中。在大多数情况下,这意味着您不需要重写 onCreateDialog() ,因为默认的空 dialog 是用 传入的 view 填充的。
某些 DialogFragment 的子类,例如 BottomSheetDialogFragment,会将您的 view 嵌入到一个样式为底部弹窗的 dialog 中。
测试
本节内容介绍如何使用框架提供的 API 测试 fragment 的行为。
Fragment 作为 app 中的可复用的容器,使您可以在各种 activity 和布局配置中呈现相同的 UI 界面。考虑到 fragment 的通用性,重要的是要验证它们是否提供了一致且资源高效的体验。请注意以下几点:
- 你的 fragment 不应依赖特定的父 activity 或 fragment
- 除非 fragment 对用户可见,否则不应创建 fragment 视图树
为了提供这些测试条件,AndroidX fragment-testing 库提供了 FragmentScenario 创建 fragment 并 改变它们的 Lifecycle.State。
🌟 注意:要成功运行包含
FragmentScenario对象的测试,请在测试的 instrumentation 线程中运行 API 的方法。要了解有关 Android 测试中使用的不同线程的更多信息,请参阅 Understand threads in tests。
声明依赖
要使用 FragmentScenario,请使用 debugImplementation 在 app 的 build.gradle 文件中定义 fragment 测试工件,如下所示:
点击查看代码详情
dependencies {
def fragment_version = "1.2.5"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
}
本节示例使用的断言来自 Espresso 和 Truth。关于测试和断言库的更多信息,请参考 Set up project for AndroidX Test。
创建 Fragment
FragmentScenario 包括以下用于在测试中启动 fragment 的方法:
launchInContainer(),用于测试 fragment 的 UI。FragmentScenario将 fragment attach 到 activity root view 容器内,该 activity 除此之外啥都没有。launch(),用于测试没有 UI 的 fragment。FragmentScenario将 这种类型的 fragment attach 到一个没有 root view 的空 activity 中。
启动其中一种 fragment 时,FragmentScenario 将被 fragment 驱动为 RESUMED 状态。此状态代表该 fragment 正在运行并且对用户可见。您可以使用 Espresso UI tests 测试相关 UI 元素的信息。
以下代码示例演示如何使用每种方法启动 fragment:
🌟 注意:您的 fragment 可能要求测试 activity 不使用默认的主题。您可以提供自己的主题作为
launch()和launchInContainer()的参数。
launchInContainer() 示例
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
// fragmentArgs 是可选的.
val fragmentArgs = bundleOf(“selectedListItem” to 0)
val scenario = launchFragmentInContainer<EventFragment>(fragmentArgs)
...
}
}
launch() 示例
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
// fragmentArgs 是可选的.
val fragmentArgs = bundleOf("numElements" to 0)
val scenario = launchFragment<EventFragment>(fragmentArgs)
...
}
}
提供依赖
如果您的 fragment 具有依赖项,则可以通过向 launchInContainer() 或 launch() 方法提供自定义 FragmentFactory 来提供这些依赖项的测试版本:
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val someDependency = TestDependency()
launchFragmentInContainer {
EventFragment(someDependency)
}
...
}
}
有关使用 FragmentFactory 为 Fragment 提供依赖项,请参考 FragmentManager 一节。
将 fragment 驱动到新状态
在 app 的 UI 测试中,通常需要 fragment 处于 RESUMED 状态时开始测试。但是,在更细粒度的单元测试中,当 fragment 从一种生命周期状态转换为另一种生命周期状态时,您可能也需要测试其行为。
要将 fragment 驱动到不同的生命周期状态,请调用 moveToState()。 此方法支持以下状态作为参数:CREATED,STARTED,RESUMED 和 DESTROYED。 该方法模拟了该 fragment 或其宿主 activity 由于一些原因驱动 fragment 状态更改的场景。
🌟 注意:如果将 fragment 切换为
DESTROYED状态,则无法将该 fragment 驱动为另一状态,也不能将该 fragment attach 到其它 activity。
以下示例将测试 fragment 移至 CREATED 状态:
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
scenario.moveToState(Lifecycle.State.CREATED)
// EventFragment moves from RESUMED -> STARTED -> CREATED
...
}
}
⚠️ 警告:如果您尝试将被 fragment 段转换为当前状态,则
FragmentScenario将忽略该请求而不会引发异常。特别是,API 允许您连续多次将 fragment 转换为 DESTROYED 状态。
重新创建 Fragment
如果您的 app 在资源不足的设备上运行,则系统可能会销毁包含您的 fragment 的 activity。这种情况要求您的 app 在用户返回 fragment 时重新创建该 fragment。为了模拟这种情况,请调用 recreate():
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
scenario.recreate()
...
}
}
FragmentScenario.recreate() 销毁 fragment 及其宿主activity,然后重新创建它们。当 FragmentScenario 类重新创建被测试的 fragment 时,该 fragment 将返回其在销毁之前所处的生命周期状态。
与 fragment UI 交互
要在被测 fragment 中触发 UI 操作,请使用 Espresso view matchers 与视图中的元素进行交互:
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
onView(withId(R.id.refresh)).perform(click())
// 断言预期的行为
...
}
}
如果需要调用 fragment 自身的方法,例如响应选项菜单中的选择,则可以使用 FragmentScenario.onFragment() 获取对 fragment 的引用并传递 FragmentAction 来安全地进行操作:
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
scenario.onFragment { fragment ->
fragment.myInstanceMethod()
}
}
}
🌟 注意:不要保留对传递给
onFragment()的 fragment 的引用。这些引用消耗系统资源,并且引用本身可能已过时,因为框架可以重新创建 fragment。
测试 dialog fragment
FragmentScenario 还支持测试 dialog fragment。尽管 dialog fragment 具有 UI 元素,但它们的布局填充在单独的 Window 中,而不是 activity 本身。因此,请使用 FragmentScenario.launch() 测试 dialog fragment。
下面的示例测试 dialog 的关闭过程:
点击查看代码详情
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testDismissDialogFragment() {
// 假设 MyDialogFragment 继承了 DialogFragment
with(launchFragment<MyDialogFragment>()) {
onFragment { fragment ->
assertThat(fragment.dialog).isNotNull()
assertThat(fragment.requireDialog().isShowing).isTrue()
fragment.dismiss()
fragment.parentFragmentManager.executePendingTransactions()
assertThat(fragment.dialog).isNull()
}
}
// 假设 dialog 存在一个按钮,text 为 "Cancel"
onView(withText("Cancel")).check(doesNotExist())
}
}
译者补充
新版文档的变化
- 在 xml 使用
FragmentContainerView作为 fragment 的容器,不要使用<fragment>标签或 FrameLayout - 建议使用 Navigation library 管理 app 内导航
- activity 中使用
getSupportFragmentManager()获取FragmentManager - fragment 中使用
getChildFragmentManager()获取管理子 fragment 的FragmentManager - fragment 使用
getParentFragmentManager()获取其宿主的FragmentManager - commit 事务时建议调用
setReorderingAllowed(true) - Fragment 有一个带有
layoutId参数的构造器,无需调用onCreateView设置布局 - FragmentFactory 默认使用无参构造器创建 fragment,如果自定义了 fragment 构造器,为了保证重建时的一致性,需要自定义 FragmentFactory
- 使用
setMaxLifecycle()限制了 Fragment 的最大生命周期,因此 setUserVisibleHint 被弃用了,保证了 ViewPager 中 fragment 可见性判断与正常情况一致 - 对于涉及多种动画效果的场景时建议使用 transition,嵌套
AnimationSet存在已知问题。 - 理解 Fragment Lifecycle,其 View Lifecycle 以及相应回调方法的关系
- 理解 observe LiveData 时
lifecycleOwner和viewLifecycleOwner的区别 - 使用 ViewModel 配合 savestate 保存/恢复状态
- 使用最新的 Fragment 通信机制:共享
ViewModel和Fragment Result API - 使用
DialogFragment来显示 Dialog,能够更好的处理配置发生变化和系统资源回收的场景 fragment-ktx库有很多方便的扩展函数和属性代理fragment library中包含了activity library
Fragment 相关文章
关于我
我是 Flywith24,Android App/Rom 层开发者。目前专注于 Android 体系化文章的写作。
Android Detail 专栏 正在更新中,想要建立系统化知识体系的小伙伴可以去看看哦。我的所有博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉