<정리> 챌린지반 과제2 - 5
저번에 그 아찔한 복습시간을 마치고 오늘은 진짜로 해야하는 과제 내용을 진행했다.
아주 짧은 내용이었지만 이를 구현하기 위해서 걸린 시간은 장난 없었다.
그리고 완성했다고 생각하고 튜터님께 갔다가 다시 갈아엎는 일도 있어서 오후 시간이 참 빨리가더라
1. SharedViewModel
처음에 나는 '한 뷰모델을 그냥 두 페이지에서 같이 쓰면서 모든 기능을 한번에 처리하면 되지 않을까?' 생각이 들었기 때문에 프래그먼트 별로 뷰모델을 만들지 않고 하나의 뷰모델을 공유하도록 짰었다.
그렇게 하다보니 따로 아이템 클래스를 만들지 않고 하나로, 따로 이벤트 클래스를 만들지 않고 하나로 구성해서 분기를 시키느라 한 뷰모델 내의 코드가 엄청나게 길어졌다.
그렇게 쭉- 달려서 과제 기능이 동작하는 것까지 확인하고 피드백 받으러 튜터님께 갔는데 이게 아니었단다.
각각의 프래그먼트마다 뷰모델이 있고 그 위에 공유 뷰모델이 있는 구조로 했어야 했다...
오우...!! 그냥 환호성을 지를 뻔 했지만 잘 참고 이후 튜터님이 해주시는 힌트들 적고 다시 시작했다.
정말 다행인 것은 코드를 완전 새로 짜는 것이 아니라 3개로 나누기만... 나누기만 하면 되니까 괜찮아...
만들었던 뷰모델을 복붙해서 프래그먼트마다 넣어줬다.
그리고 안 쓰는 건 정리하고 새로운 마음으로 시작했다.
먼저 공유 뷰모델의 가장 큰 기능은 다른 프래그먼트로 갈 데이터를 다른 프래그먼트의 데이터 클래스 형식에 맞춰 변환하여 보내주는 것이었다.
그래서 변환함수를 만들고 이 변환된 데이터를 받을 이벤트 라이브 데이터를 만들어서 넣어줬다.
private val _sharedEvent: MutableLiveData<SharedEvent> = MutableLiveData()
val sharedEvent: LiveData<SharedEvent> get() = _sharedEvent
fun sendTodoItem(item: TodoListItem) {
_sharedEvent.value = SharedEvent.SendToBookmark(changeToBookmarkItem(item))
}
fun sendBookmarkItem(entryType: ManageTodoEntryType, item: BookmarkListItem) {
_sharedEvent.value = SharedEvent.SendToTodo(entryType, changeToTodoItem(item))
}
이걸 받는 프래그먼트에서 옵저빙을 하다가 자신한테 온 이벤트를 확인하면 UI를 업데이트 시키도록 했다.
sharedEvent.observe(viewLifecycleOwner) {
when (it) {
is SharedEvent.SendToBookmark -> {
viewModel.updateItem(it.item)
}
else -> Unit
}
}
처음에 짤 때는 한 뷰모델 내에서 데이터 형식을 변환하는게 복잡하고 어려워서 하지 않았었는데 이렇게 나눠서 진행하니까 많이 편리해졌다.
그리고 확실히 각각의 프래그먼트에 대한 UI만 처리하니까 코드를 볼 때 헷갈리는 게 적어지더라
2. 데이터 전달
프래그먼트에서 공유 뷰모델로 데이터를 전달할 때는 이전에 프래그먼트에서 액티비티로 이벤트를 발생시켜 보냈듯이 했다.
sealed interface 안에 data class로 다른 프래그먼트로 보낸다는 의미의 이벤트를 만들었다.
data class SendContent(
val item: TodoListItem
) : TodoListEvent
그리고 데이터를 수정하거나 스위치를 눌렀을 때 변경된 데이터가 이벤트로 들어가도록 했다.
그러면 이벤트가 변경된걸 알아차린 옵저버가 공유 뷰모델로 데이터를 보낸다.
event.observe(viewLifecycleOwner) {
when (it) {
is TodoListEvent.OpenContent -> {
updateTodoLauncher.launch(
ManageTodoActivity.newIntentForUpdate(
requireContext(),
it.item,
it.position
)
)
}
is TodoListEvent.SendContent -> {
sharedViewModel.sendTodoItem(it.item)
}
}
}
받는 쪽은 공유 뷰모델의 이벤트 라이브데이터를 옵저빙하다가 변경되고 데이터가 넘어오면 뷰모델로 해당 데이터를 또 넘겨 UI를 업데이트한다.
3. UI 업데이트
TodoList 프래그먼트에서 스위치 눌러 ON되면 Bookmark 프래그먼트에 뜨고, TodoList 프래그먼트에서 아이템을 수정하면 Bookmark 프래그먼트에서 수정되고, TodoList 프래그먼트에서 삭제하면 Bookmark 프래그먼트에서도 삭제되도록 UI를 업데이트하는 건 공유뷰모델을 구성하고나서 가장 오래 걸린 작업이었다.
스위치 ON/OFF는 양쪽이 비슷했다. 먼저 현재 데이터의 isBookmarked를 뒤집고 현 프래그먼트의 UI를 갱신하고 공유뷰모델로 데이터를 보내주는 흐름이었다.
물론 Bookmark에서 데이터를 보내줄 때는 엔트리 타입을 같이 보내주고, TodoList에서는 엔트리 타입을 안 보내주는 차이가 있긴 했다.
이외의 액티비티를 갔다와서 값이 수정 혹은 삭제되는 경우는 거의 똑같이 현 UI 갱신을 하고 공유뷰모델로 엔트리타입과 데이터를 보내줬다.
유일하게 다른 건 프래그먼트끼리 액티비티 갔다와서 처리된 데이터를 넘겨받았을 때 UI 갱신하는 부분이었다.
Bookmark에서는 엔트리타입이 없어도 삭제, 수정, 추가가 되는데 TodoList에서는 엔트리타입 없이 하기에는 정보가 부족했기에 엔트리타입을 받아서 진행했다.
4. offscreenPageLimit
이제 단 한가지 문제가 남아있었다.
바로 가장 처음 앱을 실행시키고 새로운 TodoItem을 여러개 만든 다음 북마크를 다 체크하고 북마크로 넘어갔을 때 가장 마지막에 북마크 체크한 TodoItem만 UI에 갱신되는 문제였다.
딱 봐도 처음에 북마크를 생성하지 않아서 옵저빙이 되지 않는 문제로 보였지만 마땅한 해결책이 생각나지 않아서 조금 고민하다가 바로 튜터님한테 갔다.
튜터님이 쭉 얘기를 듣고 동작을 보여드리니까 해주시는 말이 바로 "뷰페이저에 offscreenPageLimit를 1로 해봐요" 였다.
아주 짧고 명료하고 간단했다.
viewPagerMain.offscreenPageLimit = 1
그리고 다시 실행했더니 되었다.(될 때 "왜 되지?"라면서 머리카락을 부여잡던 내가 생각나네...ㅎ)
이게 도대체 뭐냐고 물어보니까 공부해보라고 하셨고 이게 뷰페이저에서 중요한 것 중 하나라고 하셨다.
그래서 한 번 알아보자
offscreenPageLimit는 ViewPager 클래스에서 사용되는 속성이다.
현재 표시된 프래그먼트 주변에 몇 개의 프래그먼트를 미리 생성해놓을 지 결정한다.
이번처럼 offscreenPageLimit를 1로 설정하면 현재 프래그먼트 외에 한 개를 미리 생성해놓는다.
그러면 해당 프래그먼트에서 생성될 때 호출되는 함수들이 실행이 되고 현재 프래그먼트에서 생성된 프래그먼트에게 뭔가 요구를 해도 바로 반영될 수 있음을 알 수 있다.
부드러운 전환이 가능한 것도 장점이다.
하지만 너무 값을 크게 잡아버리면 메모리를 너무 잡아먹어버릴 수 있기 때문에 필요에 따라 적절히 설정하는 것이 중요하다.
이제 또 다음 주 월요일에 수업들으면서 '와~ 이런 방법이 있다고?!'하면서 이마 탁 치고 머리 띵해질 나를 생각하니 또 아찔하다...
그래도 여기까지 잘 도착한 나 자신 칭찬해~~