Android Fragment传递参数Fragment.setArguments(Bundle bundle)

论坛 期权论坛 编程之家     
选择匿名的用户   2021-6-1 02:08   57   0
Fragment在Android3.0开始提供,并且在兼容包中也提供了Fragment特性的支持。Fragment的推出让我们编写和管理用户界面更快捷更方便了。

但当我们实例化自定义Fragment时,为什么官方推荐Fragment.setArguments(Bundle bundle)这种方式来传递参数,而不推荐通过构造方法直接来传递参数呢?为了弄清这个问题,我们可以做一个测试,分别测试下这两种方式的不同

首先,我们来测试下通过构造方法传递参数的情况

[java] view plain copy
在CODE上查看代码片 派生到我的代码片
  1. public class FramentTestActivity extends ActionBarActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. if (savedInstanceState == null) {
  7. getSupportFragmentManager().beginTransaction()
  8. .add(R.id.container, new TestFragment("param")).commit();
  9. }
  10. }
  11. public static class TestFragment extends Fragment {
  12. private String mArg = "non-param";
  13. public TestFragment() {
  14. Log.i("INFO", "TestFragment non-parameter constructor");
  15. }
  16. public TestFragment(String arg){
  17. mArg = arg;
  18. Log.i("INFO", "TestFragment construct with parameter");
  19. }
  20. @Override
  21. public View onCreateView(LayoutInflater inflater, ViewGroup container,
  22. Bundle savedInstanceState) {
  23. View rootView = inflater.inflate(R.layout.fragment_main, container,
  24. false);
  25. TextView tv = (TextView) rootView.findViewById(R.id.tv);
  26. tv.setText(mArg);
  27. return rootView;
  28. }
  29. }
  30. }

可以看到我们传递过来的数据正确的显示了,现在来考虑一个问题,如果设备配置参数发生变化,这里以横竖屏切换来说明问题,显示如下



发生了什么问题呢?我们传递的参数哪去了?为什么会显示默认值?不急着讨论这个问题,接下来我们来看看Fragment.setArguments(Bundle bundle)这种方式的运行情

[java] view plain copy
在CODE上查看代码片 派生到我的代码片
  1. public class FramentTest2Activity extends ActionBarActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout. activity_main);
  6. if (savedInstanceState == null) {
  7. getSupportFragmentManager().beginTransaction()
  8. .add(R.id. container, TestFragment.newInstance("param")).commit();
  9. }
  10. }
  11. public static class TestFragment extends Fragment {
  12. private static final String ARG = "arg";
  13. public TestFragment() {
  14. Log. i("INFO", "TestFragment non-parameter constructor" );
  15. }
  16. public static Fragment newInstance(String arg){
  17. TestFragment fragment = new TestFragment();
  18. Bundle bundle = new Bundle();
  19. bundle.putString( ARG, arg);
  20. fragment.setArguments(bundle);
  21. return fragment;
  22. }
  23. @Override
  24. public View onCreateView(LayoutInflater inflater, ViewGroup container,
  25. Bundle savedInstanceState) {
  26. View rootView = inflater.inflate(R.layout. fragment_main, container,
  27. false);
  28. TextView tv = (TextView) rootView.findViewById(R.id. tv);
  29. tv.setText(getArguments().getString( ARG));
  30. return rootView;
  31. }
  32. }
  33. }


我们再来看看横竖屏切换后的运行情况



看到了吧,我们传递的参数在横竖屏切换的情况下完好保存了下来,正确的显示给用户
那么这到底是怎么回事呢,我们知道设备横竖屏切换的话,当前展示给用户的Activity默认情况下会重新创建并展现给用户,那依附于Activity的Fragment会进行如何处理呢,我们可以通过源码来查看
先来看看Activity的onCreate(Bundle saveInstance)方法

