2012年8月27日月曜日

Life cycle of view inside Fragment


I could not clearly understand life cycle of fragments which is added to back stack .
So, I created sample program at github.


Fragmentを使おうと思ったのであるけど、Fragment自体をstackさせた時の挙動がどうにも理解しにくかった。
Activityもそうなのであるけど、stackした時のlife cycleについてのドキュメントが無いから、自分で調べないとなんとも言えないのだよなー。
特に、よく分からなかったのはFragmentを含んだActivityがメモリ不足等の理由でシステムによりdestroyされた際にFragment内で作成したViewはdestroyされるっぽいんだけど、Fragmentのインスタンス自体は削除されないし、View自体も実態は削除されていないことが非常に不思議だ。Activityに紐付いたまま。
なので、メモリ不足等でActivityが削除されて、またonCreateがコールされた際にFragmentをnewしてaddするとFragmentがどんどん増えてしまう。


1:  public class MainActivity extends FragmentActivity {  
2:    private static final String TAG = "FragmentTest";  
3:    
4:    @Override  
5:    public void onCreate(Bundle savedInstanceState) {  
6:      Log.v(TAG, "Activity Enter onCreate");  
7:      super.onCreate(savedInstanceState);  
8:      setContentView(R.layout.activity_main);  
9:    
10:      if (savedInstanceState == null) {  
11:        FragmentTransaction ft = this.getSupportFragmentManager().beginTransaction();  
12:        Fragment newFragment = FirstFragment.newInstance("http://www.google.co.jp");  
13:        ft.add(R.id.fragment_container, newFragment, "first");  
14:        ft.commit();  
15:      }  
16:      Button btn = (Button) findViewById(R.id.button);  
17:      btn.setOnClickListener(new OnClickListener() {  
18:        @Override  
19:        public void onClick(View v) {  
20:          FragmentManager fm = MainActivity.this.getSupportFragmentManager();  
21:          FragmentTransaction ft = fm.beginTransaction();  
22:          Fragment newFragment = SecondFragment.newInstance("http://www.goo.ne.jp");  
23:          ft.replace(R.id.fragment_container, newFragment, "second");  
24:          ft.addToBackStack(null);  
25:          ft.commit();  
26:        }  
27:      });  
28:      Log.v(TAG, "Activity Exit onCreate");  
29:    }  

なので、上記の10行目みたいにonCreateが再生成でコールされたと判断できたら、Fragmentを作らないようにしなければいけない。
このようにしておけば、Fragmentが余計に生成されるのを避ける事が出来て納得の行くlife cycleになると思う。

もう一つ分かりにくかったのは、Fragmentをreplace等でstackさせた時にもonDestroyViewがコールされるのだ。それでbackキーで元のFragmentに戻った際に、新しくViewを作りなおさなければいけないところがなんとも釈然としなかった。stackしているだけなのだからonStopまでで良いと思うのだが。。。
しかも、ViewのInstance自体はRootのViewに紐付けられたままで削除されているわけではないので、元々あったViewのインスタンスをActivityは保持したままであるし。


1:  public class FirstFragment extends Fragment {  
2:    private static final String TAG = "FragmentTest";  
3:    private WebView mWebview = null;  
4:    
5:    @Override  
6:    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
7:      Log.d(TAG, "FirstFragment Enter onCreateView");  
8:      if (mWebview != null) return mWebview;  
9:      mWebview = (WebView) inflater.inflate(R.layout.first_fragment, container, false);  
10:      mWebview.setWebViewClient(new WebViewClient());  
11:      String url = "http://www.yahoo.co.jp";  
12:      Bundle args = getArguments();  
13:      if (mLastUrl != null) {  
14:        url = mLastUrl;  
15:      } else if (savedInstanceState != null && savedInstanceState.containsKey("url")) {  
16:        url = savedInstanceState.getString("url");  
17:      } else if (args != null && args.containsKey("url")) {  
18:        url = args.getString("url");  
19:      }  
20:      mWebview.loadUrl(url);  
21:      Log.d(TAG, "FirstFragment Exit onCreateView view = " + mWebview.toString());  
22:      return mWebview;  
23:    }  


などのように、8行目にある感じで、すでにFragment内で保持しているViewを返すとエラーで落ちるし。

FragmentをStackさせる際にreplaceを利用するとonDestroyViewがコールされるの前提でコードを書かなければいけないようだ。どこにもその辺の作法とか無いけれども、確かにサンプルでは必ずViewを新しく生成していますしね。

でも、そうするとWebViewを使った時にとても困る。WebViewを生成してonCreateViewで渡してユーザが操作した後に新しくFragmentを重ねるとViewが破棄されてしまうので、historyが消えてしまう。
historyの仕組みを自分で実装しなければいけなくって非常に面倒くさい。
Stackとかでhistoryをstackすれば良い気もするが、あまりhistoryを独自実装しないほうが良いと思う。stackからurlを取り出してloadUrlするとhistorybackなのにloadが走ってしまうから非効率だと思うし。

結論的にはwebviewを保持しているfragmentの上にはfragmentをstackさせるべきではないということだ。
ただ、WebViewを利用しないのであればFragment内でonSaveInstanceStateが使えて、非常に便利だし、backStackさせてもいい感じにViewが再生成されるので、Fragment自体はとても便利だと思う。

0 件のコメント:

コメントを投稿