ViewPager และแฟรกเมนต์ - วิธีที่ถูกต้องในการจัดเก็บสถานะของแฟรกเมนต์คืออะไร


492

แฟรกเมนต์ดูเหมือนจะดีมากสำหรับการแยกตรรกะ UI ออกเป็นโมดูล แต่ตลอดViewPagerจนวงจรชีวิตของมันยังคงเป็นสิ่งที่ผิดสำหรับฉัน ดังนั้นความคิดของปราชญ์จึงเป็นสิ่งจำเป็นอย่างยิ่ง!

แก้ไข

ดูวิธีแก้ใบ้ด้านล่าง ;-)

ขอบเขต

กิจกรรมหลักมีViewPagerชิ้นส่วนด้วย แฟรกเมนต์เหล่านั้นสามารถใช้ตรรกะที่แตกต่างกันเล็กน้อยสำหรับกิจกรรมอื่น ๆ (ส่ง) ดังนั้นข้อมูลของแฟรกเมนต์จะถูกกรอกผ่านอินเตอร์เฟสการติดต่อกลับภายในกิจกรรม และทุกอย่างทำงานได้ดีในการเปิดตัวครั้งแรก แต่! ...

ปัญหา

เมื่อกิจกรรมถูกสร้างขึ้นใหม่ (เช่นการเปลี่ยนการวางแนว) ให้ทำViewPagerเศษของ รหัส (คุณจะพบด้านล่าง) บอกว่าทุกครั้งที่มีการสร้างกิจกรรมฉันพยายามที่จะสร้างViewPagerอะแดปเตอร์ชิ้นส่วนใหม่เช่นเดียวกับชิ้นส่วน (อาจเป็นปัญหา) แต่ FragmentManager ได้เก็บชิ้นส่วนเหล่านี้ไว้ที่ไหนซักแห่ง (ที่ไหน?) และ เริ่มกลไกการนันทนาการสำหรับผู้ที่ ดังนั้นกลไกการสันทนาการจึงเรียกส่วนของ "เก่า" ของ onAttach, onCreateView ฯลฯ ด้วยการติดต่อกลับของฉันสำหรับการเริ่มต้นข้อมูลผ่านวิธีการดำเนินการของกิจกรรม แต่วิธีนี้ชี้ไปที่ส่วนที่สร้างขึ้นใหม่ซึ่งสร้างขึ้นผ่านวิธีการ onCreate ของกิจกรรม

ปัญหา

บางทีฉันใช้รูปแบบที่ไม่ถูกต้อง แต่แม้กระทั่งหนังสือ Android 3 Pro ก็ไม่ได้มีอะไรมาก ได้โปรดให้หมัดหนึ่ง - สองและชี้ให้เห็นวิธีการทำอย่างถูกวิธี ขอบคุณมาก!

รหัส

กิจกรรมหลัก

public class DashboardActivity extends BasePagerActivity implements OnMessageListActionListener {

private MessagesFragment mMessagesFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    setContentView(R.layout.viewpager_container);
    new DefaultToolbar(this);

    // create fragments to use
    mMessagesFragment = new MessagesFragment();
    mStreamsFragment = new StreamsFragment();

    // set titles and fragments for view pager
    Map<String, Fragment> screens = new LinkedHashMap<String, Fragment>();
    screens.put(getApplicationContext().getString(R.string.dashboard_title_dumb), new DumbFragment());
    screens.put(getApplicationContext().getString(R.string.dashboard_title_messages), mMessagesFragment);

    // instantiate view pager via adapter
    mPager = (ViewPager) findViewById(R.id.viewpager_pager);
    mPagerAdapter = new BasePagerAdapter(screens, getSupportFragmentManager());
    mPager.setAdapter(mPagerAdapter);

    // set title indicator
    TitlePageIndicator indicator = (TitlePageIndicator) findViewById(R.id.viewpager_titles);
    indicator.setViewPager(mPager, 1);

}

/* set of fragments callback interface implementations */

@Override
public void onMessageInitialisation() {

    Logger.d("Dash onMessageInitialisation");
    if (mMessagesFragment != null)
        mMessagesFragment.loadLastMessages();
}

@Override
public void onMessageSelected(Message selectedMessage) {

    Intent intent = new Intent(this, StreamActivity.class);
    intent.putExtra(Message.class.getName(), selectedMessage);
    startActivity(intent);
}

ผู้ช่วย BasePagerActivity หรือที่รู้จัก

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}

อะแดปเตอร์

public class BasePagerAdapter extends FragmentPagerAdapter implements TitleProvider {

private Map<String, Fragment> mScreens;

public BasePagerAdapter(Map<String, Fragment> screenMap, FragmentManager fm) {

    super(fm);
    this.mScreens = screenMap;
}

@Override
public Fragment getItem(int position) {

    return mScreens.values().toArray(new Fragment[mScreens.size()])[position];
}

@Override
public int getCount() {

    return mScreens.size();
}

@Override
public String getTitle(int position) {

    return mScreens.keySet().toArray(new String[mScreens.size()])[position];
}

// hack. we don't want to destroy our fragments and re-initiate them after
@Override
public void destroyItem(View container, int position, Object object) {

    // TODO Auto-generated method stub
}

}