[java] view plain copy
在CODE上查看代码片 派生到我的代码片
  1. protected void onCreate(Bundle savedInstanceState) {
  2. if (DEBUG_LIFECYCLE ) Slog.v( TAG, "onCreate " + this + ": " + savedInstanceState);
  3. if (mLastNonConfigurationInstances != null) {
  4. mAllLoaderManagers = mLastNonConfigurationInstances .loaders ;
  5. }
  6. if (mActivityInfo .parentActivityName != null) {
  7. if (mActionBar == null) {
  8. mEnableDefaultActionBarUp = true ;
  9. } else {
  10. mActionBar .setDefaultDisplayHomeAsUpEnabled( true);
  11. }
  12. }
  13. if (savedInstanceState != null) {
  14. Parcelable p = savedInstanceState.getParcelable( FRAGMENTS_TAG );
  15. mFragments .restoreAllState(p, mLastNonConfigurationInstances != null
  16. ? mLastNonConfigurationInstances .fragments : null);
  17. }
  18. mFragments .dispatchCreate();
  19. getApplication().dispatchActivityCreated( this , savedInstanceState);
  20. mCalled = true ;
  21. }

由于我们的Fragment是由FragmentManager来管理,所以可以跟进FragmentManager.restoreAllState()方法,通过对当前活动的Fragmnet找到下面的代码块

[html] view plain copy
在CODE上查看代码片 派生到我的代码片
  1. for (int i=0; i<fms.mActive.length; i++) {
  2. FragmentState fs = fms.mActive[i];
  3. if (fs != null) {
  4. Fragment f = fs.instantiate(mActivity, mParent);
  5. if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
  6. mActive.add(f);
  7. // Now that the fragment is instantiated (or came from being
  8. // retained above), clear mInstance in case we end up re-restoring
  9. // from this FragmentState again.
  10. fs.mInstance = null;
  11. } else {
  12. mActive.add(null);
  13. if (mAvailIndices == null) {
  14. mAvailIndices = new ArrayList<Integer>();
  15. }
  16. if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i);
  17. mAvailIndices.add(i);
  18. }
  19. }

接下来我们可以看看FragmentState.instantitate()方法的实现

[html] view plain copy
在CODE上查看代码片 派生到我的代码片
  1. public Fragment instantiate(Activity activity, Fragment parent) {
  2. if (mInstance != null) {
  3. return mInstance ;
  4. }
  5. if (mArguments != null) {
  6. mArguments .setClassLoader(activity.getClassLoader());
  7. }
  8. mInstance = Fragment.instantiate(activity, mClassName , mArguments );
  9. if (mSavedFragmentState != null) {
  10. mSavedFragmentState .setClassLoader(activity.getClassLoader());
  11. mInstance .mSavedFragmentState = mSavedFragmentState ;
  12. }
  13. mInstance .setIndex(mIndex , parent);
  14. mInstance .mFromLayout = mFromLayout ;
  15. mInstance .mRestored = true;
  16. mInstance .mFragmentId = mFragmentId ;
  17. mInstance .mContainerId = mContainerId ;
  18. mInstance .mTag = mTag ;
  19. mInstance .mRetainInstance = mRetainInstance ;
  20. mInstance .mDetached = mDetached ;
  21. mInstance .mFragmentManager = activity.mFragments;
  22. if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG,
  23. "Instantiated fragment " + mInstance );
  24. return mInstance ;
  25. }

可以看到最终转入到Fragment.instantitate()方法

