Fragment onCreateView และ onActivityCreated เรียกสองครั้ง


101

ฉันกำลังพัฒนาแอปโดยใช้ Android 4.0 ICS และแฟรกเมนต์

พิจารณาตัวอย่างที่แก้ไขนี้จากแอปตัวอย่างของ ICS 4.0.3 (API ระดับ 15):

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 จึงถูกเรียกสองครั้ง ครั้งแรกกับ Bundle ที่มีสถานะที่บันทึกไว้และครั้งที่สองที่มีค่า null ที่บันทึกไว้

สิ่งนี้ทำให้เกิดปัญหากับการรักษาสถานะของชิ้นส่วนในการหมุน


2
ฉันคิดว่าคำถามนี้อาจเกี่ยวข้องกับstackoverflow.com/a/8678705/404395
marioosh

คำตอบ:


45

ฉันเกาหัวของฉันเกี่ยวกับเรื่องนี้มาระยะหนึ่งแล้วและเนื่องจากคำอธิบายของ Dave นั้นยากที่จะเข้าใจฉันจึงโพสต์รหัส (ดูเหมือนใช้งานได้) ของฉัน:

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) {
    }
}

อย่างที่คุณเห็นมันค่อนข้างเหมือนตัวอย่าง Android นอกเหนือจากการไม่แยกตัวสร้างและการใช้ไฟล์ แทนที่แทนการเพิ่ม

หลังจาก headscratching และการลองผิดลองถูกมากฉันพบว่าการค้นหาส่วนในตัวสร้างดูเหมือนว่าจะทำให้ปัญหา onCreateView สองครั้งหายไปอย่างน่าอัศจรรย์ (ฉันคิดว่ามันเป็นเพียงค่าว่างสำหรับ onTabSelected เมื่อเรียกผ่านเส้นทาง ActionBar.setSelectedNavigationItem () เมื่อ ประหยัด / กู้คืนสถานะ)


ใช้งานได้ดีเยี่ยม! คุณช่วยให้ฉันนอนหลับในคืนนี้! ขอบคุณ :)
jaibatrik

คุณยังสามารถใช้ fragment.getClass (). getName () หากคุณต้องการลบตัวแปรคลาสและลบพารามิเตอร์ออกจากการโทร
Ben Sewards

ทำงานได้อย่างสมบูรณ์แบบกับตัวอย่าง Android "อ้างอิงก่อนหน้า TabListener" - tnx "TabListener ref. sample" ใหม่ล่าสุดของ Android [ณ วันที่ 4 ix 2013] นั้นผิดจริงๆ
Grzegorz Dev

ft.commit () เรียก method อยู่ที่ไหน ??
MSaudi

1
@MuhammadBabar ดูstackoverflow.com/questions/23248789/… . ถ้าคุณใช้addแทนและหน้าจอหมุนคุณจะมีชิ้นส่วนจำนวนมากreplace onCreateView()
CoolMind

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 (สิ่งแรกที่เกิดขึ้นเมื่อระบบกำลังสร้างความไวแสงและชิ้นส่วนที่แนบทั้งหมด) ครั้งแรก onSavedInstanceState Bundle จะบันทึกข้อมูลไว้ แต่ไม่ใช่ครั้งที่สอง

วิธีแก้ปัญหาคืออย่าถอดชิ้นส่วนในตัวสร้าง 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);
                }
            }
        }

4
โซลูชัน "not to detach the fragment in the TabListener constructor" ทำให้ส่วนแท็บทับซ้อนกัน ฉันสามารถดูเนื้อหาชิ้นส่วนอื่น ๆ มันไม่ได้ผลสำหรับฉัน
Aksel Fatih

@ flock.dux ฉันไม่แน่ใจว่าคุณหมายถึงอะไรที่ซ้อนทับกัน Android จะดูแลวิธีการจัดวางเราเพียงแค่ระบุแนบหรือแยกออก จะต้องมีมากกว่านี้เกิดขึ้น บางทีถ้าคุณถามคำถามใหม่โดยใช้โค้ดตัวอย่างเราก็จะทราบได้ว่าเกิดอะไรขึ้นกับคุณ
Dave

1
ฉันมีปัญหาเดียวกัน (ตัวสร้างส่วนย่อยหลายตัวเรียกใช้จาก Android) การค้นพบของคุณช่วยแก้ปัญหาของฉัน: สิ่งที่ฉันไม่เข้าใจคือชิ้นส่วนทั้งหมดที่เชื่อมโยงกับกิจกรรมเมื่อมีการเปลี่ยนแปลงการกำหนดค่า (โทรศัพท์หมุน) จะถูกสร้างขึ้นใหม่และเพิ่มกลับไปที่กิจกรรม (ซึ่งสมเหตุสมผล)
eugene

