백 스택에 Fragments의 인스턴스 상태를 올바르게 저장하는 방법은 무엇입니까?


489

나는 SO에 대해 비슷한 질문을 많이 발견했지만 불행히도 내 요구 사항을 충족시키는 대답은 없습니다.

세로 및 가로에 대한 레이아웃이 다르고 백 스택을 사용하고 있습니다. 백 스택을 사용 setRetainState()하면 구성 변경 루틴 을 사용하지 못하고 트릭합니다.

TextViews에서 사용자에게 특정 정보를 표시하지만 기본 핸들러에는 저장되지 않습니다. 활동만을 사용하여 신청서를 작성할 때 다음이 잘 작동했습니다.

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

Fragments를 사용하면 매우 구체적인 상황에서만 작동합니다. 특히 끔찍한 부분은 조각을 교체하여 백 스택에 넣은 다음 새 조각이 표시되는 동안 화면을 회전시키는 것입니다. 내가 이해 한 바에 따르면, 오래된 조각은 onSaveInstanceState()교체 될 때 호출을받지 못하지만 어떻게 든 연결되어 있으며이 Activity메소드는 View더 이상 존재하지 않을 때 나중에 호출 되므로 내 TextView결과를 NullPointerException.

또한 내 참조를 유지 TextViews하는 것이 Fragments에 괜찮더라도 s에 대한 좋은 아이디어가 아니라는 것을 알았 습니다 Activity. 이 경우 onSaveInstanceState()실제로 상태를 저장하지만 조각이 숨겨져있을 때 화면이 두 번 회전 하면 조각이 onCreateView()새 인스턴스에서 호출되지 않으므로 문제가 다시 나타납니다 .

상태 onDestroyView()를 일부 Bundle유형의 클래스 멤버 요소 (실제로 하나의 데이터가 아니라 더 많은 데이터 TextView)에 저장하고 저장 하는onSaveInstanceState() 데 다른 단점이 있다고 생각 했습니다 . 기본적으로 프래그먼트 현재 표시되어 있으면 두 함수를 호출하는 순서가 반대이므로 두 가지 상황을 고려해야합니다. 더 깨끗하고 정확한 해결책이 있어야합니다!


1
여기에 자세한 설명과 함께 좋은 예가 있습니다. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
emunee.com 링크는 두 번째입니다. 그것은 나를 위해 스틱 UI 문제를 해결했습니다!
Reenactor Rob

답변:


541

인스턴스 상태를 올바르게 저장하려면 Fragment다음을 수행해야합니다.

1. 프래그먼트 onSaveInstanceState()에서 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. 그리고 중요한 점은 , 활동에, 당신의 조각의 인스턴스를 저장해야 onSaveInstanceState()와의 복원 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);
    }

}

도움이 되었기를 바랍니다.


7
이것은 나를 위해 완벽하게 작동했습니다! 해결 방법, 해킹 없음은 이런 식으로 의미가 있습니다. 이것에 감사합니다. 몇 시간 동안 검색에 성공했습니다. 조각으로 값의 SaveInstanceState (), 조각을 들고 활동에 조각을 저장하고 복원 :)
MattMatt

77
mContent 란 무엇입니까?
wizurd

14
@wizurd mContent는 Fragment이며 액티비티에서 현재 프래그먼트의 인스턴스를 참조합니다.
ThanhHH

13
이것이 조각의 인스턴스 상태를 백 스택에 저장하는 방법을 설명 할 수 있습니까? 그것이 OP가 요구 한 것입니다.
hitmaneidos

51
이것은 질문과 관련이 없습니다. 프래그먼트를 백 스택에 넣을 때 onSaveInstance가 호출되지 않습니다
Tadas Valaitis

87

이것이 내가 지금 사용하는 방식입니다 ... 매우 복잡하지만 적어도 가능한 모든 상황을 처리합니다. 누군가가 관심이 있다면.

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());
    }

    /* (...) */

}

또는 수동으로 표시된 데이터 View를 변수 로 유지하고 데이터 View를 표시하는 데만 s를 사용 하여 두 항목을 동기화하는 것이 항상 가능합니다. 그래도 마지막 부분은 매우 깨끗하지 않습니다.


68
두 조각이있는 경우 : 이것은 내가 지금까지 찾은 가장 좋은 방법이지만, 나머지 (다소 이국적인) 문제는 여전히 존재하는 AB경우, A가기 backstack에 현재이고 B, 당신은의 상태를 볼 수 잃게된다 A(보이지 않는를 1) 디스플레이를 두 번 돌리면 . 문제는 onCreateView()이 시나리오에서 호출되지 않고 단지라는 것입니다 onCreate(). 따라서 나중에 onSaveInstanceState()상태를 저장할 뷰가 없습니다. 전달 된 상태를 저장 한 다음 저장해야합니다 onCreate().
devconsole

7
@devconsole이 의견에 대해 최대 5 개의 투표권을 줄 수 있기를 바랍니다. 이 두 번의 회전은 며칠 동안 나를 죽였습니다.
DroidT