ส่วน

public class MessagesFragment extends ListFragment {

private boolean mIsLastMessages;

private List<Message> mMessagesList;
private MessageArrayAdapter mAdapter;

private LoadMessagesTask mLoadMessagesTask;
private OnMessageListActionListener mListener;

// define callback interface
public interface OnMessageListActionListener {
    public void onMessageInitialisation();
    public void onMessageSelected(Message selectedMessage);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    // setting callback
    mListener = (OnMessageListActionListener) activity;
    mIsLastMessages = activity instanceof DashboardActivity;

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    inflater.inflate(R.layout.fragment_listview, container);
    mProgressView = inflater.inflate(R.layout.listrow_progress, null);
    mEmptyView = inflater.inflate(R.layout.fragment_nodata, null);
    return super.onCreateView(inflater, container, savedInstanceState);
}

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

    // instantiate loading task
    mLoadMessagesTask = new LoadMessagesTask();

    // instantiate list of messages
    mMessagesList = new ArrayList<Message>();
    mAdapter = new MessageArrayAdapter(getActivity(), mMessagesList);
    setListAdapter(mAdapter);
}

@Override
public void onResume() {
    mListener.onMessageInitialisation();
    super.onResume();
}

public void onListItemClick(ListView l, View v, int position, long id) {
    Message selectedMessage = (Message) getListAdapter().getItem(position);
    mListener.onMessageSelected(selectedMessage);
    super.onListItemClick(l, v, position, id);
}

/* public methods to load messages from host acitivity, etc... */
}

สารละลาย

โซลูชันโง่คือการบันทึกชิ้นส่วนภายใน onSaveInstanceState (ของโฮสต์กิจกรรม) ด้วย putFragment และนำไปไว้ใน onCreate ผ่าน getFragment แต่ฉันก็ยังมีความรู้สึกแปลก ๆ ที่สิ่งต่าง ๆ จะไม่ทำงานอย่างนั้น ... ดูรหัสด้านล่าง:

    @Override
protected void onSaveInstanceState(Bundle outState) {

    super.onSaveInstanceState(outState);
    getSupportFragmentManager()
            .putFragment(outState, MessagesFragment.class.getName(), mMessagesFragment);
}

protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    ...
    // create fragments to use
    if (savedInstanceState != null) {
        mMessagesFragment = (MessagesFragment) getSupportFragmentManager().getFragment(
                savedInstanceState, MessagesFragment.class.getName());
                StreamsFragment.class.getName());
    }
    if (mMessagesFragment == null)
        mMessagesFragment = new MessagesFragment();
    ...
}

1
ฉันสงสัยว่าตอนนี้: ฉันควรใช้วิธีที่แตกต่างกันมากหรือพยายามบันทึกชิ้นส่วนของกิจกรรมหลัก (Dashboard) ผ่าน onSavedInstancestate เพื่อใช้ใน onCreate () มีวิธีที่เหมาะสมในการบันทึกชิ้นส่วนเหล่านั้นและนำพวกมันออกจากชุดใน onCreate หรือไม่? ดูเหมือนว่าพวกเขาจะไม่สามารถพัสดุได้ ...
Oleksii Malovanyi

2
วิธีที่ 2 ใช้งานได้ - ดู "ซัลเฟอร์" แต่ดูเหมือนว่าจะเป็นรหัสที่น่าเกลียดใช่ไหม?
Oleksii Malovanyi

1
เพื่อความพยายามในการล้างแท็ก Android (รายละเอียดที่นี่: meta.stackexchange.com/questions/100529/… ) คุณจะโพสต์คำตอบของคุณเป็นคำตอบและทำเครื่องหมายเป็นรายการที่เลือกหรือไม่ วิธีการที่จะไม่แสดงขึ้นเป็นคำถามที่ยังไม่ได้ตอบ :)
อเล็กซานเดลูคัส

1
ใช่คิดว่ามันโอเค หวังว่า smth จะดีกว่าของฉัน ...
Oleksii Malovanyi

1
โซลูชันที่โง่ยังทำงานได้หรือไม่ มันทำให้ฉันมีข้อยกเว้นตัวชี้โมฆะ ..
Zhen Liu

คำตอบ:


451

