본문 바로가기
안드로이드

[안드로이드/Kotlin] EditText SaveState 에러

by krapoi 2023. 10. 31.
반응형

최근 회사에 입사하여 여러 가지 일을 하느라 블로그가 늦었는데, 이번 글은 입사 후 겪은 에러 중 제일 어이없는 에러에 대한 것이다.

 

먼저 문제상황부터 알려 줄 것인데, 내가 Custom EditText를 사용하다가 생긴 일이다. 자세하게 들어가 보자.

 

문제상황

우선 이 Custom EditText의 구조는 Custom ConstraintLayout에 CustomEditText와 ImageView(editText 밑줄 담당)로 이루어져 있고, 이 ConstraintLayout을 뷰에서 사용한다.

즉, ConstraintLayout안에 BaseEditText가 있는 것이다.

이 EditText를 회원가입 페이지에 사용하였는데 총 2개를 사용하였다. (이메일과 비밀번호)

그런데 화면이 바뀐 뒤 다시 돌아오면 두 개의 editText에 적혀있던 text가 통합되어 있었다. (처음에 보고 원인이 감도 안 잡혔음)

뭔 소린지 이해하지 못했다면 사진을 하나 보여주겠다.

이제 정확히 무슨 상황인지 이해되었을 것이다.

문제 상황은 충분히 이해한 것 같으니 원인부터 찾으러 가보자.

 

원인

결론부터 말하자면, EditText가 가지고 있는 SaveInstanceState가 문제였다.

이게 SparseArray를 반환하는데 이때 같은 TextView의 아이디를 반환해서 문제가 된 것이었다.

이해를 돕기 위한 사진 두 장을 보여주겠다.

이런 식으로 상위 뷰인 Constraint는 따로 만들어져 저장되지만, 하위뷰인 BaseEditText는 모두 id가 같기 때문에 같은 Array를 가리키게 된다.

그래서 결국 BaseEditText의 SaveInstaceState값이 같아지는 것이다.

 

이제 원인을 찾았다면 문제를 해결할 수 있다.

 

문제 해결

부모뷰에서 코드를 바꾸는 것만으로도 해결이 가능하다. (해당 글에서는 Custom ConstraintLayout를 말함)

 

해당 부모 뷰에 아래와 같은 코드를 적는다.

override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
    dispatchFreezeSelfOnly(container)
}

override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
    dispatchThawSelfOnly(container)
}

해당 코드로 우리는 부모 뷰가 SaveInstanceState를 반환할 때 하위뷰가 포함되지 않은 부모 뷰 자신만 저장하게 되었다.

 

그러면 이제 하위 뷰들의 SaveInstanceState를 저장하러 가보자.

 

해당 부모 뷰에 다시 아래와 같은 코드를 적는다.

companion object {
    private const val SPARSE_STATE_KEY = "SPARSE_STATE_KEY"
    private const val SUPER_STATE_KEY = "SUPER_STATE_KEY"
}

override fun onSaveInstanceState(): Parcelable? {
    return Bundle().apply {
        putParcelable(SUPER_STATE_KEY, super.onSaveInstanceState())
        putSparseParcelableArray(SPARSE_STATE_KEY, saveChildViewStates())
    }
}

private fun ViewGroup.saveChildViewStates(): SparseArray<Parcelable> {
    val childViewStates = SparseArray<Parcelable>()
    children.forEach { child -> child.saveHierarchyState(childViewStates) }
    return childViewStates
}

private fun ViewGroup.restoreChildViewStates(childViewStates: SparseArray<Parcelable>) {
    children.forEach { child -> child.restoreHierarchyState(childViewStates) }
}

override fun onRestoreInstanceState(state: Parcelable?) {
    var newState = state
    if (newState is Bundle) {
        val childrenState = when {
            SDK_INT >= 33 -> newState.getSparseParcelableArray(SPARSE_STATE_KEY, Parcelable::class.java)
            else -> @Suppress("DEPRECATION") newState.getSparseParcelableArray(SPARSE_STATE_KEY)
        }
        childrenState?.let { restoreChildViewStates(it) }
        newState = when {
            SDK_INT >= 33 -> newState.getParcelable(SUPER_STATE_KEY, Parcelable::class.java)
            else -> @Suppress("DEPRECATION") newState.getParcelable(SUPER_STATE_KEY)
        }
    }
    super.onRestoreInstanceState(newState)
}

SaveInstance를 저장할 때 당연히 부모 뷰가 필요하기에 SUPER_STATE_KEY를 사용하여 부모 뷰를 ParcelAble로 먼저 집어넣어 준다,

그다음 SPARSE_STATE_KEY를 이용하여 자식 뷰들의 SaveInstaceState를 SparseArray를 통해 넣어준다.

 

다음은 불러오기이다.

우선 하위 뷰의 SaveInstanceState를 먼저 가져온 다음 restoreChildViewStates를 이용해 먼저 배치해 준다.

그다음 부모뷰의 State를 가져와 super로 넘겨주면 끝나게 된다.

 

그러면 아래 사진처럼 container가 만들어지게 된다.

이로써 SaveInstanceState문제를 고치게 되었다.

 

외국의 한 글을 보고 고치게 되었는데 아무리 한국어로 찾아도 나오지 않더라. (내가 못 찾았을 수 도 있다.)

그렇기에 그 글을 번역한 후 Deprecate 된 메서드들을 변형 후 이 글을 쓰게 되었다.

외국글이 훨씬 더 설명을 잘했기 때문에 영어에 자신 있으면 한번 읽어보는 것을 추천한다. (번역기는 좀 오역이 있더라)

 

뭐 어쨌든, 나처럼 똑같은 버그에 삽질하지 않길 바란다.

 

 

사진 및 참고한 글

https://www.netguru.com/blog/how-to-correctly-save-the-state-of-a-custom-view-in-android

 

How to Correctly Save the State of a Custom View in Android

How to correctly save and restore Android view state including children using two different methods to avoid unexpected behaviour in recreating lifecycle?

www.netguru.com

 

반응형