在 Android 中使用 Fragment 时,我们常常会在 Fragment 中定义一些按钮监听之类的事件。在我看来,为 Fragment 中的事件定义回调函数有两种方式,一种是实现接口,另一种是传递函数类型(Kotlin)或匿名内部类(Java)。前一种较为简单,也是官方教程中提到的方式,后一种因为考虑到 Activity 重建的问题,有一些需要注意的地方。这篇文章介绍了这两种方式。
Fragment 在 Activity 重建时的行为
众所周知,初始化 Fragment 向其传递参数时,应该使用工厂模式。即定义一个“静态”函数(一般名为 newInstance
),在此函数中创建 Fragment 对象,并将参数保存在 arguments
中,后续再从 arguments
中取参数。例如初始化一个 ExampleFragment
:
class ExampleFragment : Fragment() { companion object { fun newInstance(val param: Int): ExampleFragment { val args = Bundle() args.putInt("param", param) val fragment = ExampleFragment() fragment.arguments = args return fragment } } ... }
kt
为什么要这样做,而不是直接将参数通过构造函数传入呢?因为当某些事件,例如旋转屏幕、切换分屏等造成该 Fragment(直接或间接)附着的 Activity 重建时,类中属性对应的对象全部都会被回收,除了 arguments
的 Bundle
对象。在 Fragment 附着的 Activity 准备重建时,该对象会保存在 Activity 的那个 Bundle
对象中(就是 onCreate
等生命周期回调函数的参数 savedInstanceState
,我也不知道叫什么名字😑),然后在恢复的时候,Activity 会直接反射调用该 Fragment 的默认无参构造函数,然后将该 Fragment 对应的 Bundle
取出,传递给它的 arguments
属性。所以通过构造函数传递的参数这时候就完全取不到了,而保存在 arguments
中的参数就还健在。关于这方面的资料已有很多,此处就不再赘述了。
传递事件的回调函数
一个 Fragment 中可能会定义一些事件(例如 Fragment 中的某个按钮的触摸事件),当这些事件发生了,需要通知附着的宿主类(可以是 Activity 或父 Fragment)进行相应的处理,这就需要向 Fragment 传递事件的回调函数。回调函数定义成函数类型的属性,让 Fragment 在事件发生时通过这个属性调用回调函数即可。
那么,这个回调函数怎样传入呢?基于上面提到的方式,自然想到了使用工厂模式传入。然而,仔细想想,发现这样并不行,因为 arguments
是个 Bundle
类型的属性,所能存储的变量类型是有限制的,并不支持函数类型(Java 中是匿名内部类)的存储。那么,就需要找到其他的方法传入回调函数,并且在 Activity 重建的时候能恢复。
第一种方式:实现接口
这是官方教程中提到的方式。具体来说,就是在 Fragment 中定义一个接口,然后宿主类实现这个接口,重写相应的函数,在 Fragment 中将宿主类强制转换成这个接口类,就可以在 Fragment 中通过这个接口调用回调函数了,也就实现了对宿主类的通知。以一个 ExampleFragment
为例(与官方教程相比有改动):
class ExampleFragment : Fragment() { private val callback: Callback? by lazy { when { // 如果该 Fragment 作为子 Fragment 附在某个父 Fragment 上,且父 Fragment 实现了 Callback 接口 parentFragment != null && parentFragment is Callback -> parentFragment as Callback // 如果该 Fragment 附在某个父 Fragment 上,但其未实现 Callback 接口 // 但该 Fragment(直接或间接)附在了某个 Activity 上,且 Activity 实现了 Callback 接口 activity != null && activity is Callback -> activity as Callback // 该 Fragment 没有附到任何 Activity 或父 Fragment 上,或它们都没有实现 Callback 接口 else -> null } } interface Callback { fun onSomethingHappened() } ... }
kt
它附在了 MainActivity
上,则 MainActivity
必须实现 ExampleFragment.Callback
接口:
class MainActivity : AppCompatActivity(), ExampleFragment.Callback { overrive fun onSomethingHappened() { ... } ... }
kt
这样并不用考虑该 ExampleFragment
附着的 MainActivity
重建的问题,因为只要 MainActivity
实现了 ExampleFragment.Callback
接口,就算 MainActivity
重建了,也能在 ExampleFragment
中拿到 ExampleFragment.Callback
对象,就可以调用回调函数通知 MainActivity
了。
向 Fragment 传递参数的另一种方式
实际上,还有另一种方式向 Fragment 传递参数,它就是直接对 Fragment 中的属性赋值(即调用属性的 Setter),只是需要重写属性默认的 Setter,将属性的新值保存到 arguments
中,也可以实现在 Activity 重建时的恢复,并且还可以随时更改属性的值,更改后的值也会保存到 arguments
中。以一个 ExampleFragment
为例,它附着在 MainActivity
上:
class ExampleFragment : Fragment() { var param = 0 set(value) { // 更新幕后字段 field = value // 刷新相关的控件等 updateView() // 将新值储存到 arguments arguments!!.putInt("param", value) } ... }
kt
那么,如果要传递事件的回调函数呢?也可以这样定义:
class ExampleFragment : Fragment() { var somethingHappenListener: (() -> Unit)? = null ... }
kt
实际上这是对 Fragment 中的函数类型的属性赋值。然而这里并没有重写默认的 Setter 将事件的回调函数存储到 arguments
中,原因上面已经提到过了。这样就造成了如果 MainActivity
重建,somethingHappenListener
这个属性就为空了,ExampleFragment
自然没法在事件发生时通知到 MainActivity
了。
第二种方式:传递函数类型
那么怎样才能使用属性赋值的方式向 Fragment 传递事件的回调函数呢?可以从传递的时机下手。如果只是在新建 Fragment 的时候传递,那么当然 Activity 重建后 Fragment 就没法调用回调函数了。那如果在每次 Fragment 附到 Activity 或父 Fragment 的时候传递呢,这样不就行了。基于此,我找到了一个很有用的回调函数,它就是 onAttachFragment
,看下此函数的文档(以 android.support.v4.app.FragmentActivity
中的为例):
也就是说,当 Fragment 附着到 Activity 上时会调用此函数,而此函数的参数就是附着到 Activity 上的那个 Fragment,这不正是我们想要的吗。所以,就可以在这个函数中向 Fragment 传递事件的回调函数了,以 MainActivity
为例:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { // 创建 ExampleFragment(忽略其他参数的传入) supportFragmentManager.beginTransaction().add(R.id.vContainer, ExampleFragment(), "example").commit() } } override fun onAttachFragment(fragment: Fragment) { super.onAttachFragment(fragment) if (fragment.tag == "example") { (fragment as ExampleFragment).somethingHappenListener = { ... } } } ... }
kt
在 Fragment 中也有 onAttachFragment
函数,只是其参数名为 childFragment
,当某个 Fragment 作为子 Fragment 附在此 Fragment 上时会调用。创建 Fragment 的时候也需要使用 childFragmentManager
而不是 fragmentManager
。
这样,就算 Activity 再怎么重建,只要宿主类有将 Fragment 附到其上的行为,事件的回调函数就一定可以传递到 Fragment 中,就可以愉快地在宿主类中使用属性赋值的方式向 Fragment 传递事件的回调函数啦。
后记
为什么要想到属性赋值的方式向 Fragment 传递事件的回调函数呢,因为如果使用官方推荐的那种工厂模式 + 实现接口的方式,中途如果需要修改某个参数,还是需要使用属性赋值的方式传递新值到 Fragment,还不如直接全部用属性赋值的方式呢。但实际上我更喜欢混用,即使用建造者(Builder)模式传递可以储存在 arguments
中的参数,使用属性赋值的方式传递事件的回调函数(即传递函数类型)以及更新所有的参数。因为我不太喜欢使用实现接口的方式来传递事件的回调函数,不仅仅是 Fragment,像控件的触摸事件,我一般只会用 setOnClickListener
而不是实现 View.OnClickListener
接口。当然,这些都是后话了。