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

나는 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() 데 다른 단점이 있다고 생각 했습니다 . 기본적으로 프래그먼트 현재 표시되어 있으면 두 함수를 호출하는 순서가 반대이므로 두 가지 상황을 고려해야합니다. 더 깨끗하고 정확한 해결책이 있어야합니다!



답변

인스턴스 상태를 올바르게 저장하려면 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);
    }

}

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


답변

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

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를 사용 하여 두 항목을 동기화하는 것이 항상 가능합니다. 그래도 마지막 부분은 매우 깨끗하지 않습니다.


답변

최신 지원 라이브러리에서는 여기서 논의 된 솔루션이 더 이상 필요하지 않습니다. 를 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);
    }
...
}

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


답변

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


답변

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
}


답변

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


답변