안녕하세요. 점냥입니다
작년 11월 글 이후로 6개월 만이네요. 우연히 처음 저의 블로그에 들어와 주신 분일 수도 있지만 만약 이 글로 인해 오랜만에 다시 찾아와 주신 분이라면 먼저 감사의 인사를 드리고 싶습니다.
6개월 공백의 이유를 물어보신다면 바쁜 활동으로 인해..라고 대답하고 싶지만 게으름 때문입니다.
취직으로 인해 몸과 마음의 긴장이 풀리면서 지금까지 자기 계발 시간 없이 게임만 한 것 같습니다.. 여느 때와 다르지 않게 일요일 새벽까지 게임을 하고 침대에 누웠는 데 그런 생각이 들더라고요. "주말이 벌써 끝났네. 나 이번 주말에 뭐했더라.. 게임만 했네" 내 자신이 한심하더군요. 게임 세계에서 즐거웠던 시간들이 컴퓨터를 끄고 침대에 눕자마자 저번 주와 변하지 않은 나를 마주하다 보니.. 이제는 달라져보려고 합니다. 이 블로그가 그 첫 출발점입니다.
1. Android에서 제공해주는 Ui View 상태 저장
2. ViewTree 내에 동일한 ID을 가진 위젯이 존재할 경우 UI 상태 저장 오류 해결 방법
UI View 상태 저장과 복원이 왜 필요한가요?
우선 이번 글의 주제에 대해 이야기하기 전에 UI 상태 저장과 복원에 대해서 알아야 합니다.
관련 주제에 대해 공부를 하면서 참고한 블로그에서 발견한 이미지인데 너무 딱 알맞은 이미지라 다시 가져와봤어요.
수많은 입력창이 보입니다. Android 앱에 비유해서 표현하자면 어떤 앱에서는 한 페이지에서 EditText가 10개 정도 있을 수도 있겠죠? (보통 다음 버튼으로 넘어가게 구현하지만.. 그렇다고 치자고요)
인내심 좋은 사용자가 꾸역꾸역 8개의 EditText까지 입력을 했을 때 갑자기 가로모드와 세로 모드 등으로 전환되거나 운이 나쁘게 시스템에서 메모리 이슈로 인해 앱 프로세스를 강제 종료시켰다면 사용자는 입력했던 모든 값들이 초기화된 화면을 마주하게 될 것입니다. ㅠ
그런데 말씀해주신 기능은 자동으로 제공해주는 것이 아닌가요? UI 상태 저장에 대해서 크게 신경 써본 적은 없어요.
네 맞습니다. Android Widget에서는 상태 저장 기능을 자동으로 제공해주고 있습니다. 다만 View id를 지정한 Widget만 자동으로 제공해주고 있었습니다.
이번 기회에 내부 구조를 살펴봅시다
우리가 흔히 사용하는 Activity와 Fragment의 onSaveInstanceState 메서드처럼 View에서도 상태를 저장하고 복원하는 메서드가 존재합니다! 좀 더 자세히 관련 메서드에 대해서 알아볼까요? 프레임 웍에서 기본적으로 제공해주는 뷰의 상태 저장과 복원하는 메서드는 모두 View에 정의되어 있습니다.
//View.java
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { // (1), (2)
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState(); // (3)
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
container.put(mID, state); // (4)
}
}
}
1. mID 값이 NO_ID가 아닐 경우, UI 상태 저장 로직을 실행
2. viewFlags가 saveDisable 속성을 포함하지 않을 경우
3. onSaveInstanceState() 함수를 통해 Parcelable 객체를 가져옴
4. 상태를 저장한 Bundle을 mID를 키 값으로 container에 저장함
1번의 mID는 어떤 값을 가지고 있을까요?
case com.android.internal.R.styleable.View_id:
mID = a.getResourceId(attr, NO_ID);
break;
View에서 attr로 초기화가 될 때 xml에서 andorid:id에 지정한 id가 mID 변수에 저장이 됩니다. 저장하지 않는 다면 NO_ID 값이 들어갑니다. 이제 위 코드를 다시 보면 함수 내 첫 분기에서 mID가 NO_ID 값인지를 체크하기 때문에 이것이 Android 프레임웍에서 자동으로 제공해주는 상태 저장 기능을 사용하기 위해서는 View id를 지정해야 하는 이유였습니다.
2번의 SAVE_DISABLE_MASK는 어떤 역할일까요?
상태저장 때문이 아니라 우리는 제약 조건을 명시하기 위해 뷰 ID를 지정하는 경우가 많습니다. 그런데 Id를 지정하면 자동으로 상태저장이 됩니다. 그런데 이 기능이 필요하지 않을 수 있죠. `android:saveEnabled` 속성을 사용하여 상태저장 기능을 비활성화 할 수 있습니다.
case com.android.internal.R.styleable.View_saveEnabled:
if (!a.getBoolean(attr, true)) {
viewFlagValues |= SAVE_DISABLED;
viewFlagMasks |= SAVE_DISABLED_MASK;
}
break;
3번의 onSaveInstanceState() 함수는 어떤 값을 반환할까요?
onSaveInstanceState()는 protected 접근 지정자로 선언되어 있는 메소드로 Widget은 이 함수를 오버라이드해서 자신들의 뷰에 알맞은 값들을 번들에 저장하고 반환하고 있습니다.
예를 들어 TextView의 onSaveInstanceState()입니다.
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
final boolean freezesText = getFreezesText();
boolean hasSelection = false;
int start = -1;
int end = -1;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);
if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}
ss.text = sp;
} else {
ss.text = mText.toString();
}
}
if (hasSelection) {
// XXX Should also save the current scroll position!
ss.selStart = start;
ss.selEnd = end;
}
if (isFocused() && start >= 0 && end >= 0) {
ss.frozenWithFocus = true;
}
ss.error = getError();
if (mEditor != null) {
ss.editorState = mEditor.saveInstanceState();
}
return ss;
}
return superState;
}
text 외에 selection 등의 정보를 추가로 저장하고 있네요.
뷰의 상태를 복원할 때는 어떤 로직이 있을까요? 상태 저장할때랑 크게 다르지 않습니다.
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID) { // (1)
Parcelable state = container.get(mID); // (2)
if (state != null) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
onRestoreInstanceState(state);
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onRestoreInstanceState()");
}
}
}
}
1. mID 값이 NO_ID가 아님을 체크
2. container에서 mID 값으로 상태 저장한 값이 담긴 Parcelable 객체를 가져옴
3. onRestoreInstanceState 함수 매개변수로 가져온 값을 넘겨 복원
상태 저장할 때 mID 값으로 SparseArray라는 Map 자료구조에 저장을 했고
복원을 할때는 동일한 mID 값으로 저장된 Bundle을 가져옵니다. 그리고 onRestoreInstanceState 함수를 호출하여 복원하는 작업을 수행합니다.
onRestoreInstanceState도 protected 접근 지정자로 선언되어 있는 메소드로 Widget에서 재정의하고 있습니다.
이번에도 TextView의 코드를 봅시다!
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// XXX restore buffer type too, as well as lots of other stuff
if (ss.text != null) {
setText(ss.text);
}
if (ss.selStart >= 0 && ss.selEnd >= 0) {
if (mSpannable != null) {
int len = mText.length();
if (ss.selStart > len || ss.selEnd > len) {
String restored = "";
if (ss.text != null) {
restored = "(restored) ";
}
Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd
+ " out of range for " + restored + "text " + mText);
} else {
Selection.setSelection(mSpannable, ss.selStart, ss.selEnd);
if (ss.frozenWithFocus) {
createEditorIfNeeded();
mEditor.mFrozenWithFocus = true;
}
}
}
}
if (ss.error != null) {
final CharSequence error = ss.error;
// Display the error later, after the first layout pass
post(new Runnable() {
public void run() {
if (mEditor == null || !mEditor.mErrorWasChanged) {
setError(error);
}
}
});
}
if (ss.editorState != null) {
createEditorIfNeeded();
mEditor.restoreInstanceState(ss.editorState);
}
}
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}
추가로 저 함수들을 ViewGroup에서 직접 호출해주고 있습니다.
ViewGroup 내부에는 Widget과 또 다른 ViewGroup 등 여러 Child를 포함되어 있기 때문에 반복문을 돌면서 각각 상태 저장과 복원 메서드를 호출해주고 있습니다.
'Android > Common' 카테고리의 다른 글
[Android] Bottom Navigation Bar State with Compose - (2) (0) | 2022.06.12 |
---|---|
[Android] ViewTree 내에 동일한 ID을 가진 위젯이 존재할 경우 UI 상태 저장 오류 해결 방법 - 2 (0) | 2022.05.13 |
[Android] Paging3 + Admob Native Ad (0) | 2021.11.01 |
[Android] 좋아요 기능으로 알아보는 더블 클릭 방지하는 방법 (0) | 2021.09.04 |
[Android] fromHtml Bullet 속성 변경하기 (0) | 2021.07.03 |