Làm thế nào để lưu chính xác trạng thái của Fragment trong stack back?


489

Tôi đã tìm thấy nhiều trường hợp của một câu hỏi tương tự trên SO nhưng không có câu trả lời nào không may đáp ứng yêu cầu của tôi.

Tôi có các bố cục khác nhau cho dọc và ngang và tôi đang sử dụng ngăn xếp ngược, cả hai đều ngăn tôi sử dụng setRetainState()và các thủ thuật sử dụng thói quen thay đổi cấu hình.

Tôi hiển thị một số thông tin nhất định cho người dùng trong TextViews, không được lưu trong trình xử lý mặc định. Khi viết ứng dụng của tôi chỉ sử dụng Hoạt động, các hoạt động sau đây đã hoạt động tốt:

TextView vstup;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.whatever);
    vstup = (TextView)findViewById(R.id.whatever);
    /* (...) */
}

@Override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    state.putCharSequence(App.VSTUP, vstup.getText());
}

@Override
public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    vstup.setText(state.getCharSequence(App.VSTUP));
}

Với Fragments, điều này chỉ hoạt động trong các tình huống rất cụ thể. Cụ thể, những gì phá vỡ khủng khiếp là thay thế một đoạn, đặt nó vào ngăn xếp phía sau và sau đó xoay màn hình trong khi đoạn mới được hiển thị. Theo những gì tôi hiểu, đoạn cũ không nhận được cuộc gọi onSaveInstanceState()khi được thay thế mà vẫn được liên kết bằng cách nào đó Activityvà phương thức này được gọi sau khi nó Viewkhông còn tồn tại nữa, vì vậy hãy tìm bất kỳ TextViewkết quả nào của tôi vào a NullPointerException.

Ngoài ra, tôi thấy rằng việc giữ tham chiếu đến tôi TextViewskhông phải là ý hay với Fragments, ngay cả khi nó ổn với Activity's. Trong trường hợp đó, onSaveInstanceState()thực sự lưu trạng thái nhưng sự cố sẽ xuất hiện lại nếu tôi xoay màn hình hai lần khi đoạn bị ẩn, vì nó onCreateView()không được gọi trong trường hợp mới.

Tôi đã nghĩ đến việc lưu trạng thái onDestroyView()vào một số Bundlephần tử thành viên của lớp (thực sự là nhiều dữ liệu hơn, không chỉ một TextView) và lưu vào onSaveInstanceState()nhưng còn có những hạn chế khác. Về cơ bản, nếu đoạn này hiện được hiển thị, thứ tự gọi hai hàm bị đảo ngược, vì vậy tôi cần tính đến hai tình huống khác nhau. Phải có một giải pháp sạch hơn và đúng đắn!


1
Dưới đây là ví dụ rất tốt với lời giải thích chi tiết là tốt. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
Tôi thứ hai liên kết emunee.com. Nó đã giải quyết vấn đề UI dính cho tôi!
Reenactor Rob

Câu trả lời:


541

Để lưu chính xác trạng thái của Fragmentbạn, bạn nên làm như sau:

1. Trong đoạn, lưu trạng thái cá thể bằng cách ghi đè onSaveInstanceState()và khôi phục trong onActivityCreated():

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's state here
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's state here
    }

}

2.điểm quan trọng , trong hoạt động, bạn phải lưu ví dụ của đoạn onSaveInstanceState()và khôi phục lại onCreate().

class MyActivity extends Activity {

    private MyFragment 

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's instance
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}

Hi vọng điêu nay co ich.


7
Điều này làm việc hoàn hảo cho tôi! Không có cách giải quyết, không hack, chỉ có ý nghĩa theo cách này. Cảm ơn bạn vì điều này, đã làm cho giờ tìm kiếm thành công. SaveInstanceState () của các giá trị của bạn trong đoạn, sau đó lưu đoạn trong Hoạt động giữ đoạn đó, sau đó khôi phục :)
MattMatt

77
MDentent là gì?
phù thủy

14
@wizurd mContent là một Đoạn, nó liên quan đến thể hiện của đoạn hiện tại trong hoạt động.
ThanhHH

13
Bạn có thể giải thích làm thế nào điều này sẽ lưu trạng thái của một đoạn trong ngăn xếp trở lại? Đó là những gì OP yêu cầu.
hitmaneidos

51
Điều này không liên quan đến câu hỏi, onSaveInstance không được gọi khi đoạn được đưa vào backstack
Tadas Vala viêm

87

Đây là cách tôi đang sử dụng tại thời điểm này ... nó rất phức tạp nhưng ít nhất nó xử lý tất cả các tình huống có thể xảy ra. Trong trường hợp bất cứ ai quan tâm.

