对于Android开发者来说,深入理解Fragment的原理是重要的。但是,Fragment是一个复杂的组件,大部分人使用它都会犯一些错误。
在Fragment上出现的bug有时候非常难debug,因为Fragment有非常复杂的生命周期,不是总能复现场景。
不过,一些问题能够在代码编写阶段简单地避免。下面是7个问题:
1. 在创建Fragment时,没有检查savedStateInstance
一般我们使用如下代码,在Activity(或者Fragment)的onCreate中显示Fragment界面
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.container, NewFragment())
.commit()
}
为什么上面的代码不好
上面的代码有一个问题。当你的activity是被系统杀死并恢复时,一个重复的新Fragment将被创建,即恢复的Fragment和新创建的。
正确的方式
我们应该使用savedInstanceState == null来判断。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, NewFragment())
.commit()
}
}
如果之前的Fragment被恢复,它将避免创建和执行新的Fragment。如果你想避免恢复Fragment,Manually override Fragments Auto Restoration有一些技巧(虽然不建议用于专业应用程序)。
2. 在onCreateView创建Fragment拥有的对象
有时候,我们需要保证数据对象在Fragment的生命周期中存在。我们认为我们可以在onCreateView中创建它,因为这个方法只在Fragment创建时或者从系统杀死状态恢复时执行一次。
private var presenter: MyPresenter? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
presenter = MyPresenter()
return inflater.inflate(R.layout.frag_layout, container, false)
}
为什么上面代码不好
但,上面的说法是有问题的。当Fragment是被另一个Fragment使用replace替换时,这Fragment没有被杀死。同时数据对象仍然在Fragment里面。当Fragment是被恢复(例如另一个Fragment被pop out),这onCreateView将再执行一次。因此数据对象(这里是presenter)将被再次创建。所有你的数据将被重置。
上图中的onCreateView可以在同一个Fragment实例中被反复调用。
不算好的方法
我们可以在创建之前加一个非null判断。
private var presenter: MyPresenter? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (presenter != null) presenter = MyPresenter()
return inflater.inflate(R.layout.frag_layout, container, false)
}
这个虽然能解决上面的问题,但不算一个好的方法。
更好的方式
我们应该在onCreate中创建Fragment拥有的数据对象。
private var presenter: MyPresenter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter = MyPresenter()
}
通过这种方式,数据对象将只在每次创建Fragment时被创建一次。当我们弹出顶层Fragment并重新显示Fragment视图时(即调用onCreateView),它将不会被重新创建。
3. 在 onCreateView 中执行状态恢复
我知道在onCreateView中提供了savedInstanceState。因此我们认为我们可以在这里存储数据。
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (savedInstanceState != null) {
// Restore your stuff here
}
// ... some codes creating view ...
}
为什么这个不好
上面的方式可能造成一个奇怪的问题,你存储在一些Fragment(非顶部可见Fragment)的数据会丢失。出现场景有:
● 你在堆中有超过一个Fragment(使用replace代替add)
● 你把你的应用放到后台并恢复两次或多次
● 你的Fragment被destroy(例如被系统杀死)并恢复
更多关于这个问题的细节可以看Bug that will only surface when you background your App twice
更好的方式
就像上面的示例2一样,我们应该在onCreate方法中执行状态恢复。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
// Restore your stuff here
}
}
这种方式可以确保你的Fragment状态总是被恢复,无论你的视图是否被创建(即使是堆栈中不可见的Fragment,也将恢复其数据)。
4. 在Activity中保存了Fragment的引用
有时候因为一些原因,我们想要在Activity(或者父Fragment)中获取Fragment的对象。通过下面的方式,我们可以很简单地获取Fragment的引用。
private var myFragment: MyFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
myFragment = NewFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.container, myFragment)
.commit()
}
}
private fun anotherFunction() {
myFragemnt?.doSomething()
}
为什么上述方式不对
Fragment有自己的生命周期。它被系统杀死并恢复。这个意味着引用的原始的Fragment不再存在(虽然myFragemnt不会为null,但是我们执行doSomething时可能由于Fragment被杀死而出错)。
如果我们在Activity中保持Fragment的引用,我们需要确保能不断更新对正确Fragment的引用,如果丢失了,会很棘手。
更好的方式
通过Tag获取你的Fragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, NewFragment(), FragmentTag)
.commit()
}
}
private fun anotherFunction() {
(supportFragmentManager.findFragmentByTag(FragmentTag) as?
NewFragment)?.doSomething()
}
当你需要访问它时,你总是能通过Fragment Transaction找到它。尽管这是一种可行的方式,我们还是应该尽量减少Fragment和 Activity(或父Fragment)之间的这种交流。
5. 在Fragment的onSavedStateInstance方法中访问View
有时我们想要在Fragment被系统杀死时保存一些view的信息。
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding?.myView?.let {
// doSomething with the data, maybe to save it?
}
}
为什么上面的方法不对
考虑这个场景
● 如果Fragment没有被杀死,而是被另一个Fragment给replace了,这个Fragment的onViewDestroy()方法将被调用 (一般情况下,我们在这里设置binding = null)
● 由于Fragment仍然存在,onSaveInstanceState不会被调用。但是Fragment的view已经不存在了
● 然后,这个时候Fragment被系统杀死,onSaveInstanceState被调用。但是,由于binding是null,该代码不会被执行。
正确的方法
无论你想从view中访问什么,都应该在onSavedStateInstance之前完成,并存储在其他地方。最好是所有这些都在presenter或View Model中完成。
6. 更喜欢使用add而不是replace
我们有replace和add去操作Fragment。有时我们只是想知道我们应该使用哪一个方法。可能我们应该使用add,因为它听上去更合乎逻辑。
supportFragmentManager.beginTransaction()
.add(R.id.container, myFragment)
.commit()
使用add的好处是,确保底部Fragment的view不会被销毁,并且当顶部的Fragment弹出时不需要重新创建view。在下面一些应用场景中,add是有用的。
● 当下一个Fragment是add到一个Fragment的顶部时,它们两个都是可见的,并且互相叠加。如果你在顶部Fragment上有一个半透明的view,你可以看到底部的Fragment
● 当你添加的Fragment是花费长时间加载的时(例如加载Webview),你想要避免其他Fragment弹出时重新加载它。这时你就使用add代替replace。
为什么上面的方式不好
上面提到的两种场景并不常见。因此add应该被限制使用,因为它有如下缺点。
● 使用add将保持底部的Fragment可见,它花费了更多不必要的内存。
● 添加了一个以上可见的Fragment,当它们被一起恢复时,有时可能会导致状态恢复问题。The Crazy Android Fragment Bug I’ve Investigated是2个Fragment一起加载并使用的情况,它会导致复杂和混乱的问题。
首选方式
使用replace代替add,即使是第一个Fragment提交时。因为对于第一个Fragment,replace和add没有不同,不如直接使用replace,使之成为默认的普遍做法。
7. 使用simpleName作为Fragment的Tag
有时我们想对 Fragment进行标记,以便以后检索。我们可以使用当前class的simpleName来标记它,因为它是方便的。
supportFragmentManager.beginTransaction()
.replace(
R.id.container,
fragment,
fragment.javaClass.simpleName)
.commit()
为什么这个不好
在Android中,我们使用Proguard或DexGuard来混淆类的名称。而在这个过程中,混淆后的简单名称可能会与其他类的名称发生冲突,如The danger of using getSimpleName() as TAG for Fragment所述。它可能很少见,但一旦发生,它可能会让你惊慌失措。
首选方式
考虑使用一个常数或规范名称作为tag。这将更好地确保它是唯一的。
supportFragmentManager.beginTransaction()
.replace(
R.id.container,
fragment,
fragment.javaClass.canonicalName)
.commit()
本文暂时没有评论,来添加一个吧(●'◡'●)