105

Android4.0ICSとフラグメントを使用してアプリを開発しています。

ICS 4.0.3(APIレベル15)APIのデモサンプルアプリからのこの変更された例を考えてみましょう。

public class FragmentTabs extends Activity {

private static final String TAG = FragmentTabs.class.getSimpleName();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final ActionBar bar = getActionBar();
    bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    bar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);

    bar.addTab(bar.newTab()
            .setText("Simple")
            .setTabListener(new TabListener<SimpleFragment>(
                    this, "mysimple", SimpleFragment.class)));

    if (savedInstanceState != null) {
        bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
        Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
        Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
    }

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
}

public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;
    private final Bundle mArgs;
    private Fragment mFragment;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        this(activity, tag, clz, null);
    }

    public TabListener(Activity activity, String tag, Class<T> clz, Bundle args) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mArgs = args;

        // Check to see if we already have a fragment for this tab, probably
        // from a previously saved state.  If so, deactivate it, because our
        // initial state is that a tab isn't shown.
        mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
        if (mFragment != null && !mFragment.isDetached()) {
            Log.d(TAG, "constructor: detaching fragment " + mTag);
            FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
            ft.detach(mFragment);
            ft.commit();
        }
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
            Log.d(TAG, "onTabSelected adding fragment " + mTag);
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            Log.d(TAG, "onTabSelected attaching fragment " + mTag);
            ft.attach(mFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            Log.d(TAG, "onTabUnselected detaching fragment " + mTag);
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        Toast.makeText(mActivity, "Reselected!", Toast.LENGTH_SHORT).show();
    }
}

public static class SimpleFragment extends Fragment {
    TextView textView;
    int mNum;

    /**
     * When creating, retrieve this instance's number from its arguments.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(FragmentTabs.TAG, "onCreate " + (savedInstanceState != null ? ("state " + savedInstanceState.getInt("number")) : "no state"));
        if(savedInstanceState != null) {
            mNum = savedInstanceState.getInt("number");
        } else {
            mNum = 25;
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.d(TAG, "onActivityCreated");
        if(savedInstanceState != null) {
            Log.d(TAG, "saved variable number: " + savedInstanceState.getInt("number"));
        }
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        Log.d(TAG, "onSaveInstanceState saving: " + mNum);
        outState.putInt("number", mNum);
        super.onSaveInstanceState(outState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.d(FragmentTabs.TAG, "onCreateView " + (savedInstanceState != null ? ("state: " + savedInstanceState.getInt("number")) : "no state"));
        textView = new TextView(getActivity());
        textView.setText("Hello world: " + mNum);
        textView.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.gallery_thumb));
        return textView;
    }
}

}

この例を実行してから電話を回転させて取得した出力は次のとおりです。

06-11 11:31:42.559: D/FragmentTabs(10726): onTabSelected adding fragment mysimple
06-11 11:31:42.559: D/FragmentTabs(10726): onCreate no state
06-11 11:31:42.559: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:42.567: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.286: D/FragmentTabs(10726): onSaveInstanceState saving: 25
06-11 11:31:45.325: D/FragmentTabs(10726): onCreate state 25
06-11 11:31:45.340: D/FragmentTabs(10726): constructor: detaching fragment mysimple
06-11 11:31:45.340: D/FragmentTabs(10726): onTabSelected attaching fragment mysimple
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate tab: 0
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate number: 0
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView state: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.348: D/FragmentTabs(10726): saved variable number: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated

私の質問は、なぜonCreateViewとonActivityCreatedが2回呼び出されるのですか?1回目は保存された状態のバンドルを使用し、2回目はnullのsavedInstanceStateを使用しますか?

これにより、回転時にフラグメントの状態を保持する際に問題が発生します。

4

5 に答える 5

45

私もしばらくの間これについて頭を悩ませていました、そしてデイブの説明が少し理解するのが難しいので、私は私の(明らかに働いている)コードを投稿します:

private class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mFragment=mActivity.getFragmentManager().findFragmentByTag(mTag);
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.replace(android.R.id.content, mFragment, mTag);
        } else {
            if (mFragment.isDetached()) {
                ft.attach(mFragment);
            }
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
    }
}

ご覧のとおり、コンストラクターでデタッチせず、addの代わりにreplaceを使用することを除けば、Androidのサンプルとほとんど同じです。

多くのヘッドスクラッチと試行錯誤の末、コンストラクターでフラグメントを見つけると、onCreateViewの二重の問題が魔法のように解消されるように見えることがわかりました(ActionBar.setSelectedNavigationItem()パスを介して呼び出されたときにonTabSelectedがnullになると思います状態の保存/復元)。

于 2012-10-07T08:07:28.297 に答える
28

フラグメントが1つしかない単純なアクティビティでも同じ問題が発生しました(フラグメントが置き換えられることがあります)。次に、アクティビティではなく、フラグメント(およびonCreateViewでsavedInstanceStateをチェックする)でのみonSaveInstanceStateを使用することに気付きました。

デバイスターンで、フラグメントを含むアクティビティが再開され、onCreatedが呼び出されます。そこで、必要なフラグメントを添付しました(これは最初の起動時に正しいです)。

デバイスターンで、Androidは最初に表示されていたフラグメントを再作成し、次にフラグメントが添付された包含アクティビティのonCreateを呼び出して、元の表示されたものを置き換えます。

これを回避するために、savedInstanceStateをチェックするようにアクティビティを変更しただけです。

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

if (savedInstanceState != null) {
/**making sure you are not attaching the fragments again as they have 
 been 
 *already added
 **/
 return; 
 }
 else{
  // following code to attach fragment initially
 }

 }