public final class MyFragment extends Fragment {
    private TextView vstup;
    private Bundle savedState = null;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.whatever, null);
        vstup = (TextView)v.findViewById(R.id.whatever);

        /* (...) */

        /* If the Fragment was destroyed inbetween (screen rotation), we need to recover the savedState first */
        /* However, if it was not, it stays in the instance from the last onDestroyView() and we don't want to overwrite it */
        if(savedInstanceState != null && savedState == null) {
            savedState = savedInstanceState.getBundle(App.STAV);
        }
        if(savedState != null) {
            vstup.setText(savedState.getCharSequence(App.VSTUP));
        }
        savedState = null;

        return v;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        savedState = saveState(); /* vstup defined here for sure */
        vstup = null;
    }

    private Bundle saveState() { /* called either from onDestroyView() or onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* If onDestroyView() is called first, we can use the previously savedState but we can't call saveState() anymore */
        /* If onSaveInstanceState() is called first, we don't have savedState, so we need to call saveState() */
        /* => (?:) operator inevitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

Ngoài ra , luôn có khả năng giữ dữ liệu được hiển thị ở dạng bị động Viewtrong các biến và sử dụngView s để hiển thị chúng, giữ cho hai thứ được đồng bộ hóa. Tôi không xem xét phần cuối cùng rất sạch sẽ, mặc dù.


68
Đây là giải pháp tốt nhất mà tôi đã tìm thấy cho đến nay nhưng vẫn còn (hơi lạ) Vấn đề còn lại là một: nếu bạn có hai mảnh, AB, nơi Ahiện đang trên backstack và Bđược nhìn thấy, sau đó bạn bị mất tình trạng A(vô hình một) nếu bạn xoay màn hình hai lần . Vấn đề là onCreateView()không được gọi trong kịch bản này, chỉ onCreate(). Vì vậy, sau này, onSaveInstanceState()không có quan điểm để cứu nhà nước từ. Một người sẽ phải lưu trữ và sau đó lưu trạng thái thông qua onCreate().
devconsole

7
@devconsole Tôi ước tôi có thể cho bạn 5 phiếu bầu cho bình luận này! Vòng quay này hai lần đã giết chết tôi trong nhiều ngày.
DroidT

Cảm ơn bạn đã trả lời tuyệt vời! Tôi có một câu hỏi mặc dù. Đâu là nơi tốt nhất để khởi tạo đối tượng mô hình (POJO) trong đoạn này?
Đổi mới

7
Để giúp tiết kiệm thời gian cho người khác App.VSTUPApp.STAVcả hai thẻ chuỗi đại diện cho các đối tượng họ đang cố gắng lấy. Ví dụ: savedState = savedInstanceState.getBundle(savedGamePlayString);hoặcsavedState.getDouble("averageTime")
Tanner Hallman

1
Đây là một điều của vẻ đẹp.
Ivan

61

Trên thư viện hỗ trợ mới nhất, không có giải pháp nào được thảo luận ở đây là cần thiết nữa. Bạn có thể chơi với Activitycác mảnh của bạn như bạn muốn bằng cách sử dụngFragmentTransaction . Chỉ cần đảm bảo rằng các mảnh của bạn có thể được xác định bằng id hoặc thẻ.

Các mảnh sẽ được khôi phục tự động miễn là bạn không cố gắng tạo lại chúng trên mỗi cuộc gọi đến onCreate(). Thay vào đó, bạn nên kiểm tra xemsavedInstanceState không phải là null và tìm các tham chiếu cũ đến các đoạn được tạo trong trường hợp này.

Đây là một ví dụ:

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

    if (savedInstanceState == null) {
        myFragment = MyFragment.newInstance();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.my_container, myFragment, MY_FRAGMENT_TAG)
                .commit();
    } else {
        myFragment = (MyFragment) getSupportFragmentManager()
                .findFragmentByTag(MY_FRAGMENT_TAG);
    }
...
}

Tuy nhiên, lưu ý rằng hiện tại có một lỗi khi khôi phục trạng thái ẩn của một đoạn. Nếu bạn đang ẩn các đoạn trong hoạt động của mình, bạn sẽ cần khôi phục trạng thái này theo cách thủ công trong trường hợp này.


2
Đây có phải là sửa chữa một cái gì đó bạn nhận thấy trong khi sử dụng thư viện hỗ trợ hoặc bạn đã đọc về nó ở đâu đó? Có thêm thông tin nào bạn có thể cung cấp về nó không? Cảm ơn!
Piovezan 2/2/2015

1
@Piovezan nó có thể được suy luận ngầm từ các tài liệu. Ví dụ: tài liệu startTransaction () đọc như sau: "Điều này là do khung công tác đảm nhiệm việc lưu các đoạn hiện tại của bạn ở trạng thái (...)". Tôi cũng đã mã hóa các ứng dụng của mình với hành vi dự kiến ​​này từ khá lâu rồi.
Ricardo

1
@Ricardo có áp dụng điều này nếu sử dụng ViewPager không?
Derek Beattie

1
Thông thường có, trừ khi bạn thay đổi hành vi mặc định khi thực hiện FragmentPagerAdapterhoặc FragmentStatePagerAdapter. FragmentStatePagerAdapterVí dụ, nếu bạn nhìn vào mã của , bạn sẽ thấy rằng restoreState()phương thức khôi phục các đoạn từ FragmentManagertham số bạn đã truyền dưới dạng tham số khi tạo bộ điều hợp.
Ricardo

4
Tôi nghĩ rằng sự đóng góp này là câu trả lời tốt nhất cho câu hỏi ban đầu. Đó cũng là cái mà - theo tôi - phù hợp nhất với cách thức hoạt động của nền tảng Android. Tôi khuyên bạn nên đánh dấu câu trả lời này là câu "Được chấp nhận" để giúp người đọc tương lai tốt hơn.
dbm

17

Tôi chỉ muốn đưa ra giải pháp mà tôi đã đưa ra để xử lý tất cả các trường hợp được trình bày trong bài đăng này mà tôi bắt nguồn từ Vasek và devconsole. Giải pháp này cũng xử lý trường hợp đặc biệt khi điện thoại bị xoay nhiều lần trong khi các mảnh vỡ không nhìn thấy được.

Đây là tôi lưu trữ gói để sử dụng sau này vì onCreate và onSaveInstanceState là các cuộc gọi duy nhất được thực hiện khi đoạn không hiển thị

MyObject myObject;
private Bundle savedState = null;
private boolean createdStateInDestroyView;
private static final String SAVED_BUNDLE_TAG = "saved_bundle";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        savedState = savedInstanceState.getBundle(SAVED_BUNDLE_TAG);
    }
}

Vì killView không được gọi trong tình huống xoay vòng đặc biệt, chúng ta có thể chắc chắn rằng nếu nó tạo trạng thái, chúng ta nên sử dụng nó.

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}

Phần này sẽ giống nhau.

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}

Bây giờ đây là phần khó khăn. Trong phương thức onActivityCreated của tôi, tôi khởi tạo biến "myObject" nhưng xoay vòng xảy ra onActivity và onCreateView không được gọi. Do đó, myObject sẽ không có giá trị trong tình huống này khi định hướng xoay nhiều lần. Tôi khắc phục điều này bằng cách sử dụng lại cùng một gói đã được lưu trong onCreate như gói đi ra.

    @Override
public void onSaveInstanceState(Bundle outState) {

    if (myObject == null) {
        outState.putBundle(SAVED_BUNDLE_TAG, savedState);
    } else {
        outState.putBundle(SAVED_BUNDLE_TAG, createdStateInDestroyView ? savedState : saveState());
    }
    createdStateInDestroyView = false;
    super.onSaveInstanceState(outState);
}

Bây giờ bất cứ nơi nào bạn muốn khôi phục trạng thái, chỉ cần sử dụng gói saveState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}

Bạn có thể cho tôi biết ... "MyObject" ở đây là gì không?
kavie

2
Bất cứ điều gì bạn muốn nó được. Nó chỉ là một ví dụ đại diện cho một cái gì đó sẽ được lưu trong gói.
DroidT

3

Cảm ơn DroidT , tôi đã thực hiện điều này:

Tôi nhận ra rằng nếu Fragment không thực thi onCreateView (), thì khung nhìn của nó không được khởi tạo. Vì vậy, nếu đoạn trên ngăn xếp phía sau không tạo ra các khung nhìn của nó, tôi lưu trạng thái được lưu trữ cuối cùng, nếu không, tôi xây dựng gói riêng của mình với dữ liệu tôi muốn lưu / khôi phục.

1) Mở rộng lớp này:

import android.os.Bundle;
import android.support.v4.app.Fragment;

public abstract class StatefulFragment extends Fragment {

    private Bundle savedState;
    private boolean saved;
    private static final String _FRAGMENT_STATE = "FRAGMENT_STATE";

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (getView() == null) {
            state.putBundle(_FRAGMENT_STATE, savedState);
        } else {
            Bundle bundle = saved ? savedState : getStateToSave();

            state.putBundle(_FRAGMENT_STATE, bundle);
        }

        saved = false;

        super.onSaveInstanceState(state);
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        if (state != null) {
            savedState = state.getBundle(_FRAGMENT_STATE);
        }
    }

    @Override
    public void onDestroyView() {
        savedState = getStateToSave();
        saved = true;

        super.onDestroyView();
    }

    protected Bundle getSavedState() {
        return savedState;
    }

    protected abstract boolean hasSavedState();

    protected abstract Bundle getStateToSave();

}

2) Trong Fragment của bạn, bạn phải có điều này:

@Override
protected boolean hasSavedState() {
    Bundle state = getSavedState();

    if (state == null) {
        return false;
    }

    //restore your data here

    return true;
}

3) Ví dụ: bạn có thể gọi hasSattedState trong onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}

-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();

1
Không liên quan đến câu hỏi ban đầu.
Jorge E. Hernández
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.