稀有猿诉
十年磨一剑,历炼出锋芒,说话千百句,不如码二行。
RSS
Blog
Archives
About
Android实战技巧:Fragment的那些坑
APR 30TH, 2015 12:40 AM
Fragment是Android在3.0(Homeycomb)版本时加入的用以更灵活的构建多屏幕界面的可UI组件。关于Fragment以基本使用方法可以参考官方的教程和最佳实践,以及选择Activity还是Fragment。 但是Fragment使用起来却远没有教程中说的那样简单,也远比Activity要复杂一些,这里总结了孤在使用Fragment时所遇到的坑。
嵌套Fragment时Duplicated id或者Tag之痒
这是一个小坑,但是初学者很容易遇到,特别是在Fragment之中套有Fragment时,且又是布局中添加子Fragment时更容易遇到。
现象:
Fragment中套有另一个Fragment,当第二次进入父Fragment时或者由Fragment创建的界面时会抛异常,大致意思是子Fragment的Id或Tag重复了。如果你在layout中给子fragment加了id或者tag,那么一定会遇到此异常。
原因:
在添加Fragment时都可以为Fragment指定一个Id或者Tag用以标识这个Fragment。因为每个Activity所附带的Fragment都是放在一个对象池中,在Activity的生命周期里,Fragment仍然在池中,即使是把某一个Fragment从Activity中detach掉(也即用FragmentManager pop掉),这个池是由FragmentManager来管理的。当你再次要以某个id或者Tag添加Fragment时,FragmentManager会在池中检索,如果发现已经存在Fragment对象带有此Id或者Tag时,就会抛此异常并报怨Id重复。这么做的目的就是减少对象的创建,尽可以的复用对象。
如何破解:
在布局中写fragment时,不要添加id或者tag;
如果非要添加id或者tag,就在代码中添加fragment,如使用Id或者Tag时,先到FragmentManager中查找对象是否存在,不存在时再创建,也即:
Fragment target = getFragmentManager().findFragmentByTag("tag"); if (target == null) { targe = new SomeFragment(); } FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.add(R.id.content, target, "tag"); ft.commit();
replace之痛
现象:
当有二个相同的整体页面层叠时,想把最后一个布局中的某个用Fragment来replace,会发现,它把前面的replace,后面的没效果。
原因:
布局的Id在一个窗体(Activity)中是唯一的,Fragment的replace也是使用此唯一的Id来把相应布局替换成Fragment的。当相同的页面层叠时,同一个Id的布局出现了二次,但Id是一样的。所以FragmentTransaction在replace时仅替换了一个。而不会像期待的那样,替换最后一个页面。
如何破解:
如果相同的页面非要层叠,要么不使用Fragment,要么为布局设置不同的Id。这种情况多出现在布局的复用上面,比如某二个页面长的像,所以复用了同一整体布局。但实际的逻辑上不是相同的页面,完全可以为布局设置不同的Id。
可见性之疼
现象:
当有多个Fragment层叠在一起时,每个Fragment如何能感知其对用户的可见性。比如应用有三个页面,A,B和C,比如A是整体类别列表,B是每个类别的详情,C又是类别的某种更详细的信息,当C显示出来时,A和B怎么能知道它其实对于用户已经不可见了,所以就可以不刷新,不加载数据等等。当C被用户BACK后,B又如何感觉它变成可见了?
原因:
Fragment的生命周期与Activity是一样的,添加到Activity会把OnCreate类似的回调走一遍,然后,Activity onResume/onPause/onstart/onStop时,其所持有的Fragment也走相应的onResume/onPause/onstart/onPause。但是Fragment与Activity非常不同的是,Activity当有另一个Activity显示时,当前的Activity会走onPause/onStop,而Fragment则完全没有感知。最多只能从FragmentManager那里知道BackStackState改变了,但是是Fragment增加了,还是减少了,并不能知道。
如何破解:
这个一个非常令人蛋疼的问题,简单的页面还好,但是涉及到数据加载或者要针对某些事件(网络)刷新时就有问题了,对用户不可见的页面没必要刷新。可行的解法就是:
监听FragmentManager的BackStackState的改变
定义页面路径深度然后与BackStack深度比较,以感知是否对用户可见 如前面A是一级,其path为1,B是2,C是3。当前Stack深度为3时,C是可见的,A与B不可见,以此类推。
空白区域的点击之脓
现象:
一个Fragment,层叠在另外一个Fragment或者Activity之上,此Fragment中有一些空白区域,也即Widget之外的空白区域,当点击这些空白区域的时候发现这个Fragment下面的Fragment或者Activity中的View收到了事件并且响应了点击事件。
原因:
Fragment的本质就是一个View布局的管理器,当Fragment attach到Activity时,其实就是把Fragment#onCreateView()返回的View,替换掉(如果是用replace)FragmentTransaction#replace中指定的View,或者添加到(如果是add)FragmentTransaction#add()中指定的ViewGroup里面。
当我们以层叠方式显示多个Fragment时,通常的做法就是弄一个FrameLayout,然后每次把Fragment add到此布局。因此,这时Activity的页面布局树实际上就是一个FrameLayout里面包含几个View。
所以,当点击上面Fragment的空白区域时,如果事件没被吃掉,就会向下传递。
如何破解:
在Fragment的根布局加上一个clickable=true,这会让根布局把点击事件吃掉,以防止事件会继续传递下去,造成上面的情况。
Activity重新创建之殇
现象:
这个没有一般性的错误,只会有与项目相关的具体的错误异常,或者页面显示不正确。以及为什么教程中都有这么一句:
123456
@OverrideonCreate(Bundle savedInstance) { if (savedIntance == null) { // create fragment and add it to Activity. }}
原因:
Activity除了正常启动走到onCreate,还有另外的入口,比如系统配置信息发生变化时,或者Activity在栈比较深的地方,系统会把Activity杀掉,然后再重新创建它,问题就是在这个重新创建。重新创建与新建一个Activity不同,它是要尽可能的恢复先前所在的状态,因为这对用户来说是透明的,也就是说不能让用户感知到,否则体验会相当差。唯一与常规创建的区别就在于传给onCreate的参数savedInstanceState是不是null.
如何破解:
为了能在Activity重建时恢复状态,需要:
对于Activity
要在onSaveInstanceState()时,把一些变量保存,然后在onCreate时恢复
对于Fragment
告诉系统,你想恢复状态Fragment#setRetainInstance(true)。然后,也在onSavedInstance()中保存状态,在onCreate时恢复。 这就够了,系统会在重新创建Activity时把其所持有的Fragment也创建出来。所以为什么每个Fragment子类都需要定义一个默认的Constructor。更多的可以参考这篇文章。
FragmentTransaction的异步操作之殇
FragmentTransaction是异步的,commit()仅是相当于把操作加入到FragmentManager的队列,然后FragmentManager会在某一个时刻来执行,并不是立即执行。所以,真正开始执行commit()时,如果Activity的生命周期发生了变化,比如走到了onPause,或者走到了onStop,或者onDestroy都走完了,那么就会报出IllegalStateException。
还有一个异步的原因就是,在异步中操作(显示)Fragment。比如,先去网络请求数据,然后根据数据显示一个Fragment,这个特别容易出现的情况是网络请求回来了,但是Activity已经不在了,这时如果commit也会报出IllegalStateException。
具体的原因,以及如何避免可以参考大牛的这篇文章。
常见的解法就是作者建议的:1. 小心在生命周期中commit 。2 尽量不要在异步回调中commit 另外的解法 就是
在异步回调中判断Activity是否在销毁中,isFinishing,如果true,就停止做其他事情
尽可能把异步任务控制在活动的生命周期内(onStart->onStop)。当出现stop时终止异步任务。再次start时再次启动。
但是这个并不适用所有情况。比如按HOME的情况,通常这个过程不需要把任务停掉。因为一般情况下,再切回来时,应用应该保持切走时的状态,比如,加载一个数据,按HOME切走,再回来时,应该加载完成。这也正是多任务系统的一个表现。 如果onstop时停掉任务,那么要做很多工作来在onstart时恢复状态。
使用commitAllowStateLoss() 这个是最终方案。除了从设计 上避免以外,这是唯 一的方式。
恶心的Activity重建以及恢复其Fragment
首先说安卓系统非常恶心的一点就是某些情况下系统会杀掉Activity,然后重新创建并尝试恢复其先前的状态,比如当旋转屏幕时,当系统语言发生变化时,当栈中的Activity被回收了,又到栈顶时等等,这点非常恶心,常常带来问题。识别重建与新建的方法就是看onCreate中的Bundle参数是不是null。
对于FragmentActivity,更加恶心,此种场景时,它在onSaveInstance时会保存Fragment,然后在onCreate时会重新创建,会调用Framgment的默认无参构造来创建Fragment对象。所以这也是为什么文档中说Fragment一定要有一个默认的构造函数,而且最好不要有带参数的构造函数,传参数要用setArguments。默认构造函数的原因是为了重建Fragment实例。setArguments的参数是一个Bundle也会跟随Fragment保存起来,在重建Fragment时会帮你恢复。这里的恢复状态的数据的保存都是通过Binder方式保存在系统中,这也说明为啥参数非要是一个Bundle。
那么问题来了,当你确实需要带参数的构造函数,或者说系统无法帮你重建Fragment(比如Fragment要从动态加载的Dex中获取)时怎么办呢?
首先,我们要模拟这一场景,最方便的就是把activity的configChanges去掉,然后旋转屏幕。
一个思路就是阻止系统恢复Fragment,我们可以自己来加载,因为重建也会走到Activity的onCreate,所以我们有理由重走一遍初始化流程。怎么阻止呢,就是在FragmentActivity保存所有Fragment状态前把Fragment从FragmentManager中移除掉。
1234567
@Overridepublic void onSaveInstance(Bundle out) { FragmentTransaction ft = getSupportFragmentManager().benginTransaction(); ft.remove(frag); ft.commitAllowStateLoss(); super.onSaveInstance(out);}
参考
Fragments
Building a Dynamic UI with Fragments
Fragment Transactions & Activity State Loss
Handling Configuration Changes with Fragments
Posted by Alex Hilton Apr 30th, 2015 12:40 am
effectiveandroid
1
« Android NDK开发的一些技巧CSS Animation初探 »
Comments
最新最早最热
4条评论
1条新浪微博
solidajun
期待作者更多的精彩文章~
1月26日回复顶转发
AndWang
第一个应该是target你写成了targe,嘿嘿
4月21日回复顶转发
风语安然
非常非常感谢!!!解决了我的大问题。 空白区域的点击之脓,哈哈哈
5月8日回复顶转发
蒋朋
以为来到了小黑屋,哈哈,不错哦
5月30日回复顶转发
社交帐号登录:
微信
微博
QQ
人人
更多»
发布
稀有猿诉正在使用多说
About Me
实用主义的浮躁码农
专注移动互联网,五年Android开发经验,也关注iOS开发和WEB开发
Recent Posts
说说Android的MVP模式
Android技巧:学习使用GridLayout
安卓开发技术:监听软键盘的显示与隐藏
Android应用性能剖析全攻略
Android Studio技能之快捷键
更多的文章
浪人的星空
Categories
miscellaneous
effectiveandroid
android
ios
java
web
GitHub Repos
pandoraCrawler to get hot movies
MoreEffectiveAndroidExamples of Android App Development
alexhilton.github.io
miscellaneousMiscellaneous code samples when learning new languages
cocoajourneyThe projects with Cocoa Touch
effectivecocoaAPI demos and tutorials about Cocoa Touch
EffectiveJavaSample codes for Java SE
EffectiveAndroidExamples and exercises and tutorials when learning Android Development.
gitatouilleGit manuals, Git tutorials, articles about Git and Git tricks. All in an app.
vulcanAndroid sample app with iOS style and theme including bottom tabs and header navigation bar
@alexhilton on GitHub
Copyright © 2016 - Alex Hilton - Powered by Octopress