큰 답변 감사합니다! 그래도 질문이 하나 있습니다. 이 프래그먼트에서 모델 객체 (POJO)를 인스턴스화하기 가장 좋은 곳은 어디입니까?
Renjith

7
다른 사람들의 시간을 절약 App.VSTUP하고 , App.STAV그들이 얻으려고하는 객체를 나타내는 문자열 태그입니다. 예 : savedState = savedInstanceState.getBundle(savedGamePlayString);또는savedState.getDouble("averageTime")
Tanner Hallman

1
이것은 아름다움의 것입니다.
Ivan

61

최신 지원 라이브러리에서는 여기서 논의 된 솔루션이 더 이상 필요하지 않습니다. 를 Activity사용하여 원하는대로 조각을 재생할 수 있습니다 FragmentTransaction. 조각 또는 ID로 태그를 식별 할 수 있는지 확인하십시오.

를 호출 할 때마다 조각을 다시 만들지 않으면 조각이 자동으로 복원됩니다 onCreate(). 대신, savedInstanceStatenull이 아닌지 확인 하고이 경우 생성 된 조각에 대한 이전 참조를 찾아야합니다.

예를 들면 다음과 같습니다.

@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);
    }
...
}

그러나 조각의 숨겨진 상태를 복원 할 때 현재 버그 가 있습니다 . 활동에서 단편을 숨기고있는 경우이 경우이 상태를 수동으로 복원해야합니다.


2
이 수정 프로그램은 지원 라이브러리를 사용하는 동안주의를 기울였습니까? 더 많은 정보를 제공 할 수 있습니까? 감사!
Piovezan

1
@Piovezan 그것은 문서에서 암시 적으로 추론 될 수 있습니다. 예를 들어 beginTransaction () 문서는 다음과 같이 읽습니다. "프레임 워크가 현재 조각을 상태 (...)에 저장하기 때문에 처리합니다." 나는 또한 꽤 오랫동안 내 예상 동작으로 앱을 코딩하고 있습니다.
Ricardo

1
ViewPager를 사용하는 경우 @Ricardo가 적용됩니까?
데릭 비티

1
FragmentPagerAdapter또는 구현에서 기본 동작을 변경하지 않는 한 일반적으로 그렇습니다 FragmentStatePagerAdapter. FragmentStatePagerAdapter예를 들어 의 코드를 보면 어댑터를 작성할 때 매개 변수로 전달한 restoreState()조각에서 메소드가 조각을 복원 함을 알 FragmentManager수 있습니다.
Ricardo

4
이 기여가 원래의 질문에 대한 가장 좋은 답변이라고 생각합니다. 내 의견으로는 안드로이드 플랫폼의 작동 방식과 가장 잘 맞는 것이기도합니다. 장래 독자들에게 도움 이되도록이 답변을 "허용됨"으로 표시하는 것이 좋습니다 .
dbm

17

방금 Vasek 및 devconsole에서 파생 된이 게시물에 제시 된 모든 사례를 처리하는 솔루션을 제공하고 싶습니다. 이 솔루션은 또한 프래그먼트가 보이지 않는 동안 전화가 두 번 이상 회전하는 특수한 경우를 처리합니다.

onCreate와 onSaveInstanceState가 프래그먼트가 보이지 않을 때만 호출되므로 나중에 사용할 수 있도록 번들을 저장했습니다.

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

특수 회전 상황에서 destroyView가 호출되지 않기 때문에 상태가 생성되면 사용해야한다는 것을 확신 할 수 있습니다.

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

이 부분은 동일합니다.

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

이제 여기 까다로운 부분이다. 내 onActivityCreated 메서드에서 "myObject"변수를 인스턴스화하지만 onActivity에서 회전이 발생하고 onCreateView가 호출되지 않습니다. 따라서 방향이 두 번 이상 회전하면이 상황에서 myObject가 null이됩니다. onCreate에 저장된 동일한 번들을 나가는 번들과 재사용 하여이 문제를 해결합니다.

    @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);
}

이제 상태를 복원하려는 경우 savedState 번들을 사용하십시오.

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

말해 줄 수 있습니까? "MyObject"는 무엇입니까?
kavie

2
당신이 원하는 모든 것. 번들에 저장 될 무언가를 나타내는 예제 일뿐입니다.
DroidT

3

DroidT 덕분에 이것을 만들었습니다.

Fragment가 onCreateView ()를 실행하지 않으면 뷰가 인스턴스화되지 않는다는 것을 알고 있습니다. 따라서 백 스택의 조각이 뷰를 만들지 않으면 마지막 저장 상태를 저장하고, 그렇지 않으면 저장 / 복원하려는 데이터로 자체 번들을 빌드합니다.

1)이 수업을 확장하십시오 :

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) 조각에 다음이 있어야합니다.

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

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

    //restore your data here

    return true;
}

3) 예를 들어 onActivityCreated에서 hasSavedState를 호출 할 수 있습니다.

@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
원래 질문과 관련이 없습니다.
Jorge E. Hernández
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.