FragmentをonCreateで何も考えずにaddして増殖するケースが後を絶たない件

  • 61
    いいね
  • 4
    コメント

連続して3つの現場で同じバグが発生しているアプリを直すという経験をしたので、おそらくかなり多発している気がするこのような不幸が発生することが減るように書き残しておきます。

Fragmentのaddを何も考えずonCreateに書くとActivityのリストア時にリークする

例えば以下のようなごく単純なサンプルアプリ。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.container, new MainFragment())
                .commit();
    }
}
public class MainFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_main, null);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.d("test", "createFragment");
    }

    @Override
    public void onDestroyView() {
        Log.d("test", "destroyFragment");
        super.onDestroyView();
    }
}

これを「Activityを保持しない」設定をONにした状態で動かしてみる。

1 アプリ起動

08-12 19:54:18.035 3635-3635/com.example.kirimin.myapplication D/test: createFragment

2 ホーム画面に戻り、アプリを再度開く

08-12 19:55:25.383 3635-3635/com.example.kirimin.myapplication D/test: destroyFragment
08-12 19:55:28.902 3635-3635/com.example.kirimin.myapplication D/test: createFragment
08-12 19:55:28.902 3635-3635/com.example.kirimin.myapplication D/test: createFragment

3 念のためもう一度...

08-12 19:55:56.400 3635-3635/com.example.kirimin.myapplication D/test: destroyFragment
08-12 19:55:56.402 3635-3635/com.example.kirimin.myapplication D/test: destroyFragment
08-12 19:55:58.515 3635-3635/com.example.kirimin.myapplication D/test: createFragment
08-12 19:55:58.515 3635-3635/com.example.kirimin.myapplication D/test: createFragment
08-12 19:55:58.516 3635-3635/com.example.kirimin.myapplication D/test: createFragment

はい。
Fragmentの初期化時にAPIを叩いていたりするとちょっとした悲劇です。

何が起こっているか

Activityがリストアされる度にAcrtivityのonCreateが呼ばれるため、新しいFragmentがcontainerにaddされるが、古いFragmentもそのまま残っているため見えないフラグメントが裏にどんどん溜まってゆく。

どうすればよいか

addではなくreplaceを使用する

add()の代わりにreplace()メソッドを使用すればとりあえず無限増殖は防ぐ事が出来る。
ただしこの場合、どうも古いFragmentが破棄されるよりも先にonCreateViewやonViewCreatedが走ってしまうようで、以下のようにActivityのリストア時に二回Fragmentの初期化処理が走ってしまう。

08-12 20:05:59.984 4463-4463/com.example.kirimin.myapplication D/test: destroyFragment
08-12 20:06:01.273 4463-4463/com.example.kirimin.myapplication D/test: createFragment
08-12 20:06:01.275 4463-4463/com.example.kirimin.myapplication D/test: createFragment
08-12 20:06:01.276 4463-4463/com.example.kirimin.myapplication D/test: destroyFragment

onSavedInstanceStateがnullじゃない場合にはaddを行わない

Activityを以下のように書き換えてあげるとリストア時のonCreateではfragmentのaddを行わなくなり、Fragment増殖問題は解決する。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.container, new MainFragment())
                    .commit();
        }
    }
}


正しく一つのFragmentがリストアされ再利用されている。

08-12 20:08:40.134 4568-4568/com.example.kirimin.myapplication D/test: createFragment
08-12 20:08:45.357 4568-4568/com.example.kirimin.myapplication D/test: destroyFragment
08-12 20:08:46.435 4568-4568/com.example.kirimin.myapplication D/test: createFragment

他の方法

どう書くのがスマートなんですかね。。。