เมื่อFragmentPagerAdapterเพิ่มแฟรกเมนต์ให้กับ FragmentManager จะใช้แท็กพิเศษตามตำแหน่งเฉพาะที่จะแตกแฟรกเมนต์ FragmentPagerAdapter.getItem(int position)จะถูกเรียกก็ต่อเมื่อไม่มีแฟรกเมนต์สำหรับตำแหน่งนั้นอยู่ หลังจากหมุนแล้ว Android จะสังเกตเห็นว่าได้สร้าง / บันทึกชิ้นส่วนสำหรับตำแหน่งนี้แล้วและพยายามเชื่อมต่อกับมันFragmentManager.findFragmentByTag()แทนการสร้างขึ้นใหม่ ทั้งหมดนี้มาฟรีเมื่อใช้FragmentPagerAdapterและเป็นสาเหตุที่ปกติจะมีรหัสการเริ่มต้นส่วนของคุณในgetItem(int)วิธีการ

แม้ว่าเราจะไม่ได้ใช้มันไม่ได้เป็นความคิดที่ดีในการสร้างชิ้นส่วนใหม่ทุกครั้งเดียวในFragmentPagerAdapter Activity.onCreate(Bundle)ดังที่คุณสังเกตเห็นเมื่อมีการเพิ่มแฟรกเมนต์ลงใน FragmentManager มันจะถูกสร้างขึ้นใหม่สำหรับคุณหลังจากการหมุนและไม่จำเป็นต้องเพิ่มอีก การทำเช่นนั้นเป็นสาเหตุของข้อผิดพลาดทั่วไปเมื่อทำงานกับแฟรกเมนต์

วิธีการปกติเมื่อทำงานกับชิ้นส่วนคือ:

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

    ...

    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit(); 
    }

    ...

}

เมื่อใช้ a FragmentPagerAdapterเราจะละทิ้งการจัดการแฟรกเมนต์ไปยังอะแด็ปเตอร์และไม่ต้องทำตามขั้นตอนข้างต้น โดยค่าเริ่มต้นมันจะโหลดล่วงหน้าชิ้นส่วนหนึ่งไว้ด้านหน้าและด้านหลังตำแหน่งปัจจุบันเท่านั้น (แม้ว่าจะไม่ทำลายชิ้นส่วนเหล่านั้นเว้นแต่ว่าคุณกำลังใช้งานอยู่FragmentStatePagerAdapter) นี้จะถูกควบคุมโดยViewPager.setOffscreenPageLimit (int) ด้วยเหตุนี้วิธีการโทรโดยตรงบนแฟรกเมนต์ภายนอกของอะแด็ปเตอร์จึงไม่รับประกันว่าจะใช้ได้เนื่องจากอาจไม่ได้มีชีวิตอยู่

เพื่อตัดเรื่องสั้นสั้น ๆ ทางออกของคุณที่จะใช้putFragmentเพื่อให้ได้รับการอ้างอิงในภายหลังนั้นไม่ได้บ้านักและไม่แตกต่างจากวิธีปกติในการใช้ชิ้นส่วนต่อไป (ด้านบน) เป็นการยากที่จะได้รับการอ้างอิงเป็นอย่างอื่นเนื่องจากอะแดปเตอร์ถูกเพิ่มเข้าไปในแฟรกเมนต์ไม่ใช่คุณเป็นการส่วนตัว ตรวจสอบให้แน่ใจว่าoffscreenPageLimitสูงพอที่จะโหลดชิ้นส่วนที่คุณต้องการตลอดเวลาเนื่องจากคุณต้องพึ่งพาชิ้นส่วนที่มีอยู่ สิ่งนี้ข้ามความสามารถในการโหลดที่ขี้เกียจของ ViewPager แต่ดูเหมือนจะเป็นสิ่งที่คุณต้องการสำหรับแอปพลิเคชันของคุณ

อีกวิธีหนึ่งคือการแทนที่FragmentPageAdapter.instantiateItem(View, int)และบันทึกการอ้างอิงไปยังแฟรกเมนต์ที่ส่งคืนจากการเรียก super ก่อนที่จะส่งคืน (มันมีตรรกะเพื่อค้นหาแฟรกเมนต์หากมีอยู่แล้ว)

สำหรับภาพฟูลเลอร์ให้ดูที่แหล่งที่มาของFragmentPagerAdapter (สั้น) และViewPager (ยาว)


7
ชอบส่วนสุดท้าย FragmentPageAdapter.instantiateItem(View, int)มีแคชสำหรับชิ้นส่วนและย้ายใส่ในตรรกะแคชภายใน ในที่สุดการแก้ไขข้อผิดพลาดในระยะยาวที่ปรากฏขึ้นเฉพาะในการหมุน / เปลี่ยนการตั้งค่าและถูกขับรถฉันบ้า ...
คาร์ลอ Sobrinho

2
ปัญหานี้ได้รับการแก้ไขเมื่อฉันเปลี่ยนการหมุน อาจเป็นเพราะที่ฉันมองไม่เห็น แต่เอกสารนี้มีอะไรบ้าง คือการใช้แท็กเพื่อกู้คืนจากสถานะก่อนหน้า? อาจเป็นคำตอบที่ชัดเจน แต่ฉันค่อนข้างใหม่กับการพัฒนา Android

