Fragment 这些 API 已废弃,你还在使用吗?

阅读 107

2022-08-16

引言

Fragment 诞生之初就被定义为一个小型 Activity,因此它代理了 Activity 的许多能力(例如 startActivityForResult 等),职责不够单一。随着 Jetpack 各种新组件的出现,Fragment 的很多职责被有效地进行了分担,其本身也可以更更好地聚焦在对 UI 的划分而管理上面,以前的一些 API 也可以退出历史舞台了。本文就盘点一下 Fragment 那些被废弃的 API。

本文的介绍基于 Fragment 版本 1.4.0

1. instantiate

Fragment 这些 API 已废弃,你还在使用吗?_ide

以前, Fragment 的构造函数不允许携带参数,因为某些场景中 Fragment 会由系统自动创建,例如基于 XML 创建 Fragment、Activity 被杀死后的恢复重建等等。此时,系统通过调用 ​​instantiate​​ 来创建 Fragment,instantiate 通过反射调用 Fragment 无参的构造函数。

现在 Fragment 的构造函数允许携带参数了,我们可以通过自定义 ​​FragmentFactory​​,调用 Fragment 的任意构造函数,而系统通过调用 FragmentFactory 来创建 Fragment。

我们可以自定义 FragmentFactory,并重写它的 instantiate 方法来创建 Fragment:

class MyFragmentFactory(private val arg: Any) : FragmentFactory() {

override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
val clazz = loadFragmentClass(classLoader, className)
if (clazz == MyFragment::class.java) {
return MyFragment(arg)
}
return super.instantiate(classLoader, className)
}

}

我们将 FragmentFactory 设置给 FragmentManger,之后系统就可以在各种场景中使用工厂创建 Fragment 了。

//Activity
override fun onCreate(savedInstanceState: Bundle?) {

supportFragmentManager.fragmentFactory = myFragmentFactory
super.onCreate(savedInstanceState)

}

注意 FragmentFactory 的设置必须在 super.onCreate 之前,因为当 Activity 进入重建路径时,会在 super.onCreate 中使用到它。

关于 FragmentFactory 的更多介绍:​​juejin.cn/post/698904…​​

2. onActivityCreated

Fragment 这些 API 已废弃,你还在使用吗?_客户端_02

Fragment 早期设计中与 Activity 耦合较多,例如在生命周期方面上除了代理了 Activity 标准生命周期回调以外,还增加了 ​​onActivityCreated​​​ 用来观察与 Activity 的绑定关系,onActivityCreated 被认为是 ​​onStart​​ 之前最后一个阶段,此时 Fragment 的 View Hierarchy 已经与 Activity 绑定,因此常用来在这里完成一些基于 View 的初始化工作。

现在,官方正在逐渐去掉 Fragment 与 Activity 之间的耦合,一个更加独立的 Fragment 更利于复用和测试,因此 ​​onActivityCreated​​​ 被废除,取而代之的是在 ​​onViewCreated​​​ 中处理与 View 相关的初始化逻辑,与 View 无关的初始化可以前置到 ​​onCreate​​。但要注意 onViewCreated 回调的时间点,Fragment 的 View 还没加入 Activity View 的 Hierarchy。

如果我们实在需要获得 Activity 的 onCreate 事件通知,可以通过在 ​​onAttach(Context)​​​ 中通过 ​​LifecycleObserver​​ 来获取

