必知必会,7个使用Android Fragment容易犯的错误【译】

4,408 阅读7分钟

作者:Elye

原文链接:medium.com/mobile-app-…

对于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()