这篇文章是讲有关于Fragment的使用,大部分还是比较基础的知识点。之所以写出来呢,因为我在工作中发现在使用fragment时走了很多弯路,遇见了很多坑。就是因为还有很多细节的东西没有掌握。在这里分享出来,也能方便自己回顾。
1. fragment概述
fragment是一种控制器对象,activity可以委派一些任务给它,通常这些任务是管理用户界面。受管的用户界面可以是整个屏幕或者是小部分屏幕。
fragment与支持库
Google在API11的时候引入的fragment(为满足平板UI设计的灵活性要求),最低能兼容到API4。
为了应用能够兼容旧版本设备,Android提供了开发支持库,分别是Fragment(android.support.v4.app.Fragment)、FragmentActivity(android.support.v4.app.Fragment-Activity)
关于操作系统内置版的fragment库
开发者在平时都会优先使用支持库版的fragment而不是系统内置的版本,是因为Google每年都会更新支持库,通过此来发布引入新特性,修复Bug,如果想体现这些好处,我们只需要升级项目的支持库版本就好,Google提供支持库的本意就是能够方便开发人员在不支持API的版本是能体验Fragment。
2. 托管UI fragment
我们都知道fragment是由activity来管理的,在托管fragment的期间,activity必须处理好以下俩件事:
- 管理好fragment的生命周期
- 在布局中为fragment安排位置
2.1 fragment的生命周期
fragment生命周期 | fragment与Activity生命周期的对比 |
---|---|
fragment生命周期与activity的生命周期方法非常类似,也具备停止、暂停、运行转态,也拥有可以覆盖的方法,在关键的节点时候完成一些任务,许多方法也对应了activity的生命周期方法。大部分人都知道fragment的onAttach()、onCreat()、onCreatView()这三个生命周期方法是在Activity的onCreat()的时候执行的。看过源码应该知道,这三个方法是在onCreate()中的setContentView()中调用的。
生命周期方法在开发中是非常重要的,因为fragment代表activity工作,它的状态就反应着activity的状态,显然,fragment需要相对应的生命周期方法来处理activity的工作。那么fragment与activity的生命周期方法最为关键的区别就在于,fragment的生命周期方法是由activity来调用而不是操作系统,操作系统不关心activity用来管理视图的fragment。
2.2 托管的俩种方式
activity托管fragment有以下俩种方式:
- 在activity布局中添加fragment
- 在activity通过code添加
第一种方式比较简单,直接通过activityu的布局绑定fragment,但是不够灵活,在fragment生命周期过程中,无法切换其他fragment视图,所以在此我们不多作介绍。
第二种方式:
虽然我们是要在托管的activity中通过代码添加fragment,但是首先得定义布局容器,也就是为frgament安排位置,布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
创建fragment以及实现生命周期方法
package com.haife.album_master.activities;
import android.os.Bundle;
import android.support.v4.app.Fragment;
/**
* A simple {@link Fragment} subclass.
*/
public class BlankFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
以上代码需要注意几点:
- Fragment的生命周期方法都是公共方法,与Activity不同,activity生命周期方法是保护方法,前面也说了,这样是为了让托管的activity能够调用
- 类似于activity,fragment中也有保存和获取状态的Bundle。如同activity的onSaveInstance()方法使用相同,我们也可以覆盖这个方法在fragment中
- fragment的试图创建并不是在Fragment.onCreat()方法中生成,虽然我们在方法中配置了fragment 的实例,但创建和配置视图在另一个生命周期方法onCreatView()中完成的
package com.haife.album_master.activities;
import android.os.Bundle;
import android.support.v4.app.Fragment;
/**
* A simple {@link Fragment} subclass.
*/
public class BlankFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
TextView textView = new TextView(getActivity());
textView.setText(R.string.hello_blank_fragment);
return textView;
}
}
添加fragment到FragmentManager
FragmentManager类管理的是fragment队列和fragment的事物回退栈,下图为FragmentManager图解:
要以代码的方式添加fragment到activity中,我们只需要调用activity中的FragmentManager,首先获取FragmentManager。
FragmentManager fm = getSupportFragmentManager();
Fragment事务
当我们在activity中获取到FragmentManager时,我们需要把fragment交由其管理。代码如下:
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = new BlankFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
}
add(...)方法是整个事务的核心,除了添加,fragment事务还可被用作移除、附加、分离、替换fragment队列中的fragment,这个是使用fragment在运行时组装和重新组装用户界面的关键,FragmentManager管理着fragment事务的回退栈。
FragmentManager.beginTransaction()方法是创建并返回FragmentTransaction实例,由此可得到一个FragmentTransaction队列。
现在我们重头到位总结一下上面的代码,首先,用过资源Id向FragmenbManager发出请求获取fragment实例,如果获取的fragment不为空,那么FragmentManager直接返回它,这里有个问题,很多人会问为什么fragment会存在于队列中呢?你明明还没有添加fragment,其实很简单,我们之前说过,当设备旋转的时候activity的生命周期会发生变化,在activity销毁的时候,activity的FragmentManager会将fragment队列保存起来,当activity执行生命周期的onCreat()方法的时候,FragmentManager会优先回去到保存的Fragment队列,然后重建重而恢复之前的状态,这样你们就知道原因了吧!下面判空的时候如果为空,就直接添加到fragment队列中0。
抽象的activity类
我们可以看到上面的实现代码几乎通用,但是唯一不足的地方就是在添加fragment到FragmentManager的时候实例化的fragment只能是BlankFragment,为了应用的灵活性。所以接下来我们继续优化。如何处理呢,那我们是不是可以在上面的activity定义一个抽象的方法来当作activity的父类,那么子类就会实现该方法来返回fragment实例。
为了区分,我们创建一个BaseFragmentActivity
public class BaseFragmentActivity extends FragmentActivity {
protect abstrct Fragment creatFragment();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = creatFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
}
MainActivity修改如下
public class MainActivity extends BaseFragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected Fragment creatFragment(){
retrun new BlankFragment();
}
}
现在看起来我们的MainActivity是不是简练整洁,在实际开发中多使用抽象类会大大的节约你的开发时间,提高效率。
3.使用Fragment argument
在开发中有一个非常普遍你的需求,就是activity与fragmenr之间的值传递。
比如,现在我们需求,从一个fragment跳转到另一新的目标activity中(携带参数),然H后目标Fragment要负责接收这个值。实现代码部分如下:
目标activity
public class TargetActivity extends BaseFragmentActivity {
public static final String EXTRA_UUID = "com.haife.album_master.activities.targetId"
public static Intent newIntent(Context ctx,String targetId){
Intent intent = new Intent(ctx,TargetActivity.class);
intent.putExtra(EXTRA_UUID,targetId);
return intent;
}
}
跳转处
@Override
public void onClick(){
Intent intent = TargetActivity.newInstance(getActivity(),id);
startActivity(intent);
}
到这里,参数id已经被被安全存储在TargetActivity中,然而获取参数和使用的是Fragment类。
fragment中获取extra信息
fragment有俩种方式来获取intent中的数据,一种比较简单直接,另一种复杂灵活的方式是涉及到fragment arguement。我们先看简单的实现方式,在fragment中通过getActivity()方式获取到TargetActivity的intent,返回至TargetFragment类,得到TargetActivity的intent 的extra信息后,在用它获取String id的值。
public class TargetFragment extends Fragment {
public void oncreat(Bundle saveInstanceState){
String id = getActivity().getIntent().getStringExtra(TargetAtivity.EXTRA_UUID);
}
}
继续几行代码,我们就能从托管的activity的intent中获取到信息,然而这种方式会让我们的TargetFragment无法复用,当前只能被用于TargetActivity,如何优化?我们可以将要获取的值保存在TargetFragment的某个地方,而不是在TargetAvcivity的私有空间中,这样,fragment就不用依赖于activity的intent内指定的intent,就能获取自己所需的extra数据。这“某个地方”其实就是fragment argument。
fragment argument
每个Fragment都存在一个Bundle对象,bundle中含有键值对。我们可以类似于附加extra到activity的intent中一样,一个键值对应一个argument。如何附加arguement给fragment呢?我们需要调用Fragment.setArgument(Bundle),注意这个方法必须在fragment创建后调用。andriod开发者通用的做法是在fragment中定一个静态的newInstance()方法,在此方法内完成fragment的实例和Bundle对象的创建,然后把argument放入bundle中附加给fragment。代码如下:
package com.haife.album_master.activities;
import android.os.Bundle;
import android.support.v4.app.Fragment;
/**
* A simple {@link Fragment} subclass.
*/
public class TargetFragment extends Fragment {
private static final String ARG_TARGET_ID = "arg_target_id";
public static BlankFragment newInstance(String id){
Bundle bundle = new Bundle();
bundle.putString(ARG_TARGET_ID,id);
BlankFragment blankFragment = new BlankFragment();
blankFragment.setArguments(bundle);
return blankFragment;
}
}
现在,需要创建fragment的时候,activity要调用BlankFragment.newInstance(String)的方法,而不是向上面那样直接调用构造方法,activity可传任意参数到newInstance()方法,按实际开发需要,这里传的是在activity中extra的值:
public class TargetActivity extends BaseFragmentActivity {
private static final String EXTRA_UUID = "com.haife.album_master.activities.targetId"
@Override
protected Fragment creatFragment(){
String id = getIntent().getStringExtra(EXTRA_UUID);
return TargetFragment.newInstance(id);
}
}
这里将EXTRA_ID改为私有是因为其他类都用不到了。注意的是,activity和fragment不需要也无法同时保持独立性,activity必要要了解fragment 的内部细节。
获取 argument
上面我们已经附加了argument给TargetFragment,接下来我们如何获取到他的argument?
public class TargetFragment extends Fragment {
private static final String ARG_TARGET_ID = "arg_target_id";
private static final String DEFAULT_VALUE = "default_value";
@Override
public void onCreate(Bundle saveInstanceState){
String id = getArguments().getString(ARG_TARGET_ID, DEFAULT_VALUE);
}
}
代码写到这里,虽然比较少,但是很通用。下面是fragment的一点小拓展,如果frament中存在一个列表项(RecycleView),当数据源发生改变时,我们该如何在fragment中刷新它?
public class BlankFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
updateUI();
}
private void updateUI() {
if (mAdapter == null) {
mAdapter = new MyAdapter(userList);
recycle.setAdapter(mAdapter);
}else {
mAdapter.notifyDataSetChanger();
}
}
}
解释一下为什么选择覆盖onResum()方法来刷新列表,而不用onStart(),当有其他activbity位于BlankFragment的宿主activity的之前时,我们无法确定宿主activity是否会被停止,如果前面的activity是透明的,那么在onStart()方法中刷新列表是无效,一般来说,要保证fragment视图得到刷新,在onResum()方法中处理是最安全的。
到这里fragment argument就介绍完了,那为什么要使用它呢?我们为什么不直接在fragment内部创建一个实例变量?想想就知道,当操作系统重建fragment或者用户离开应用,甚至系统回收内存,又或是应用配置发生改变时,所有的实例变量就不复存在了,所以设计fragment argument的本意就是为了上述场景。当然又有人说你可以用实例状态保存下来,然后通过onSaveInstanceState(Bundle)方法存储。这当然也可以,前提是,你回过头来看代码的时候能够记的住~~~
4.通过fragment获取返回结果
fragment与activity,有Fragment.startActivityForResult(...)和Fragment.onActivityResult(...),用法类似于Activity的同名方法.但是,从fragment中返回结果的处理有些不同,fragment能从activity中接受返回结果,其自身是无法持有返回结果的。尽管Fragment有自己的startActivityForResult(...)和onActivityResult(...),却没有setResult(...)方法,相反,我们可以让activity返回结果值。具体代码如下:
public class BlankFragment extends Fragment {
...
public void returnResult(){
getActivity().setResult(Activity.Result_OK,null);
}
...
5. 结尾
最后说点闲话吧,在我们设计应用时,正确的使用fragment是非常重要的,但是我们也不能滥用它。Google设计fragment的本意是封装可复用的组件,这里的组件是指屏幕的组件,单屏上最多使用2-3个fragment是比较好的。其实在选择activity还是UI fragment来管理用户界面的时候,我认为不用考虑太多。能使用fragment实现的时候,就不要去考虑activity了。