override fun onAttach(context: Context) {
super.onAttach(context)
requireActivity().lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
//...

onAttach(Context) 是 API23 之后新增的 API,前身是 ​​onAttach(Activity)​​,它也是为了去掉与 Activity 的耦合而被废弃和取代。

关于 Fragment 生命周期的更多内容:​​juejin.cn/post/700697…​​

3. setRetainInstance

Fragment 这些 API 已废弃,你还在使用吗?_ide_03

当系统发生横竖屏旋转等 ConfigurationChanged 时,伴随 Activity 的重新 onCreate,Fragment 也会重新创建。​​setRetainInstance(true)​​ 可以保持 ConfigurationChanged 之后的 Fragment 实例不变。因为有这个特性,以前我们经常会借助 setRetainInstance 来保存 Fragment 甚至 Activity 的状态。

但是使用 setRetainInstance 保存状态存在隐患,如果 Fragment 持有了对 Activity View 的引用则会造成泄露或者异常,所以我们仅保存与 View 无关的状态即可,不应该保存整个 Fragment 实例,所以 ​​setRetainInstance/getRetainInstance​​ 被废弃,取而代之的是推荐使用 ViewModel 保存状态。

对于 ViewModel 的基操想必大家都很熟悉就不赘述了。这里只提醒一点,既然 ViewModel 可以在 ConfigurationChanged 之后保持状态,那么 ViewModel 的初始化只需进行一次即可。不少人会像下面这样初始化 ViewModel

class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){

private val viewModel : DetailTaskViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

//订阅 ViewModel
viewMode.uiState.observe(viewLifecycleOwner) {
//update ui
}

//请求数据

在 onViewCreated 中使用 ​​fetchTaskData​​​ 请求数据,当横竖屏旋转造成 Fragment 重建时,虽然我们可以从 ViewModel 中获取最新数据,但是仍然会执行一次多余的 fetchTaskData 。因此更合理的 ViewModel 初始化时机应该是在其内部的 ​​init​​ 中进行,代码如下:

class TasksViewModel: ViewModel() {

private val _tasks = MutableLiveData<List<Task>>()
val tasks: LiveData<List<Task>> = _uiState

init

关于 ViewModel 初始化时机的相关内容,请参考:​​juejin.cn/post/699721…​​

4. setUserVisibleHint

Fragment 这些 API 已废弃,你还在使用吗?_Android_04

Fragment 经常配合 ViewPager 使用以满足多 Tab 页场景的需求。默认情况下屏幕外部的 Fragment 会跟随显示中的 Fragment 一同被加载,这会影响初始页面的显示速度。​​setUserVisibleHint​​​ 是以前我们常用的“懒加载”实现方案:当 ViewPager 中的 Fragment 进/出屏幕时,​​FragmentPagerAdapter​​ 会对其调用 setUserVisibleHint,传入 true/false,通知其是否可见:

@Override
public void setUserVisibleHint(boolean{
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
onVisible(); //自定义回调: 进入屏幕
} else {
onInVisible();//离开屏幕

如上,通过重写 setUserVisibleHint 我们可以在 ​​onVisible/onInVisible​​​ 中获知 Fragment 显示的时机,便于实现懒加载。但是这种做法有缺陷,首先,你需要为 Fragment 增加基类来定义 onVisible/onInvisible,其次,新增的这两个方法跟原生的生命周期回调交织在一起,增加了代码复杂度和出错的概率。幸好现在我们有了新的“懒加载”解决方案: ​​FragmentTransaction#setMaxLifecycle​​​:setMaxLifecycle 可以将屏幕外尚未显示的 Fragment 的最大的生命周期的状态限制在 ​​Started​

Fragment 这些 API 已废弃,你还在使用吗?_bundle_05

当 Fragment 真正进入屏幕后再推进到 ​​Resumed​​​,此时 ​​onResume​​ 才会响应。借助 setMaxLifecycle 我们仅依靠原生回调即可实现懒加载,而且还避免了额外基类的引入。

如果你使用的是 ViewPager2,其对应的 ​​FragmentStateAdapter​​​ 已经默认支持了 setMaxLifecycle 。对于传统的 ViewPager,启动 setMaxLifecycle 的方法也很简单,​​FragmentPagerAdapter​​​ 的构造方法新增了一个 ​​behavior​​​ 参数, 只要在此处传值为 FragmentPagerAdapter.​​BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT​​​ 即可,在 ​​instantiateItem​​ 方法中,会根据 behavior 为创建的 Fragment 设置 setMaxLifecycle。

// FragmentPagerAdpater.java
@Override
public Object instantiateItem(@NonNull ViewGroup container, int{
...
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
// mBehaviour为1的时候走新逻辑if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
// 初始化item时将其生命周期限制为STARTED
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
// 兼容旧版逻辑
fragment.setUserVisibleHint(false);
}
}

return

关于 setMaxLifecycle 的工作原理:​​juejin.cn/post/696163…​​

5. onActivityResult

Fragment 这些 API 已废弃,你还在使用吗?_客户端_06

以前,我们在 Fragment 可以通过 ​​startActivityForResult/onActivityResult​​​ 启动 Activity 并获取返回的结果,这本质是调用了 Activity 的同名方法。随着 ​​Activity Result API​​ 的启用,startActivityForResult/onActivityResult 已经在 Activity 以及 Fragment 中被废弃。相对于 onActivityResult 的结果返回方式,Activity Result API 避免了对 requestCode 的依赖,以更加直观的方式获得 Activity 返回结果。

基本使用步骤如下图:

Fragment 这些 API 已废弃,你还在使用吗?_Android Jetpack_07

首先,我们创建一个 ​​ActivityResultContract​​​,这里定义了跨 Activity 通信的输入输出协议,系统预置了一系列 ​​ActivityResultContracts.XXXX​​​ 可直接使用。然后,我们使用 ​​registerForActivityResult​​ 注册我们的 Contract 和对应的 Callback,Callback 中我们可以获取 Activity 的返回结果。代码如上

val launcher : ActivityResultLauncher =
registerForActivityResult(
//使用预置的 Contract:StartActivityForResult
ActivityResultContracts.StartActivityForResult()) {
activityResult ->
// 获取 Activity 返回的 ActivityResult
Log.d("TargetActivity", activityResult.toString())
// D/TargetActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}

registerForActivityResult 会返回一个 ​​ActivityResultLauncher​​ 句柄,我们使用它启动 Activity,如下:

val intent = Intent(this, TargetActivity::class.java)
launcher.launch(intent)

最后我们在目标 Activity 中调用 ​​setResult​​ 返回结果即可:

//TargetActivity.kt
class TargetActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(Activity.RESULT_OK, Intent().putExtra("my-data", "data"))
finish()
}
}

关于 Activity Result API 的更多底层原理可以参考

​​juejin.cn/post/692286…​​

6. requestPermissions

Fragment 这些 API 已废弃,你还在使用吗?_bundle_08

​requestPermissions/onRequestPermissionsResult​​ 底层也是基于 startActivityForResult/onActivityResult 实现的,因此同样被废弃了,升级为 Result API 的方式。

ActivityResultContracts 预置了申请权限相关的 Contract:

request_permission.setOnClickListener {
requestPermission.launch(permission.BLUETOOTH)
}

request_multiple_permission.setOnClickListener {
requestMultiplePermissions.launch(
arrayOf(
permission.BLUETOOTH,
permission.NFC,
permission.ACCESS_FINE_LOCATION
)
)
}

// 申请单一权限
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
// Do something if permission grantedif (isGranted) toast("Permission is granted")
else toast("Permission is denied")
}

// 一次申请多权限
private val requestMultiplePermissions =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions : Map<String, Boolean> ->
// Do something if some permissions granted or denied
permissions.entries.forEach {
// Do checking here

7. setTargetFragment

Fragment 这些 API 已废弃,你还在使用吗?_Android_09

​setTargetFragment/getTargetFragment​​ 原本用于 Fragment 之间的通信,例如从 FragmentA 跳转到 FragmentB ,在 B 中发送结果返回给 A:

// 向 FragmentB 设置 targetFragment
FragmentB fragment = new FragmentB();
fragment.setTargetFragment(FragmentA.this, AppConstant.REQ_CODE_SECOND_FRAGMENT);

//切换至 FragmentB
transaction.replace(R.id.fragment_container, fragment).commit();

// FragmentB 中获取 FragmentA 并进行回调

如上,代码非常简单,但是这样的通信无法感应生命周期,即使 FragmentA 处于后台也会在 ​​onActivityResult​​​ 响应回调。目前 TargetFragment 相关 API 已经被废弃,取而代之的是根为合理的 ​​Fragment Result API​​。

假设需要在 FragmentA 监听 FragmentB 返回的数据,首先在 FragmentA 设置监听

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setFragmentResultListener 是 fragment-ktx 提供的扩展函数
setFragmentResultListener("requestKey") { requestKey, bundle ->
// 监听key为“requestKey”的结果, 并通过bundle获取
val result = bundle.getString("bundleKey")
// ...
}
}

// setFragmentResultListener 是Fragment的扩展函数,内部调用 FragmentManger 的同名方法
public fun Fragment.setFragmentResultListener(
requestKey: String,
listener: ((requestKey: String, bundle: Bundle) -> Unit)
) {
parentFragmentManager.setFragmentResultListener(requestKey, this, listener)
}

当从 FragmentB 返回结果时:

val result = "result"
setFragmentResult("requestKey", bundleOf("bundleKey" to result))

//setFragmentResult 也是 Fragment 的扩展函数,其内部调用 FragmentManger 的同名方法
public fun Fragment.setFragmentResult(requestKey: String, result: Bundle)

上面的代码可以用下图表示:

Fragment 这些 API 已废弃,你还在使用吗?_Android_10

FragmentA 通过 ​​Key​​​ 向 FragmentManager 注册 ​​ResultListener​​​,FragmentB 返回 result 时, FM 通过 Key 将结果回调给FragmentA ,而且最重要的是 Result API 是生命周期可感知的,​​listener.onFragmentResult​​​ 在 ​​Lifecycle.Event.ON_START​​ 的时候才调用,也就是说只有当 FragmentA 返回到前台时,才会收到结果。

关于 Fragment Result API 的更多介绍,可以参考:

​​juejin.cn/post/697724…​​

最后

Fragment 是帮助我们组织和管理 UI 的重要组件,即使在 Compose 时代也具有使用价值,因此谷歌官方一直致力于对它的 API 的优化,希望他更加易用和便于测试。这些已废弃的 API 在未来的版本中将会彻底删除,所以如果你还在使用着他们,应该尽快予以替换。

官方也提供了工具帮助我们发现对于过期 API 的使用,Fragment-1.4.0 之后,我们可以通过全局设置严格模式策略,发现项目中的问题:

class MyApplication : Application() {

override fun onCreate() {
super.onCreate()

FragmentStrictMode.defaultPolicy =
FragmentStrictMode.Policy.Builder()
.detectFragmentTagUsage() //setTargetFragment的使用
.detectRetainInstanceUsage()//setRetainInstance的使用
.detectSetUserVisibleHint()//setUserVisibleHint的使用
.detectTargetFragmentUsage()//setTargetFragment的使用
.apply {
if (BuildConfig.DEBUG) {
// Debug 模式下崩溃
penaltyDeath()
} else {
// Release 模式下上报

关于 FragmentStrictMode 的更多内容,请参考:​​developer.android.com/guide/fragm…​​

我正在参与掘金技术社区创作者签约计划招募活动,​​点击链接报名投稿​​。

精彩评论(0)

0 0 举报