まちがいさがし

スマホやガジェットについてなど。技術系メモ以外を中心に。

FragmentにFragmentをつけるときはActivityのFragmentManagerじゃない方がいい

今日、とても悩んで時間を無駄にしてしまいました。。。

Fragmentの下にFragmentをつけるとき、
tab切り替えなどでonCreateViewが毎回呼ばれる気がします。

Fragmentの二段重ね

だいぶ前のはやりに追いつこうと、
今日、タブの下にViewPagerをつけてさらにFragmentを切り替えしようと思いました。

  • FragmentActivity(親)
  • Tab(ActionBarの)
    • Fragment(子)
    • ViewPager (FragmentPagerAdapter)
      • Fragment(孫)

そうするとFragmentActivityの下でのFragmentの切り替えは普通にonAttachからonDetachまで呼ばれるのですが、その下のFragmentの切り替えはonCreateが全然呼ばれません。。。
これはタブとViewPagerの階層が違うってViewPagerやFragmentPagerAdapterがちゃんと消せていないせいかと思いました。
結果的にはその通りっぽいのですが…

ViewPagerのせい?

ViewPagerが何やっているのか分からず、しかも普通に①階層で操作していても
たまにしか子FragmentのonCreateViewが呼ばれません。

いろいろ調べると
FragmentPagerAdapterとListFragmentを使ったらはまった - りふぉんろぐ。
のようにどうやら隣のFragmentも一度にonCreateViewしてくれてるみたいです。
そしてある程度離れてからじゃないとdestroyされない様です。

Fragmentのせい?

今回はまだsavedInstanceなどViewPagerの位置を保存する機構を入れていませんでした。
このタイミングだし実装しよう、とおもって
MyFragmentに

@Override
public void onSaveInstanceState(Bundle outState) {
    Log.e(TAG, "onsaveinstancestate");
     outState.putInt("position", mViewPager.getCurrentItem());
}

見たく書いてみました。

でも、いつまでたってもonSaveInstanceStateが呼ばれません。
で、調べてみると、、、
Fragments | Android Developers
にはちゃんと呼ばれるっぽいことが書いてありますが、
android - savedInstanceState when restoring fragment from back stack - Stack Overflow
みたく、呼ばれていなくて困っているひともいるみたいです。
↑のリンク先での回答をみると
「FragmentはActivityみたいに後ろにいてまってるわけじゃないから切り替えたらもう一回作り直しますよ。
だから状態保存はonSaveInstanceStateじゃなくてメンバー変数使いなさい」
みたいに書いてあります。

おー確かに。。

でも位置を保存せずに毎回0のpositionにするようにしても一階層だとうまく動くのに
二階層だと孫FragmentのonCreateViewが呼ばれません。。

やっぱりViewPagerやFragmentPagerAdapterのせい?

ViewPagerとFragmentPagerAdapterの組合せはFragmentを
View見たくキャッシュしてくれちゃうのでやっぱりこれのせいかなと思いました。
android - support FragmentPagerAdapter holds reference to old fragments - Stack Overflow
のように手動での更新がされなくて困っているひとがいるみたいでした。

それでいろいろOverrideしたり無理やりキャッシュをみたり見なかったりしたり
一番最初のリンクにあるようにsetAdapterでnull入れてクラッシュしたり。。

さらに子FragmentのonAttachで親Activityをrecreateしたら無限ループしたりw(楽しいですね)
ここでもしかしたらActivityが生きてるせいじゃないかと思いました。。

良く見たらActivityのインスタンスからAdapterつくってた。

Adapterのポインタが毎回新しくなってるのに
ViewPagerで移動しても一回破棄されたことにならないとFragmentがnewされない。。
ということに気がつきました。
そもそもViewPagerも子FragmentもPagerAdapterもnewされてるのに
孫Fragmentだけnewされないところがおかしいそうです。。
孫FragmentをnewしてるのはPagerAdapterだ、自分の実装がいけないのかな。
うまいわけはないけれどPagerAdapterのgetItemがそもそもよばれていませんでした。

そこでPagerAdapterってどうやってnewしてるんだっけ?と思ってみてみました。
といっても
Android Tips #31 ViewPager で Fragment を使う | Developers.IO
のをFragmentに置き換えただけでした。

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.i(TAG, "oncreateview," + mViewPager + ", " + mPagerAdapter);
    super.onCreate(savedInstanceState);

    View view = inflater.inflate(R.layout.viewpager_fragment, container, false);
    MyFragmentPagerAdapter adapter = new MyFragmentPagerAdapter(getActivity().getSupportFragmentManager());
    ViewPager pager = (ViewPager) view.findViewById(R.id.myViewPager);

    // PagerAdapterにいろいろつっこんで

    pager.setAdapter(adapter);

    return view;
}

子FragmentもPagerAdapterもViewPagerも新しいのに。。。
でもFragment#getActivity()で帰ってくるActivityっていっつも同じ。。。!!
そしたら、、もしかして、、getFragmentManagerがかえすFragmentManagerもおんなじ。。。?
っ!。。。。

そこでFragment#getChildFragmentManager()にかえてみました。。
こんなものあるんですね。
ちゃんと読んでいないのがばればれ。。

View view = inflater.inflate(R.layout.viewpager_fragment, container, false);
mPagerAdapter = new MusicPagerAdapter(this.getChildFragmentManager());
mViewPager = (ViewPager) view.findViewById(R.id.musicPager);

これでうまくいきました!

ここまで時間かかりました…

まとめ

  • 参考にしたものはそのままコピペではなく状況に合ってるかチェック。。
  • 誰かのせいにしないでちゃんとReferenceよもう

そもそも使ってるAdapterとかちゃんと理解していないのが
問題に気付くのに時間がかかった原因ではないかと。。