Fragment管理指北

fragment-management-cover.jpeg

随着ViewModel的大量使用,曾经并不是那么好用的Fragment走上了主流的舞台。相较于简明易懂的Activity管理,Fragment的管理更为复杂。在上周的需求中,我和同事共同完成了一个旧页面的彻底改造。在改造的过程中,我们发现了一个Fragment偶尔重复添加的bug。由于报错信息有限,同时对于Fragment的理解不够,我直接就是一波反向定位:new_moon_with_face:, 认为主要的问题是Activity被kill后恢复时,未判断是否是第一次添加fragment导致了重复添加。最后发现并不是这个问题导致的重复添加:disappointed_relieved:,但是探索的过程非常有趣,也增加了我对Fragment的理解。所以写下此文与大家分享。

茴字有四样写法,你知道么?

Replace

Fragment的replace也许是最简单的替换方法,也是《第一行代码》第一个提到的Fragment管理方式。replace首先会清空同一容器Id下的Fragment栈(remove),然后才是添加新的Fragment。这样处理最大的优点就是简单,因为如果我们全部使用replace的话,显然栈中永远只有一个Fragment,管理起来非常方便。但是简单的背面是每次销毁重建Fragment,带来的性能消耗是难以想象的。所以引出了基于栈的管理方式。

Add, Show和Hide

Fragment的管理类BackStackRecord使用了一个ArrayList(为什么是ArrayList不是Stack?)来管理我们的每一个操作。

fragment-op-flag.png

fragment-op-class.png

结合FragmentTransaction中的静态变量和Op类,FragmentTransaction的实现类能够记录我们每一次对Fragment进行的操作,这通常代表我们能够很轻松地完成回滚操作(已帮我们封装好)。同时给予了我们fragment复用的能力。要想享受这些能力,我们就应该使用add来添加新的Fragment,而不是replace。但是add同样有自己的问题。

fragment-add-only.gif

当我们只add不进行hide,show管理时,fragment会重叠在一起(!)。所以添加多个Fragment时一定要进行hide,show处理。但是这样就高枕无忧了吗?看看这样一个片段,这里我开启了开发者模式退出app就kill进程的开关。

fragment-rebuild-after-destroy.gif

管理Fragment的Activity代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MainActivity : AppCompatActivity() {

// Use this flag to remember the current Fragment
private var currentFragmentTag: String? = null
private val testViewModel by lazy {
ViewModelProvider(this).get(TestViewModel::class.java)
}

private fun addFragment(fragment: Fragment, fragmentTag: String) {
var cur: Fragment? = null
var nxt: Fragment? = null
currentFragmentTag?.let {
cur = supportFragmentManager.findFragmentByTag(currentFragmentTag)
}
fragmentTag?.let {
nxt = supportFragmentManager.findFragmentByTag(fragmentTag)
}
supportFragmentManager.beginTransaction().apply {
// Hide the current fragment
cur?.let {
hide(it)
}
/*
* If the fragment has been already existed in the stack, reuse it.
* Else add the new fragment.
*/
nxt?.let { show(it) } ?: add(R.id.main_container, fragment, fragmentTag)
commit()
}
currentFragmentTag = fragmentTag
Log.d("Main", supportFragmentManager.backStackEntryCount.toString())
}

// Control logic
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testViewModel.fragmentIndex.observe(this, Observer<Int> {
when (it) {
1 -> addFragment(FirstFragment(), FirstFragment.TAG)
2 -> addFragment(SecondFragment(), SecondFragment.TAG)
}
})
testViewModel.fragmentIndex.value = 1
}
}

新出现的问题,和之前只add没有hide,show处理的情况有些类似。很容易联想到是否在哪个环节又出现了只add没有hide的操作。通过查阅资料我们能够了解到,当Activity因为内存不足被回收时,会调用onSaveInstance()来保存视图层(FragmentActivity提供实现)。当Activity再度重建时,之前实例化的fragment会恢复到Activity中。同时onCreate中又走了一遍创建新fragment的逻辑,所以导致了fragment的重叠。

解决方案

重写onSaveInstanceState

如果不走父类的onSaveInstanceState,那么恢复fragment的流程也不会走。当然,所有的状态也都会丢失

1
2
3
override fun onSaveInstanceState(outState: Bundle) {
//super.onSaveInstanceState(outState)
}

通过onCreate的参数savedInstanceState判断是否第一次加载

在第一次创建时,savedInstanceState总是为null。而当恢复activity时,savedInstanceState中保存了数据不再为空,那么我们显然能够通过这种方式来判断是否需要创建新的fragment。这里需要注意的是我们不能使用viewmodel来保存当前的页面,因为viewmodel只在activity是在前台被销毁时,恢复时能够获取到相同的viewmodel。如果activity是在后台太久被杀死的,那么就获取不到相同的viewmodel了。所以唯一的解决方式是通过savedInstanceState来保存信息,恢复时重新读取并赋值给ViewModel一些相关值(比如我存了当前页的Id在ViewModel中,需要恢复)。这样就能完美解决我们的问题。

fragment-final.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class MainActivity : AppCompatActivity() {

private var currentFragmentTag: String? = null
private val testViewModel by lazy {
ViewModelProvider(this).get(TestViewModel::class.java)
}

private fun replaceFragment(fragment: Fragment, fragmentTag: String) {
var cur: Fragment? = null
var nxt: Fragment? = null
currentFragmentTag?.let {
cur = supportFragmentManager.findFragmentByTag(currentFragmentTag)
}
fragmentTag?.let {
nxt = supportFragmentManager.findFragmentByTag(fragmentTag)
}
supportFragmentManager.beginTransaction().apply {
cur?.let {
hide(it)
}
nxt?.let { show(it) } ?: add(R.id.main_container, fragment, fragmentTag)
commit()
}
currentFragmentTag = fragmentTag
Log.d("Main", supportFragmentManager.backStackEntryCount.toString())
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Not the first time, recover data.
if(savedInstanceState!=null){
testViewModel.fragmentIndex.value = savedInstanceState.getInt("finalIndex")
currentFragmentTag = savedInstanceState.getString("finalTag")
}
testViewModel.fragmentIndex.observe(this, Observer<Int> {
when (it) {
1 -> replaceFragment(FirstFragment(), FirstFragment.TAG)
2 -> replaceFragment(SecondFragment(), SecondFragment.TAG)
}
})
// First time to create the activity.
if(savedInstanceState==null)testViewModel.fragmentIndex.value = 1
}

// Save important data here.
override fun onSaveInstanceState(outState: Bundle) {
currentFragmentTag?.let {
outState.putString("finalTag", it)
}
testViewModel.fragmentIndex.value?.let{
outState.putInt("finalIndex",it);
}
super.onSaveInstanceState(outState)
}
}