26

ฉันมีปัญหาเดียวกันกับกิจกรรมง่ายๆที่มีชิ้นส่วนเพียงชิ้นเดียว (ซึ่งจะถูกแทนที่ในบางครั้ง) จากนั้นฉันก็รู้ว่าฉันใช้ onSaveInstanceState เฉพาะในแฟรกเมนต์ (และ onCreateView เพื่อตรวจสอบ SavedInstanceState) ไม่ใช่ในกิจกรรม

บนอุปกรณ์เปิดกิจกรรมที่มีชิ้นส่วนเริ่มต้นใหม่และเรียกว่า onCreated ฉันได้แนบส่วนที่จำเป็น (ซึ่งถูกต้องในการเริ่มครั้งแรก)

บนอุปกรณ์ให้ Android สร้างส่วนที่มองเห็นขึ้นมาใหม่เป็นครั้งแรกจากนั้นเรียกว่า onCreate of the contain activity ที่มีการต่อ Fragment ซึ่งจะแทนที่ส่วนที่มองเห็นได้ดั้งเดิม

เพื่อหลีกเลี่ยงไม่ให้ฉันเปลี่ยนกิจกรรมของฉันเพื่อตรวจสอบ saveInstanceState:

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 ของกิจกรรมด้วยซ้ำ


ขอบคุณ. ช่วยฉันด้วย AppCompatActivity + PreferenceFragmentCompat และหยุดทำงานในขณะที่แสดงกล่องโต้ตอบในส่วนที่ต้องการหลังจากเปลี่ยนการวางแนวเนื่องจากตัวจัดการส่วนเป็นโมฆะในการสร้างส่วนที่สอง
RoK

12

คำตอบที่ได้รับการโหวตสองรายการที่นี่แสดงวิธีแก้ปัญหาสำหรับกิจกรรมที่มีโหมดการนำทางNAVIGATION_MODE_TABSแต่ฉันมีปัญหาเดียวกันกับไฟล์NAVIGATION_MODE_LIST. มันทำให้ Fragments ของฉันสูญเสียสถานะอย่างลึกลับเมื่อการวางแนวหน้าจอเปลี่ยนไปซึ่งน่ารำคาญมาก โชคดีที่รหัสที่เป็นประโยชน์ของพวกเขาทำให้ฉันสามารถคิดออก

โดยทั่วไปเมื่อใช้การนำทางรายการ `` onNavigationItemSelected () is automatically called when your activity is created/re-created, whether you like it or not. To prevent your Fragment'sonCreateView () from being called twice, this initial automatic call toonNavigationItemSelected () should check whether the Fragment is already in existence inside your Activity. If it is, return immediately, because there is nothing to do; if it isn't, then simply construct the Fragment and add it to the Activity like you normally would. Performing this check prevents your Fragment from needlessly being created again, which is what causesonCreateView () `จะถูกเรียกสองครั้ง!

ดู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;
    }
}

ผมยืมแรงบันดาลใจสำหรับการแก้ปัญหานี้จากที่นี่


วิธีนี้ใช้ได้กับปัญหาที่คล้ายกันของฉันกับลิ้นชักการนำทาง ฉันค้นหาแฟรกเมนต์ที่มีอยู่ตาม ID และตรวจสอบว่ามีคลาสเดียวกันกับแฟรกเมนต์ใหม่หรือไม่ก่อนที่จะสร้างใหม่
William

8

สำหรับฉันดูเหมือนว่าเป็นเพราะคุณกำลังสร้างอินสแตนซ์ TabListener ของคุณทุกครั้ง ... ดังนั้นระบบจึงสร้างส่วนของคุณขึ้นมาใหม่จาก SavedInstanceState จากนั้นคุณจะทำอีกครั้งใน onCreate ของคุณ

คุณควรรวมสิ่งif(savedInstanceState == null)นั้นไว้เพื่อให้มันเริ่มทำงานก็ต่อเมื่อไม่มี saveInstanceState


ฉันไม่คิดว่าถูกต้อง เมื่อฉันรวมโค้ด addTab ของฉันในบล็อก if แฟรกเมนต์จะแนบกับกิจกรรม แต่ไม่มีแท็บ ดูเหมือนว่าคุณต้องเพิ่มแท็บทุกครั้งในเมธอด onCreate ฉันจะตรวจสอบเรื่องนี้ต่อไปและโพสต์เพิ่มเติมเมื่อเข้าใจดีขึ้น
Dave
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.