作者: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)将被再次创立。一切你的数据将被重置。

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

上图中的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()