1
@ Industrial-antidepressant มันเป็น id ของ container (เช่น FrameLayout) ที่จะทำการเพิ่ม Fragment
antonyt

1
ครับสำหรับผมมันเป็นมากกว่าFragmentPageAdapter.instantiateItem(ViewGroup, int) FragmentPageAdapter.instantiateItem(View, int)
ชื่อที่แสดง

1
Btw คุณรู้หรือไม่ว่า (ถ้ามี) วิธีการแยกส่วนวงจรชีวิตที่เรียกว่าเมื่อส่วนได้รับการรูดออกจากหน้าจอ? มันเป็นonDetach()หรืออย่างอื่น?
ygesher

36

ฉันต้องการที่จะนำเสนอโซลูชั่นที่ขยายตัวในantonyt's คำตอบที่ยอดเยี่ยมและกล่าวถึงการเอาชนะFragmentPageAdapter.instantiateItem(View, int)เพื่อบันทึกการอ้างอิงถึงสร้างขึ้นFragmentsเพื่อให้คุณสามารถทำงานกับพวกเขาในภายหลัง สิ่งนี้ควรทำงานด้วยFragmentStatePagerAdapter; ดูบันทึกเพื่อดูรายละเอียด


นี่คือตัวอย่างง่ายๆของวิธีการที่จะได้รับการอ้างอิงถึงFragmentsกลับโดยFragmentPagerAdapterที่ไม่ต้องพึ่งพาภายในนี้ตั้งอยู่บนtags Fragmentsกุญแจสำคัญคือการแทนที่instantiateItem()และบันทึกการอ้างอิงในการมีแทนgetItem()ใน

public class SomeActivity extends Activity {
    private FragmentA m1stFragment;
    private FragmentB m2ndFragment;

    // other code in your Activity...

    private class CustomPagerAdapter extends FragmentPagerAdapter {
        // other code in your custom FragmentPagerAdapter...

        public CustomPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            // Do NOT try to save references to the Fragments in getItem(),
            // because getItem() is not always called. If the Fragment
            // was already created then it will be retrieved from the FragmentManger
            // and not here (i.e. getItem() won't be called again).
            switch (position) {
                case 0:
                    return new FragmentA();
                case 1:
                    return new FragmentB();
                default:
                    // This should never happen. Always account for each position above
                    return null;
            }
        }

        // Here we can finally safely save a reference to the created
        // Fragment, no matter where it came from (either getItem() or
        // FragmentManger). Simply save the returned Fragment from
        // super.instantiateItem() into an appropriate reference depending
        // on the ViewPager position.
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
            // save the appropriate reference depending on position
            switch (position) {
                case 0:
                    m1stFragment = (FragmentA) createdFragment;
                    break;
                case 1:
                    m2ndFragment = (FragmentB) createdFragment;
                    break;
            }
            return createdFragment;
        }
    }

    public void someMethod() {
        // do work on the referenced Fragments, but first check if they
        // even exist yet, otherwise you'll get an NPE.

        if (m1stFragment != null) {
            // m1stFragment.doWork();
        }

        if (m2ndFragment != null) {
            // m2ndFragment.doSomeWorkToo();
        }
    }
}

หรือถ้าคุณชอบที่จะทำงานร่วมกับtagsแทนตัวแปรสมาชิกระดับ / การอ้างอิงไปยังFragmentsคุณยังสามารถคว้าtagsชุดโดยFragmentPagerAdapterในลักษณะเดียวกัน: หมายเหตุ: นี้ไม่ได้นำไปใช้FragmentStatePagerAdapterเพราะมันไม่ได้ตั้งค่าเมื่อมีการสร้างของtagsFragments

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
    // get the tags set by FragmentPagerAdapter
    switch (position) {
        case 0:
            String firstTag = createdFragment.getTag();
            break;
        case 1:
            String secondTag = createdFragment.getTag();
            break;
    }
    // ... save the tags somewhere so you can reference them later
    return createdFragment;
}

โปรดทราบว่าวิธีนี้จะไม่พึ่งพาการลอกเลียนแบบtagชุดภายในโดยFragmentPagerAdapterใช้ API ที่เหมาะสมในการดึงข้อมูล วิธีนี้แม้ว่าการtagเปลี่ยนแปลงในเวอร์ชันอนาคตของSupportLibraryคุณจะยังคงปลอดภัย


อย่าลืมว่าขึ้นอยู่กับการออกแบบของคุณActivityว่าFragmentsคุณกำลังพยายามทำอยู่อาจจะหรืออาจไม่มีอยู่ดังนั้นคุณต้องคำนึงถึงสิ่งนั้นด้วยการnullตรวจสอบก่อนที่จะใช้การอ้างอิงของคุณ

