2012年4月16日月曜日

Android Fragment で setArguments() してるサンプルが多いのはなぜ?

Fragment のサンプルでは、setArguments() を使って Bundle を介して値を渡している例を多く見かけます。

  1. HogeFragment f = new HogeFragment();  
  2. Bundle args = new Bundle();  
  3. args.putInt("num", num);  
  4. f.setArguments(args);  
とやるより

  1. HogeFragment f = new HogeFragment(num);  


  1. HogeFragment f = new HogeFragment();  
  2. f.setNum(num);  
とかやった方がいいんじゃない? Arguments 介するのは面倒じゃない?なにがいいの? と思う人も多いのではないでしょうか。

そこで、Arguments がどういいのかを説明したいと思います。


1. Fragment のコンストラクタで引数を渡すのはダメ

Fragment のリファレンス に書いてあるように、

All subclasses of Fragment must include a public empty constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the empty constructor is not available, a runtime exception will occur in some cases during state restore.


Fragment を継承したクラスは空のコンストラクタを用意しないと行けません。メモリが足りなくなったときに Activity スタック内の(バックグラウンドの)Activity が破棄されるように、Fragment もメモリが足りなくなったときは破棄されます。破棄された Fragment が再び必要になったときにシステムは空のコンストラクタから Fragment のインスタンスを再生成します。
*そのため、空のインスタンスの用意されていない Fragment が上記の状態に遭遇するとアプリが落ちます。


2. setter では再生成時に値を渡せない

上記の再生成時に使われるのが Fragment#instantiate(Context context, String fname, Bundle args) メソッドです。 このメソッドは引数で渡されたクラス名の Fragment の空のコンストラクタを呼び出してインスタンスを生成し、第3引数でセットした Bundle を setArguments() でセットしてから返します。

つまり、② だと、再生成されたときに引数に num を取るコンストラクタが呼ばれないし、③ だと、再生成されたときに setter が呼ばれないため num を Fragment に渡せません。 しかし、① であれば、HogeFragment が破棄されるときにシステムが getArguments() で num の値を Bundle に保存して再生成のときにその Bundle を setArguemnts() でセットしてくれるので、再生成時も num の値を HogeFragment に渡すことができるのです。


この再生成時に値を渡せないという問題は DialogFragment ではより顕著になります。
例えば、次のようにボタンを押したらダイアログが表示されるというコードがあるとします。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="fill_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <Button  
  8.         android:id="@+id/button"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="show dialog" />  
  12.   
  13. </LinearLayout>  
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.main);  
  7.           
  8.         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {  
  9.               
  10.             @Override  
  11.             public void onClick(View v) {  
  12.                 showDialog();  
  13.             }  
  14.         });  
  15.     }  
  16.       
  17.       
  18.     void showDialog() {  
  19.         MyAlertDialogFragment newFragment = MyAlertDialogFragment.newInstance(R.string.hello);  
  20.         newFragment.setNum(10);  
  21.         newFragment.show(getFragmentManager(), "dialog");  
  22.     }  
  23.   
  24.     public static class MyAlertDialogFragment extends DialogFragment {  
  25.   
  26.         private int mNum;  
  27.   
  28.         public static MyAlertDialogFragment newInstance(int title) {  
  29.             MyAlertDialogFragment frag = new MyAlertDialogFragment();  
  30.             Bundle args = new Bundle();  
  31.             args.putInt("title", title);  
  32.             frag.setArguments(args);  
  33.             return frag;  
  34.         }  
  35.   
  36.         public void setNum(int num) {  
  37.             mNum = num;  
  38.         }  
  39.   
  40.         @Override  
  41.         public Dialog onCreateDialog(Bundle savedInstanceState) {  
  42.             int title = getArguments().getInt("title");  
  43.   
  44.             return new AlertDialog.Builder(getActivity())  
  45.                 .setIcon(android.R.drawable.ic_dialog_alert)  
  46.                 .setTitle(title)  
  47.                 .setMessage(mNum + "")  
  48.                 .setPositiveButton(android.R.string.ok, null)  
  49.                 .setNegativeButton(android.R.string.cancel, null)  
  50.                 .create();  
  51.         }  
  52.     }  
  53. }  
DialogFragment を使うと、ダイアログが表示されている状態で Activity が再生成された場合に自動でダイアログも再表示してくれます。

例えば、上記のコードでボタンを押してダイアログが表示された状態で画面を回転させると新しく onCreate() が呼ばれますが、ボタンは押されてないので本来はダイアログも消えてしまいます。しかし Dialog ではなく DialogFragment を使っているのでシステムが DialogFragment を再生成して再び表示してくれるのです。

この際の再生成は上記コードの showDialog() を通らず、空のコンストラクタから生成されるため setNum() は呼ばれません。よって回転させると、Arguments としてセットしたタイトルは回転前と同じですが、メッセージ(mNum の値)は 10 ではなく 0 になります。


このように、Fragment はシステムから再生成されることが多いので、setter を使うよりも Arguments を介したほうが最終的にいろいろ楽になります。
ということで、オレオレ setter ではなく Arguemts を使うようにしましょう!



0 件のコメント:

コメントを投稿