アクティビティのonSaveInstanceStateも上書きしませんでした。

于 2013-09-30T17:52:33.187 に答える
26

わかりました、これが私が見つけたものです。

私が理解していなかったのは、構成の変更が発生したとき(電話が回転したとき)にアクティビティにアタッチされたすべてのフラグメントが再作成され、アクティビティに追加されて戻ることです。(これは理にかなっています)

TabListenerコンストラクターで起こっていたのは、タブが見つかってアクティビティにアタッチされた場合にタブが切り離されたことです。下記参照:

mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
    if (mFragment != null && !mFragment.isDetached()) {
        Log.d(TAG, "constructor: detaching fragment " + mTag);
        FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
        ft.detach(mFragment);
        ft.commit();
    }

アクティビティonCreateの後半で、以前に選択したタブが保存されたインスタンスの状態から選択されました。下記参照:

if (savedInstanceState != null) {
    bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
    Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
    Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
}

タブが選択されると、onTabSelectedコールバックで再接続されます。

public void onTabSelected(Tab tab, FragmentTransaction ft) {
    if (mFragment == null) {
        mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
        Log.d(TAG, "onTabSelected adding fragment " + mTag);
        ft.add(android.R.id.content, mFragment, mTag);
    } else {
        Log.d(TAG, "onTabSelected attaching fragment " + mTag);
        ft.attach(mFragment);
    }
}

アタッチされるフラグメントは、onCreateViewメソッドとonActivityCreatedメソッドへの2番目の呼び出しです。(最初は、システムがアクティビティとすべてのアタッチされたフラグメントを再作成するときです)onSavedInstanceStateバンドルが最初にデータを保存したが、2回目は保存しませんでした。

解決策は、TabListenerコンストラクターでフラグメントをデタッチせず、アタッチしたままにすることです。(FragmentManagerでタグで見つける必要があります)また、onTabSelectedメソッドで、フラグメントをアタッチする前に、フラグメントがデタッチされているかどうかを確認します。このようなもの:

public void onTabSelected(Tab tab, FragmentTransaction ft) {
            if (mFragment == null) {
                mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
                Log.d(TAG, "onTabSelected adding fragment " + mTag);
                ft.add(android.R.id.content, mFragment, mTag);
            } else {

                if(mFragment.isDetached()) {
                    Log.d(TAG, "onTabSelected attaching fragment " + mTag);
                    ft.attach(mFragment);
                } else {
                    Log.d(TAG, "onTabSelected fragment already attached " + mTag);
                }
            }
        }
于 2012-06-12T12:27:15.510 に答える
12

ここでの2つの賛成の回答は、ナビゲーションモードのアクティビティの解決策を示しNAVIGATION_MODE_TABSていますが、。で同じ問題が発生しましたNAVIGATION_MODE_LIST。画面の向きが変わると、フラグメントの状態が不可解に失われる原因になり、非常に面倒でした。ありがたいことに、彼らの有用なコードのおかげで、私はそれを理解することができました。

基本的に、リストナビゲーションを使用するonNavigationItemSelected()場合、アクティビティが作成/再作成されると、好きかどうかに関係なく、自動的に呼び出されます。フラグメントが2回呼び出されるのを防ぐためにonCreateView()、この最初の自動呼び出しonNavigationItemSelected()は、フラグメントがアクティビティ内にすでに存在するかどうかを確認する必要があります。もしそうなら、何もすることがないので、すぐに戻ってください。そうでない場合は、通常どおりにフラグメントを作成してアクティビティに追加します。このチェックを実行すると、フラグメントが不必要に再作成されるのを防ぐことができます。これにより、onCreateView()2回呼び出されます。

以下の私のonNavigationItemSelected()実装を参照してください。

public class MyActivity extends FragmentActivity implements ActionBar.OnNavigationListener
{
    private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item";

    private boolean mIsUserInitiatedNavItemSelection;

    // ... constructor code, etc.

    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState)
    {
        super.onRestoreInstanceState(savedInstanceState);

        if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM))
        {
            getActionBar().setSelectedNavigationItem(savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM));
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState)
    {
        outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, getActionBar().getSelectedNavigationIndex());

        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onNavigationItemSelected(int position, long id)
    {    
        Fragment fragment;
        switch (position)
        {
            // ... choose and construct fragment here
        }

        // is this the automatic (non-user initiated) call to onNavigationItemSelected()
        // that occurs when the activity is created/re-created?
        if (!mIsUserInitiatedNavItemSelection)
        {
            // all subsequent calls to onNavigationItemSelected() won't be automatic
            mIsUserInitiatedNavItemSelection = true;

            // has the same fragment already replaced the container and assumed its id?
            Fragment existingFragment = getSupportFragmentManager().findFragmentById(R.id.container);
            if (existingFragment != null && existingFragment.getClass().equals(fragment.getClass()))
            {
                return true; //nothing to do, because the fragment is already there 
            }
        }

        getSupportFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
        return true;
    }
}

ここからこのソリューションのインスピレーションを借りました。

于 2013-01-12T16:44:18.300 に答える
8

毎回TabListenerをインスタンス化しているためだと私には思えます...したがって、システムはsavedInstanceStateからフラグメントを再作成し、onCreateで再度実行しています。

これをラップして、if(savedInstanceState == null)savedInstanceStateがない場合にのみ起動するようにする必要があります。

于 2012-06-11T16:57:50.947 に答える