นอกจากนี้หากคุณทำงานด้วยFragmentStatePagerAdapterคุณก็ไม่ต้องการเก็บการอ้างอิงอย่างหนักกับคุณFragmentsเพราะคุณอาจมีหลายคนและการอ้างอิงที่ยากจะทำให้พวกเขาอยู่ในหน่วยความจำโดยไม่จำเป็น บันทึกการFragmentอ้างอิงในWeakReferenceตัวแปรแทนค่ามาตรฐานแทน แบบนี้:

WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
    // reference hasn't been cleared yet; do work...
}

เกี่ยวกับปัญหาการขาดแคลนหน่วยความจำ Android อาจขอให้ FragmentManager ทำลายชิ้นส่วนที่ไม่ได้ใช้ใช่ไหม หากฉันพูดถูกรุ่นที่สองของคุณจะใช้งานได้รุ่นแรกอาจล้มเหลว (ไม่รู้ว่ารุ่น Android ปัจจุบันทำเช่นนี้หรือไม่ แต่ดูเหมือนว่าเราต้องคาดหวังไว้)
Moritz ทั้งคู่

1
สิ่งที่เกี่ยวกับการสร้างFragmetPagerAdapterใน onCreate ของกิจกรรมในทุกการหมุนหน้าจอ เป็นสิ่งที่ผิดนี้เพราะมันอาจบายพาสการนำเพิ่มชิ้นส่วนในFragmentPagerAdapter
Bahman

การเอาชนะinstantiateItem()เป็นวิธีที่จะไป; สิ่งนี้ช่วยให้ฉันจัดการกับการหมุนหน้าจอและการดึงอินสแตนซ์ Fragment ที่มีอยู่ของฉันเมื่อกิจกรรมและอะแดปเตอร์กลับมาทำงานต่อได้ ฉันทิ้งความคิดเห็นไว้ในโค้ดเพื่อเป็นการเตือน: หลังจากการหมุนgetItem()จะไม่ถูกเรียกใช้ วิธีการนี้เท่านั้นที่instantiateItem()ได้รับเรียก การใช้งานที่ยอดเยี่ยมสำหรับinstantiateItem()การแนบชิ้นส่วนจริงอีกครั้งหลังจากการหมุน (ตามความจำเป็น) แทนที่จะสร้างอินสแตนซ์ใหม่!
HelloImKevo

ฉันได้รับตัวชี้โมฆะ Fragment createdFragment = (Fragment) super.instantiateItem..ในโซลูชั่นแรก
AlexS

หลังจากตามล่าซ้ำ / ตัวแปรที่แตกต่างกันทั้งหมดของปัญหานี้นี่เป็นทางออกที่ดีที่สุด (อ้างอิงข้ามจากอีก Q ที่มีชื่อที่เกี่ยวข้องมากขึ้นดังนั้นขอขอบคุณสำหรับสิ่งนั้น!)
MandisaW

18

ฉันพบโซลูชันอื่นที่ค่อนข้างง่ายสำหรับคำถามของคุณ

ดังที่คุณเห็นได้จากซอร์สโค้ด FragmentPagerAdapterชิ้นส่วนที่จัดการโดยFragmentPagerAdapterร้านค้าในFragmentManagerภายใต้แท็กที่สร้างโดยใช้:

String tag="android:switcher:" + viewId + ":" + index;

viewIdเป็นcontainer.getId()ที่containerเป็นของคุณViewPagerตัวอย่างเช่น indexคือตำแหน่งของชิ้นส่วนที่ ดังนั้นคุณสามารถบันทึก ID วัตถุไปที่outState:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("viewpagerid" , mViewPager.getId() );
}

@Override
    protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null)
        viewpagerid=savedInstanceState.getInt("viewpagerid", -1 );  

    MyFragmentPagerAdapter titleAdapter = new MyFragmentPagerAdapter (getSupportFragmentManager() , this);        
    mViewPager = (ViewPager) findViewById(R.id.pager);
    if (viewpagerid != -1 ){
        mViewPager.setId(viewpagerid);
    }else{
        viewpagerid=mViewPager.getId();
    }
    mViewPager.setAdapter(titleAdapter);

หากคุณต้องการสื่อสารกับส่วนนี้คุณสามารถรับได้จากFragmentManagerเช่น:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")

21
hmmm ฉันไม่คิดว่านี่เป็นวิธีที่ดีในการตอบการตั้งชื่อแท็กภายในที่ไม่ได้หมายความว่าจะคงเดิมตลอดไป
Dori

1
สำหรับผู้ที่มองหาวิธีการแก้ปัญหาที่ไม่พึ่งพาภายในtagให้ลองคำตอบของฉัน
Tony Chan

16

ฉันต้องการนำเสนอทางเลือกสำหรับกรณีที่แตกต่างกันเล็กน้อยเนื่องจากการค้นหาคำตอบหลายครั้งทำให้ฉันเข้ามาที่หัวข้อนี้

กรณีของฉัน - ฉันกำลังสร้าง / เพิ่มหน้าเว็บแบบไดนามิกและเลื่อนไปยัง ViewPager แต่เมื่อหมุน (onConfigurationChange) ฉันจะจบด้วยหน้าใหม่เพราะแน่นอนว่า OnCreate ถูกเรียกอีกครั้ง แต่ฉันต้องการอ้างอิงถึงหน้าทั้งหมดที่สร้างขึ้นก่อนการหมุน

ปัญหา - ฉันไม่มีตัวระบุเฉพาะสำหรับแต่ละส่วนที่ฉันสร้างดังนั้นวิธีเดียวในการอ้างอิงคือการจัดเก็บข้อมูลอ้างอิงใน Array ที่จะกู้คืนหลังจากการหมุน / การกำหนดค่า

วิธีแก้ปัญหา - แนวคิดหลักคือการมีกิจกรรม (ซึ่งแสดงชิ้นส่วน) ยังจัดการอาร์เรย์ของการอ้างอิงถึงชิ้นส่วนที่มีอยู่เนื่องจากกิจกรรมนี้สามารถใช้การรวมกลุ่มใน onSaveInstanceState

public class MainActivity extends FragmentActivity

ดังนั้นในกิจกรรมนี้ฉันประกาศให้สมาชิกเอกชนติดตามหน้าเว็บที่เปิดอยู่

private List<Fragment> retainedPages = new ArrayList<Fragment>();

สิ่งนี้ถูกอัพเดททุกครั้งที่มีการเรียกใช้และเรียกคืน OnSaveInstanceState ใน onCreate

@Override
protected void onSaveInstanceState(Bundle outState) {
    retainedPages = _adapter.exportList();
    outState.putSerializable("retainedPages", (Serializable) retainedPages);
    super.onSaveInstanceState(outState);
}

... ดังนั้นเมื่อถูกจัดเก็บจะสามารถเรียกคืนได้ ...

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

    if (savedInstanceState != null) {
        retainedPages = (List<Fragment>) savedInstanceState.getSerializable("retainedPages");
    }
    _mViewPager = (CustomViewPager) findViewById(R.id.viewPager);
    _adapter = new ViewPagerAdapter(getApplicationContext(), getSupportFragmentManager());
    if (retainedPages.size() > 0) {
        _adapter.importList(retainedPages);
    }
    _mViewPager.setAdapter(_adapter);
    _mViewPager.setCurrentItem(_adapter.getCount()-1);
}

สิ่งเหล่านี้เป็นการเปลี่ยนแปลงที่สำคัญของกิจกรรมหลักดังนั้นฉันจึงต้องการสมาชิกและวิธีการภายใน FragmentPagerAdapter ของฉันเพื่อให้สิ่งนี้ทำงานได้ดังนั้นภายใน

public class ViewPagerAdapter extends FragmentPagerAdapter

โครงสร้างที่เหมือนกัน (ดังที่แสดงไว้ใน MainActivity)

private List<Fragment> _pages = new ArrayList<Fragment>();

และการซิงค์นี้ (ตามที่ใช้ข้างต้นใน onSaveInstanceState) ได้รับการสนับสนุนโดยวิธีการเฉพาะ

public List<Fragment> exportList() {
    return _pages;
}

public void importList(List<Fragment> savedPages) {
    _pages = savedPages;
}

และในที่สุดในชั้นเรียน

public class CustomFragment extends Fragment

เพื่อให้การทำงานทั้งหมดนี้มีการเปลี่ยนแปลงสองประการแรก

public class CustomFragment extends Fragment implements Serializable

จากนั้นเพิ่มสิ่งนี้ไปยัง onCreate ดังนั้นชิ้นส่วนจะไม่ถูกทำลาย

setRetainInstance(true);

ฉันยังอยู่ในกระบวนการห่อหัวของฉันรอบ ๆ ชิ้นส่วนและวงจรชีวิตของ Android ดังนั้นข้อแม้ที่นี่อาจมีความซ้ำซ้อน / ความไร้ประสิทธิภาพในวิธีนี้ แต่มันใช้งานได้สำหรับฉันและฉันหวังว่าอาจเป็นประโยชน์สำหรับผู้อื่นที่มีกรณีคล้ายกับของฉัน


2
+1 สำหรับ setRetainInstance (จริง) - มนต์ดำที่ฉันกำลังมองหา!
เสื่อมค่า

มันง่ายยิ่งขึ้นโดยใช้ FragmentStatePagerAdapter (v13) ซึ่งจัดการให้คุณด้วยการคืนค่าและปล่อยสถานะ
คนขายเนื้อ

คำอธิบายที่ชัดเจนและทางออกที่ดีมาก ขอบคุณ!
Bruno Bieri

8