[java] view plain copy
在CODE上查看代码片 派生到我的代码片
  1. public static Fragment instantiate(Context context, String fname, Bundle args) {
  2. try {
  3. Class<?> clazz = sClassMap .get(fname);
  4. if (clazz == null) {
  5. // Class not found in the cache, see if it's real, and try to add it
  6. clazz = context.getClassLoader().loadClass(fname);
  7. sClassMap .put(fname, clazz);
  8. }
  9. Fragment f = (Fragment)clazz.newInstance();
  10. if (args != null) {
  11. args.setClassLoader(f.getClass().getClassLoader());
  12. f. mArguments = args;
  13. }
  14. return f;
  15. } catch (ClassNotFoundException e) {
  16. throw new InstantiationException( "Unable to instantiate fragment " + fname
  17. + ": make sure class name exists, is public, and has an"
  18. + " empty constructor that is public" , e);
  19. } catch (java.lang.InstantiationException e) {
  20. throw new InstantiationException( "Unable to instantiate fragment " + fname
  21. + ": make sure class name exists, is public, and has an"
  22. + " empty constructor that is public" , e);
  23. } catch (IllegalAccessException e) {
  24. throw new InstantiationException( "Unable to instantiate fragment " + fname
  25. + ": make sure class name exists, is public, and has an"
  26. + " empty constructor that is public" , e);
  27. }

通过此方法可以看到,最终会通过反射无参构造实例化一个新的Fragment,并且给mArgments初始化为原先的值,而原来的Fragment实例的数据都丢失了,并重新进行了初始化

通过上面的分析,我们可以知道Activity重新创建时,会重新构建它所管理的Fragment,原先的Fragment的字段值将会全部丢失,但是通过Fragment.setArguments(Bundle bundle)方法设置的bundle会保留下来。所以尽量使用Fragment.setArguments(Bundle bundle)方式来传递参数

延伸阅读: android中方便为fragment写入参数的FragmentArgs简介

Android开发有时候会令人头痛。你不得不为诸如建立fragment这样简单的事情写很多代码。幸运的是java支持一个强大的工具:注释处理器(Annotation Processors)。

Fragment的问题是你不得不设置很多参数,从而让它正常运行。很多android开发新手通常这样写:

01 public class MyFragment extends Fragment
02 {
03 private int id;
04 private String title;
05
06 public static MyFragment newInstance(int id, String title)
07 {
08 MyFragment f = new MyFragment();
09 f.id = id;
10 f.title = title;
11 return f;
12 }
13
14 @Override
15 public View onCreateView(LayoutInflater inflater, ViewGroup container,
16 Bundle savedInstanceState)
17 {
18 Toast.makeText(getActivity(), "Hello " + title.substring(0, 3),
19 Toast.LENGTH_SHORT).show();
20 }
21 }

这样做怎么了?我已经在自己的设备上尝试过了,它很好用?

它的却能工作,但是你有没有试过把你的设备从竖向改为横向?你的app将会因为NullPointerException而崩溃,当你试图访问id或title时。

我的app是正常的,因为我把app设置为竖向。所以我从来没遇到过这个问题。

随便你!Android是一个真正的多任务操作系统。多个app在同一时间运行,同时如果需要内存android系统将会销毁activity(和其中包含的fragment)。可能你在日常的app开发中不会注意这些问题。然而,当你在play store中发布后,你将会注意到你的app崩溃了,但你不知道什么原因。使用你app的用户可能同时间使用多个app,很有可能你的app在后台被销毁了。例如:A 用户打开你的app,MyFragment在屏幕上显示。下一步你的用户按了home键(这是你的app在后台运行),并且打开了其它应用。你的app可能会因为释放内存而被销毁。之后,用户返回你的app,例如通过多任务按钮。所以,Android现在会怎么做?Android会恢复之前的app状态,同时恢复MyFragment,这就是问题所在。Fragment试图访问title,但title是null,因为它不是被永久保存的。

我知道了,所以我需要把它们保存在onSaveInstanceState(Bundle)中?

不是。官方的文档有一些不清楚,但是onSaveInstanceState(Bundle)的使用方法应该跟你用Activity.onSaveInstanceState(Bundle)一样:你使用这个方法保存实例的“临时”状态,例如去处理屏幕的方向(从竖向到横向,反之亦然)。所以说当app在后台被杀掉时fragment的实例状态并不能被保存成持久数据,它的作用是再一次返回前台时恢复数据。它的作用跟Activity.onSaveInstanceState(Bundle)在Activity中的作用相同,它们用于“临时”保存实例状态。然而,持久的参数是通过intent外部数据传输的。

所以我应该在Activity中得Intent保存Fragment的参数?

不需要,Fragment有它自己的机制。有两个方法:Fragment.setArguments(Bundle)和Fragment.getArguments(),你必须通过这两个方法来确保参数被持久保存。这就是我上面提到的痛苦之处。需要有大量的代码这样写。第一,你要创建一个Bundle,然后你需要放入键值对,最后调用Fragment.setArguments()。不幸的是,你的工作还没有结束,你必须通过Fragment.getArguments()来读出Bundle。一些这样的工作:

01 public class MyFragment extends Fragment
02 {
03 private static String KEY_ID = "key.id";
04 private static String KEY_TITLE = "key.title";
05 private int id;
06 private String title;
07
08 public static MyFragment newInstance(int id, String title)
09 {
10 MyFragment f = new MyFragment();
11 Bundle b = new Bundle();
12 b.putInt(KEY_ID, id);
13 b.putString(KEY_TITLE, title);
14 f.setArguments(b);
15 return f;
16 }
17
18 @Override
19 public void onCreate(Bundle savedInstanceState)
20 {
21 // onCreate it's a good point to read the arguments
22 Bundle b = getArguments();
23 this.id = b.getInt(KEY_ID);
24 this.title = b.getString(KEY_TITLE);
25 }
26
27 @Override
28 public View onCreate(LayoutInflater inflater, ViewGroup container,
29 Bundle savedInstanceState)
30 {
31 // No NullPointer here, because onCreate() is called before this
32 Toast.makeText(getActivity(), "Hello " + title.substring(0, 3),
33 Toast.LENGTH_SHORT).show();
34 }
35 }
我希望你现在能明白我所说的“痛苦”。在你的应用中你将会为每一个fragment写很多代码。如果有人为你写这样的代码,这将不让人满意。注释处理允许你在编译的时候生成java代码。注意我们并不是在讨论评价在运行时间使用反射的注释。

FragmentArgs是一个轻量的包,用于为你的fragment生成精确的java代码。

01 import com.hannesdorfmann.fragmentargs.FragmentArgs;
02 import com.hannesdorfmann.fragmentargs.annotation.Arg;
03
04 public class MyFragment extends Fragment
05 {
06 @Arg
07 int id;
08 @Arg
09 String title;
10
11 @Override
12 public void onCreate(Bundle savedInstanceState)
13 {
14 super.onCreate(savedInstanceState);
15 FragmentArgs.inject(this); // read @Arg fields
16 }
17
18 @Override
19 public View onCreateView(LayoutInflater inflater, ViewGroup container,
20 Bundle savedInstanceState)
21 {
22 Toast.makeText(getActivity(), "Hello " + title, Toast.LENGTH_SHORT)
23 .show();
24 }
25 }

只需要在你的Fragment类中加入注释字段,FragmentArgs就会生成引用代码。在你的Activity中你将使用生成的Builder类(你的fragment的后缀是”Builder”),而不是使用new MyFragment()或静态的MyFragment.newInstance(int id,String title)方法。

例如:

01 public class MyActivity extends Activity
02 {
03 public void onCreate(Bundle savedInstanceState)
04 {
05 super.onCreate(savedInstanceState);
06 int id = 123;
07 String title = "test"; // Using the generated Builder
08 Fragment fragment = new MyFragmentBuilder(id, title).build(); // Fragment Transaction
09 getFragmentManager().beginTransaction().replace(R.id.container,fragment).commit();
10 }
11 }

你可能已经注意到在Fragment.onCreate(Bundle)中声明的FragmentArgs.inject(this)。这个调用使你的fragment获得了生成代码的连接。你可能会问你自己:“我需不需要在每一个Fragment中的onCreate(Bundle)中加入inject()方法”。答案是no。你只需要在你的fragment基类中插入这一句就可以,并且在所有的fragment中继承它。

01 public class BaseFragment extends Fragment
02 {
03 @Override
04 public void onCreate(Bundle savedInstanceState)
05 {
06 super.onCreate(savedInstanceState);
07 FragmentArgs.inject(this); // read @Arg fields
08 }
09 }
10
11 public class MyFragment extends BaseFragment
12 {
13 @Arg
14 String title;
15
16 @Override
17 public View onCreateView(LayoutInflater inflater, ViewGroup container,
18 Bundle savedInstanceState)
19 {
20 Toast.makeText(getActivity(), "Hello " + title, Toast.LENGTH_SHORT)
21 .show();
22 }
23 }

信用:一部分的注释生成代码是基于Hugo Visser的Bundles项目。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP