본문 바로가기

Android/지식저장소

[Android] BaseSavedState 를 이용하여 View 에 상태 저장하기

안드로이드는 화면을 회전시키는 것과 같이 configuration change 가 발생하면 액티비티가 재생성된다. (onDestroy() 후에 onCreate() 부터 라이프사이클이 다시 시작되기 때문) 따라서 뷰에 어떤 정보를 저장하고있었다면 모두 잃게 될 것이다. 이를 해결하기 위해 다양한 방법이 공식문서에 소개되어 있지만, 여기에 자세히 설명되어있지 않은 방법이 있다. 내가 영어를 못해서그렇지, 이미 있을 수도 있음

 

그 방법은 바로 View 클래스의 static inner class 로 BaseSavedState에 내가 저장하고 싶은 정보를 담아 복원하면 된다. Configuration change 가 일어나도 정보가 저장될 뿐더러 메모리 부족으로 프로세스가 kill 당하더라도 데이터를 유지할 수 있다. 원리는 Parcelable 로 감싸서 IPC 로 어딘가에 저장하도록 구현되어있겠지..? (내 추측)

TextView 나 EditText 도 위 방법으로 데이터를 저장/복원하고있기 때문에, 세팅된 데이터는 configuration change 가 일어나도 데이터가 사라지지 않는걸 확인할 수 있다. 아래 TextView 클래스 내에 구현된 SavedState 를 보면 대충 어떤 느낌인지 이해할 수 있을 것이다.
더보기
더보기
public static class SavedState extends BaseSavedState {
        int selStart = -1;
        int selEnd = -1;
        CharSequence text;
        boolean frozenWithFocus;
        CharSequence error;
        ParcelableParcel editorState;  // Optional state from Editor.

        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(selStart);
            out.writeInt(selEnd);
            out.writeInt(frozenWithFocus ? 1 : 0);
            TextUtils.writeToParcel(text, out, flags);

            if (error == null) {
                out.writeInt(0);
            } else {
                out.writeInt(1);
                TextUtils.writeToParcel(error, out, flags);
            }

            if (editorState == null) {
                out.writeInt(0);
            } else {
                out.writeInt(1);
                editorState.writeToParcel(out, flags);
            }
        }

        @Override
        public String toString() {
            String str = "TextView.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " start=" + selStart + " end=" + selEnd;
            if (text != null) {
                str += " text=" + text;
            }
            return str + "}";
        }

        @SuppressWarnings("hiding")
        public static final Parcelable.Creator<SavedState> CREATOR =
                new Parcelable.Creator<SavedState>() {
                    public SavedState createFromParcel(Parcel in) {
                        return new SavedState(in);
                    }

                    public SavedState[] newArray(int size) {
                        return new SavedState[size];
                    }
                };

        private SavedState(Parcel in) {
            super(in);
            selStart = in.readInt();
            selEnd = in.readInt();
            frozenWithFocus = (in.readInt() != 0);
            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);

            if (in.readInt() != 0) {
                error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            }

            if (in.readInt() != 0) {
                editorState = ParcelableParcel.CREATOR.createFromParcel(in);
            }
        }
    }

 

그리고 마지막으로 아래 예제코드와 함께 설명은 주석으로 정리를 끝내겠다. TextView 에서는 좀 더 상세하게 구현한 것 같은데, 아래만 해도 충분하다. (내 경험)

// View - 상태 복원 메서드 
override fun onRestoreInstanceState(state: Parcelable?) {
    if (state is SavedState) {
    	// 부모의 상태 복구
        super.onRestoreInstanceState(state.superState)
        
        // 내 상태(커스텀뷰) 복구
        커스텀뷰.정보1 = state.정보1
        커스텀뷰.정보2 = state.정보2
        커스텀뷰.정보3 = state.정보3
    } else {
        super.onRestoreInstanceState(state)
    }
}

// View - 상태 저장 메서드
override fun onSaveInstanceState(): Parcelable? {
    val parcelable = super.onSaveInstanceState()

    // 저장할게 없으면 그대로 null 리턴
    if (parcelable == null) {
        return parcelable
    }

    // 저장할게 있으면 부모 상태와 함께 내 상태 저장
    return SavedState(parcelable).apply {
        정보1 = 커스텀뷰.정보1
        정보2 = 커스텀뷰.정보2
        정보3 = 커스텀뷰.정보3
    }
}


// View 안에 inner class 로 작성
private class SavedState : BaseSavedState {
    constructor(superState: Parcelable) : super(superState)
    
    // Parcel 객체에 상태가 저장되어있으므로, 이 객체로부터 저장된 상태를 복구하기 위한 생성자
    // 순서가 매우 중요
    private constructor(source: Parcel) : super(source) {
        커스텀뷰.정보1 = source.readParcelable(Parcelable클래스::class.java.classLoader)
        커스텀뷰.정보2 = source.readLong()
        커스텀뷰.정보3 = source.readInt()
    }

    companion object {
        @JvmField
        val CREATOR = object : Parcelable.Creator<SavedState> {
            override fun createFromParcel(source: Parcel): SavedState {
                return SavedState(source)
            }

            override fun newArray(size: Int): Array<SavedState?> {
                return arrayOfNulls(size)
            }
        }
    }

    var 정보1: Parcelable클래스? = null
    var 정보2: Long = 0L
    var 정보3: Int = 0

    override fun writeToParcel(out: Parcel, flags: Int) {
        super.writeToParcel(out, flags)
        
        // 순서가 매우 중요하다. 생성자와 동일해야함
        out.writeParcelable(정보1, flags) // 여기 들어가는 flag 는 뭔지 안찾아봤다. 알아볼것...
        out.writeLong(정보2)
        out.writeInt(정보3)
    }
}

 

그런데 사실 AAC 뷰모델을 사용하고있다면, 아마도 뷰에 데이터를 저장해야할 경우는 많이 없을 것이다..!