โซลูชันของฉันหยาบคายมาก แต่ใช้งานได้: เมื่อชิ้นส่วนของฉันสร้างขึ้นแบบไดนามิกจากข้อมูลที่เก็บไว้ฉันเพียงลบส่วนทั้งหมดจากการPageAdapterโทรก่อนsuper.onSaveInstanceState()แล้วจึงสร้างพวกเขาใหม่ในการสร้างกิจกรรม:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
    mSectionsPagerAdapter.removeAllfragments();
    super.onSaveInstanceState(outState);
}

คุณไม่สามารถลบออกonDestroy()มิฉะนั้นคุณจะได้รับข้อยกเว้นนี้:

java.lang.IllegalStateException: ไม่สามารถทำการกระทำนี้หลังจาก onSaveInstanceState

นี่คือรหัสในอะแดปเตอร์หน้า:

public void removeAllfragments()
{
    if ( mFragmentList != null ) {
        for ( Fragment fragment : mFragmentList ) {
            mFm.beginTransaction().remove(fragment).commit();
        }
        mFragmentList.clear();
        notifyDataSetChanged();
    }
}

ฉันบันทึกหน้าปัจจุบันเท่านั้นและกู้คืนในonCreate()หลังจากสร้างแฟรกเมนต์แล้ว

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  

ฉันไม่รู้ว่าทำไม แต่แจ้งข้อมูล DataSetChanged (); ทำให้แอปหยุดทำงาน
Malachiasz

4

นั่นคือBasePagerAdapterอะไร คุณควรใช้หนึ่งในอะแดปเตอร์เพจเจอร์มาตรฐาน - อย่างใดอย่างหนึ่งFragmentPagerAdapterหรือFragmentStatePagerAdapterขึ้นอยู่กับว่าคุณต้องการเศษเล็กเศษน้อยที่ไม่ต้องการโดยViewPagerจะถูกเก็บไว้รอบ (อดีต) หรือมีการบันทึกสถานะของพวกเขา (หลัง) และสร้างใหม่ถ้า จำเป็นอีกครั้ง

โค้ดตัวอย่างสำหรับการใช้ViewPagerสามารถดูได้ที่นี่

มันเป็นความจริงที่การจัดการชิ้นส่วนในมุมมองเพจเจอร์ข้ามอินสแตนซ์ของกิจกรรมมีความซับซ้อนเล็กน้อยเนื่องจากFragmentManagerในกรอบจะดูแลการบันทึกสถานะและเรียกคืนชิ้นส่วนที่แอ็คทีฟที่เพจเจอร์ได้ทำไว้ ทั้งหมดนี้หมายถึงจริงๆคืออะแดปเตอร์เมื่อเริ่มต้นความต้องการเพื่อให้แน่ใจว่ามันเชื่อมต่อกับชิ้นส่วนใด ๆ ที่เรียกคืนมี คุณสามารถดูรหัสสำหรับFragmentPagerAdapterหรือFragmentStatePagerAdapterดูวิธีการทำ


1
รหัส BasePagerAdapter มีอยู่ในคำถามของฉัน ในฐานะที่เป็นหนึ่งสามารถมองเห็นมันก็ขยาย FragmentPagerAdapter เพื่อวัตถุประสงค์ในการดำเนินการTitleProvider ดังนั้นทุกอย่างทำงานร่วมกับ Android dev แนวทางที่แนะนำแล้ว
Oleksii Malovanyi

ฉันไม่รู้จัก "FragmentStatePagerAdapter" คุณช่วยฉันชั่วโมงอย่างแท้จริง ขอบคุณ
Knossos

2

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

ตรวจสอบให้แน่ใจว่าคุณโทรViewPager.setOffscreenPageLimit()ก่อนที่จะโทรViewPager.setAdapter(fragmentStatePagerAdapter)

เมื่อโทรViewPager.setOffscreenPageLimit()... ViewPager จะมองไปที่อะแดปเตอร์ในทันทีและลองรับชิ้นส่วน สิ่งนี้อาจเกิดขึ้นก่อนที่ ViewPager จะมีโอกาสกู้คืน Fragments จาก saveInstanceState (ดังนั้นการสร้าง Fragments ใหม่ที่ไม่สามารถเริ่มต้นใหม่จาก SavedInstanceState ได้เนื่องจากเป็นส่วนใหม่)


0

ฉันคิดวิธีแก้ปัญหาที่เรียบง่ายและสง่างามนี้ โดยสันนิษฐานว่ากิจกรรมมีหน้าที่สร้างชิ้นส่วนและอะแดปเตอร์เพิ่งให้บริการ

นี่คือรหัสของอะแดปเตอร์ (ไม่มีอะไรแปลก ๆ ที่นี่ยกเว้นความจริงที่ว่าmFragmentsเป็นรายการของส่วนที่ดูแลโดยกิจกรรม)

class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {

    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

    @Override
    public int getCount() {
        return mFragments.size();
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        TabFragment fragment = (TabFragment)mFragments.get(position);
        return fragment.getTitle();
    }
} 

