Fragment 使用已经很广泛了,它的生命周期比Activity的稍微复杂点,Fragment 不是 View,它的职责,更像是 MVP 模式中的 P。Fragment 经常嵌套于Activity中,或与 ViewPager 配套使用;ViewPager + Fragment 应该是最常用的一种使用方法,但也会引出一个问题,那就是 Fragment 延迟加载的优化。
ViewPager 是有预加载机制的,默认提前加载下一个,这就导致在当前页面,会执行下一个页面Fragment的 onCreateView() 和 onResume() 方法,这就有点坑了,如果我们在上面两个方法中加载数据,就提前加载并刷新页面了,在底端手机上可能会造成UI的不流畅,或浪费了一次请求,或者一些根据生命周期回调做的页面展示统计,可能就不准确了。这里还有一个方法要提及 setUserVisibleHint(),这个方法是根据 Fragment 的展示或隐藏回调的,关于它的逻辑,可以参考FragmentPagerAdapter 类。
如果做懒加载呢?做个功能总是要有逻辑分析的,那么就一步步处理吧。
一、在 onCreateView() 方法中初始化布局及控件的初始化 initView(),为了防止布局多次初始化,可以针对根布局 mRootView 做个非空判断,初始化一次;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(mRootView == null){
View view = inflater.inflate(R.layout.fragment_tab_sticky, container, false);
initView();
mRootView = view;
} else {
ViewGroup mParent = (ViewGroup) mRootView.getParent();
if (mParent != null) {
mParent.removeView(mRootView);
}
}
return mRootView;
}
二、如果在 onCreateView() 方法中加载数据,即使在 initView() 方法中,那么在 ViewPager 这种情况下,仍然会预加载,那么我们就考虑另外一个方法 setUserVisibleHint(),我们可以在这个方法中来进行数据请求;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
}
三、假如说 ViewPager 中添加了5个 Fragment,那么第一个Fragment被加载时,会先执行 setUserVisibleHint() 方法回调,然后才是 onCreateView() 方法,并且 setUserVisibleHint() 方法会执行多次,如果直接在 setUserVisibleHint(boolean isVisibleToUser) 方法中根据 isVisibleToUser 的值进行数据请求并刷新UI,则大概率会报控件的空指针。 (可以自己写个日志打印下)
四、可以在 onCreateView() 中添加一个 mRootView 初始化的标志 boolean mIsViewCreated,在 setUserVisibleHint() 方法中,只有 mIsViewCreated 为 true,才进入数据请求的判断。但由于 setUserVisibleHint(boolean isVisibleToUser) 会多次调用,所以判断还要加上 isVisibleToUser 的值。
boolean mIsViewCreated = false;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
... // 布局初始化
mIsViewCreated = true;
return mRootView;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(mIsViewCreated){
if(isVisibleToUser){
takeData(); // 数据获取
}
}
}
五、第一个Fragment被加载,setUserVisibleHint(boolean isVisibleToUser) 会执行两次,并且都是在 onCreateView() 之前,所以按照上面的写法,第一个Fragment不会执行数据加载的逻辑,所以可以在 onCreateView() 中在布局初始化之后,并且是可视的情况下,主动调用 setUserVisibleHint() 方法。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
... // 布局初始化
mIsViewCreated = true;
if(getUserVisibleHint()){
setUserVisibleHint(true);
}
return mRootView;
}
以上初步完成了数据的懒加载,但有几个问题:1、第一个Fragment,或者ViewPager左右反复滑动,takeData() 方法会多次执行,还需要加入其它判断条件;2、我们做的这个懒加载,不止是数据,还有其他也可以延迟加载,所以这个地方可以与业务逻辑剥离,takeData() 代码的地方,做成界面展示获取焦点的方法;3、既然有获取焦点的方法,那么最好也提供失去焦点的方法,便于业务逻辑;4、我们没考虑 Activity 被覆盖,回到桌面,重新进来 这些场景,此时 setUserVisibleHint() 和 onCreateView() 都不会执行,所以还需要考虑 onResume() 方法。
六、重写定义一个方法 dispatchVisibleHintState(boolean state) ,意思是展示或隐藏界面,在这个方法中,我们定义写具体的细节。
private void dispatchVisibleHintState(boolean state){
if(state){
dispatchShow();
}else{
dispatchDis();
}
}
private void dispatchShow() {
// 获取焦点,显示UI
}
private void dispatchDis() {
// 失去焦点,隐藏UI
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(mIsViewCreated){
dispatchVisibleHintState(isVisibleToUser);
}
}
七、 setUserVisibleHint() 方法中的逻辑需要修改了,可以再添加一个成员变量 mCurrentVisibleState,默认 false, 它的意思就是当前是否正在显示,放在 dispatchVisibleHintState(boolean state) 中接收 state 的值。每个页面加载数据,我们需要保证该页面是可见的,并且 mCurrentVisibleState 是false,说明没加载过,此时调用 dispatchVisibleHintState(true) 方法,改变 mCurrentVisibleState 的值,这样问题1便被解决了; 失去焦点的前提是获取过焦点,所以失去焦点的判断则与获取焦点的判断相反,见代码
boolean mCurrentVisibleState = false;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(mIsViewCreated){
if(!mCurrentVisibleState && getUserVisibleHint()){
dispatchVisibleHintState(true);
} if(mCurrentVisibleState && !getUserVisibleHint()){
dispatchVisibleHintState(false);
}
}
}
private void dispatchVisibleHintState(boolean state){
mCurrentVisibleState = state;
if(state){
dispatchShow();
}else{
dispatchDis();
}
}
八、问题4提到的地方,在这些场景中,需要用到 onResume() 和 onPause() 方法,这些场景都是获取过焦点,然后又失去了焦点,此时需要重新获取。当Activity切入后台,重新切回时,ViewPager 中当前页面和下一个页面的 Fragment 都会执行 onResume() 方法,那么我们就需要同时判断 mCurrentVisibleState 为false 且 getUserVisibleHint()为true的情况了,因为切入前台后,才会执行 onResume() 方法,此时当前 Fragment 是可见的,下一个 Fragment 则是不可见的; 同理,当app切入后台时,mCurrentVisibleState必须是true,并且 getUserVisibleHint() 也得是true,应为改页面是从可见变为不可见。
@Override
public void onResume() {
super.onResume();
if(!mCurrentVisibleState && getUserVisibleHint()){
dispatchVisibleHintState(true);
}
}
@Override
public void onPause() {
super.onPause();
if(mCurrentVisibleState && getUserVisibleHint()){
dispatchVisibleHintState(false);
}
}
注意,setUserVisibleHint(boolean isVisibleToUser) 方法中的两种状态,判断条件的相反的;onResume() 和 onPause() 中,getUserVisibleHint() 都是true,必须是可见的,然后根据 mCurrentVisibleState 状态来判断,这样这两个方法与 setUserVisibleHint(boolean isVisibleToUser) 都调用dispatchVisibleHintState()方法,但不会引起冲突。以上就是 dispatchShow() 获取界面,dispatchDis() 失去界面, 我们可以在里面做相应的数据及界面UI刷新;有些数据是一次性的,可以加boolean值,有些是根据时间限制来的,比如说再次来到该页面,与上次相比,超过15分钟就重新获取数据,都可以在这个里面处理逻辑。
最后上个稍微完整点的代码
Fragment:
View mRootView;
boolean mIsViewCreated = false; // 其实可以用 mRootView == null 来代替这个属性,这里是为了逻辑分隔
boolean mCurrentVisibleState = false;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if(mRootView == null){
View view = inflater.inflate(R.layout.fragment_tab_sticky, container, false);
initView(view);
mRootView = view;
} else {
ViewGroup mParent = (ViewGroup) mRootView.getParent();
if (mParent != null) {
mParent.removeView(mRootView);
}
}
mIsViewCreated = true;
if(getUserVisibleHint()){
setUserVisibleHint(true);
}
return mRootView;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(mIsViewCreated){
if(!mCurrentVisibleState && getUserVisibleHint()){
dispatchVisibleHintState(true);
} else if(mCurrentVisibleState && !getUserVisibleHint()){
dispatchVisibleHintState(false);
}
}
}
@Override
public void onResume() {
super.onResume();
if(!mCurrentVisibleState && getUserVisibleHint()){
dispatchVisibleHintState(true);
}
}
@Override
public void onPause() {
super.onPause();
if(mCurrentVisibleState && getUserVisibleHint()){
dispatchVisibleHintState(false);
}
}
private void dispatchVisibleHintState(boolean state){
mCurrentVisibleState = state;
if(state){
dispatchShow();
}else{
dispatchDis();
}
}
private void dispatchShow() {
}
private void dispatchDis() {
}