ปัญหาทั้งหมดของเธรดนี้กำลังได้รับการอ้างอิงของส่วน "เก่า" ดังนั้นฉันใช้รหัสนี้ใน onCreate ของกิจกรรม

    if (savedInstanceState!=null) {
        if (getSupportFragmentManager().getFragments()!=null) {
            for (Fragment fragment : getSupportFragmentManager().getFragments()) {
                mFragments.add(fragment);
            }
        }
    }

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


0

ในการรับแฟรกเมนต์หลังการวางแนวคุณต้องใช้. getTag ()

    getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId + ":" + positionOfItemInViewPager)

สำหรับการจัดการอีกเล็กน้อยฉันเขียน ArrayList ของฉันเองสำหรับ PageAdapter ของฉันเพื่อรับแฟรกเมนต์โดย viewPagerId และ FragmentClass ที่ตำแหน่งใด ๆ :

public class MyPageAdapter extends FragmentPagerAdapter implements Serializable {
private final String logTAG = MyPageAdapter.class.getName() + ".";

private ArrayList<MyPageBuilder> fragmentPages;

public MyPageAdapter(FragmentManager fm, ArrayList<MyPageBuilder> fragments) {
    super(fm);
    fragmentPages = fragments;
}

@Override
public Fragment getItem(int position) {
    return this.fragmentPages.get(position).getFragment();
}

@Override
public CharSequence getPageTitle(int position) {
    return this.fragmentPages.get(position).getPageTitle();
}

@Override
public int getCount() {
    return this.fragmentPages.size();
}


public int getItemPosition(Object object) {
    //benötigt, damit bei notifyDataSetChanged alle Fragemnts refrehsed werden

    Log.d(logTAG, object.getClass().getName());
    return POSITION_NONE;
}

public Fragment getFragment(int position) {
    return getItem(position);
}

public String getTag(int position, int viewPagerId) {
    //getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.shares_detail_activity_viewpager + ":" + myViewPager.getCurrentItem())

    return "android:switcher:" + viewPagerId + ":" + position;
}

public MyPageBuilder getPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
    return new MyPageBuilder(pageTitle, icon, selectedIcon, frag);
}


public static class MyPageBuilder {

    private Fragment fragment;

    public Fragment getFragment() {
        return fragment;
    }

    public void setFragment(Fragment fragment) {
        this.fragment = fragment;
    }

    private String pageTitle;

    public String getPageTitle() {
        return pageTitle;
    }

    public void setPageTitle(String pageTitle) {
        this.pageTitle = pageTitle;
    }

    private int icon;

    public int getIconUnselected() {
        return icon;
    }

    public void setIconUnselected(int iconUnselected) {
        this.icon = iconUnselected;
    }

    private int iconSelected;

    public int getIconSelected() {
        return iconSelected;
    }

    public void setIconSelected(int iconSelected) {
        this.iconSelected = iconSelected;
    }

    public MyPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
        this.pageTitle = pageTitle;
        this.icon = icon;
        this.iconSelected = selectedIcon;
        this.fragment = frag;
    }
}

public static class MyPageArrayList extends ArrayList<MyPageBuilder> {
    private final String logTAG = MyPageArrayList.class.getName() + ".";

    public MyPageBuilder get(Class cls) {
        // Fragment über FragmentClass holen
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return super.get(indexOf(item));
            }
        }
        return null;
    }

    public String getTag(int viewPagerId, Class cls) {
        // Tag des Fragment unabhängig vom State z.B. nach bei Orientation change
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return "android:switcher:" + viewPagerId + ":" + indexOf(item);
            }
        }
        return null;
    }
}

ดังนั้นเพียงแค่สร้าง MyPageArrayList ด้วยแฟรกเมนต์:

    myFragPages = new MyPageAdapter.MyPageArrayList();

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_data_frag),
            R.drawable.ic_sd_storage_24dp,
            R.drawable.ic_sd_storage_selected_24dp,
            new WidgetDataFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_color_frag),
            R.drawable.ic_color_24dp,
            R.drawable.ic_color_selected_24dp,
            new WidgetColorFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_textsize_frag),
            R.drawable.ic_settings_widget_24dp,
            R.drawable.ic_settings_selected_24dp,
            new WidgetTextSizeFrag()));

และเพิ่มลงใน viewPager:

    mAdapter = new MyPageAdapter(getSupportFragmentManager(), myFragPages);
    myViewPager.setAdapter(mAdapter);

หลังจากนี้คุณจะได้รับหลังจากการปฐมนิเทศเปลี่ยนชิ้นส่วนที่ถูกต้องโดยใช้ระดับ:

        WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager()
            .findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class));

-30

เพิ่ม:

   @SuppressLint("ValidFragment")

ก่อนเรียน

มันไม่ทำงานทำอะไรเช่นนี้

@SuppressLint({ "ValidFragment", "HandlerLeak" })
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.