아... 쉽지 않다 쉽지않아
어떻게 이렇게 짤 생각을 하셨는지 지금의 나는 영~ 모르겠다!
그래도 어떻게든 따라가려고는 하는데 마무리하고 나서 보니까 이게 또 짜던 대로 짠 거 같기도 한 느낌..?
많은 변화가 있었고 하나씩 되돌아보자
엔트리타입은 넘어가도 되겠죠...?
[깃허브]
https://github.com/heesoo-park/ChallengeRecruitAssignment/tree/week2_contain_ListAdapter
1. 대대적인 이름 변경
뭔가 읽으면서 직관적이지 않다고 생각이 들거나, 이름에 정보가 부족하다 싶은 부분들을 수정하고 보완했다.
클래스, 변수, 파일명 가리지 않고 많이 바뀌어서 이전 포스팅과 다른 이름이 자주 나올 것이다.
2. DataStore 삭제 & TodoListUiState 생성
이렇게 짜고 한번 튜터님께 질문하러 갔다가 튜터님이 이렇게 짜는 거 이제 그만하라고 하셔서 익숙한 방식을 버리기로 했다.
그리고 List<TodoModel>을 변수로 가지는 TodoListUiState 데이터 클래스를 만들었다.
이걸 통해서 TodoList 페이지의 리사이클러뷰를 업데이트하게 된다.
data class에 companion object 붙이는 방식을 알게 됐고 새로운 초기화 방식도 알게 됐다.
data class TodoListUiState(
val todoList: List<TodoModel>
) {
companion object {
fun init() = TodoListUiState(
todoList = emptyList()
)
}
}
3. ManageTodo 액티비티 레이아웃에 수정, 삭제 버튼 추가
ManageTodo 액티비티를 수정, 삭제할 때도 사용하기 위해서 버튼을 추가했고 어떤 엔트리타입인지에 따라 등록 버튼을 보여주거나 수정, 삭제 버튼을 보여줄 것이다.
기본값은 수정, 삭제 버튼이 안 보이는 것으로 잡았다.
if (entryType == ManageTodoEntryType.UPDATE) {
btnRegisterTodoRegister.visibility = View.GONE
btnRegisterTodoDelete.visibility = View.VISIBLE
btnRegisterTodoUpdate.visibility = View.VISIBLE
}
4. 프래그먼트 newInstance() 추가
액티비티에서 어떤 데이터를 원하고 추가 작업을 할지 정한 다음 인텐트를 돌려줬던 newIntent() 함수처럼 프래그먼트에서 newInstance() 함수를 추가했다.
여기서는 그냥 다른 거 없이 프래그먼트 객체를 생성해서 반환해줬다.
companion object {
fun newInstance() = BookmarkListFragment()
}
5. MainTab 데이터 클래스 생성
이전에 뷰페이저 어댑터에서 프래그먼트를 생성할 때 하드 코딩으로 몇 번째에 어떤 프래그먼트를 쓸지를 적었었는데 그런 불편함에서 벗어나고자 프래그먼트와 그에 맞는 탭 아이템 제목을 가지는 데이터 클래스를 만들었다.
이걸 리스트에 넣어서 사용하면 뷰페이저 어댑터에서 편하게 유지 보수가 가능해진다.
title이 Int인 이유는 strings.xml에 들어있는 리소스 아이디를 저장해놓을 것이기 때문이다.
data class MainTab(
val fragment: Fragment,
@StringRes val title: Int
)
6. 뷰페이저 어댑터 코드 변경
앞에서 말한대로 MainTab 데이터 클래스를 담는 리스트를 만들어서 프래그먼트들을 관리하고 프래그먼트를 생성할 때 주어지는 position에 맞는 리스트 값의 fragment만 보내주면 되도록 변경했다.
탭 아이템의 제목을 주는 것도 position에 맞는 리스트 값의 title만 보내주면 된다.
class ViewPagerAdapter(fragmentActivity: FragmentActivity): FragmentStateAdapter(fragmentActivity) {
private val fragmentList = listOf(
MainTab(TodoListFragment.newInstance(), R.string.main_tab_todo),
MainTab(BookmarkListFragment.newInstance(), R.string.main_tab_bookmark)
)
override fun getItemCount(): Int {
return fragmentList.size
}
override fun createFragment(position: Int): Fragment = fragmentList[position].fragment
fun getTitle(position: Int): Int = fragmentList[position].title
}
7. 프래그먼트 RecyclerView.adapter를 ListAdapter로 변경
이번 주차 과제에 가장 큰 주제였다.
ListAdapter로 변경하고 DiffUtil을 사용하는 것까지가 한 세트이긴한데... 뭐 그게 그거지요
ListAdapter는 RecyclerView.Adapter를 베이스로 하는 클래스다.
ListAdapter는 상속받을 때 <> 안에 사용하는 데이터 클래스와 뷰홀더를 받고, () 안에는 처음보는 TodoComparator 객체를 받더라
정확히는 내가 만드는 건데 DiffUtil.ItemCallback을 상속받는 Comparator 객체다.
이름은 맘대로 지어도 되는 거 같은데 아마 비교하는 역할이라 ~Comparator라고 쓰는 거 같다.
class TodoListAdapter : ListAdapter<TodoModel, TodoListAdapter.TodoViewHolder>(TodoComparator())
그리고 나서 추가되는건 TodoComparator 클래스 뿐이고 나머지는 리사이클러뷰 어댑터에서 사용했던 걸 그대로 둔다.
물론 코드는 바꿔야했다.
그냥 쓰려고 했는데 빨간줄이 그냥 쫙! 쫙!
어떻게 바꿨냐면 이전에는 onBindViewHolder에서 xml과 연결시켜놓은 변수의 값을 세팅해줬는데 이제는 그걸 뷰홀더 함수를 호출해서 하는 식으로 바꿨다.
이렇게 하는 건 처음이긴 한데... 왜 이렇게 하는지 이해도 아직 잘 안 되어있긴 한데... 어쨌든 따봉이다!
그리고 리사이클러뷰 아이템 클릭 이벤트에 인터페이스 함수 실행시키는 것도 비슷한 방식으로 뷰홀더 함수를 호출해서 하는 식으로 변경했다.
ListAdapter를 하면서 currentList라는 걸 처음 써봤는데 이게 현재 어댑터에 들어있는 리스트를 가져온다고 하더라
inner class TodoViewHolder(private val binding: TodoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(todo: TodoModel) {
binding.apply {
tvTodoItemTitle.text = todo.title
tvTodoItemDescription.text = todo.description
switchTodoItemBookmark.isChecked = todo.isBookmarked
}
}
fun connectInterface(position: Int) {
binding.constraintLayoutTodoItem.setOnClickListener {
todoClick?.onClick(currentList[position], position)
}
}
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
holder.bind(currentList[position])
holder.connectInterface(position)
}
DiffUtil.ItemCallback을 상속받는 TodoComparator 클래스는 무조건 작성해야하는 함수가 두 개있다.
하나는 areItemsTheSame이고, 다른 하나는 areContentsTheSame이다.
ListAdapter의 submitList 함수가 불리면 백그라운드 스레드에서 AsyncListDiffer가 동작하여 리스트의 차이를 계산하고 변화를 UI에 적용시켜준다고 한다.
그 때 사용되는 함수들이다.
먼저 areItemsTheSame이 호출되고 그게 같으면 areContentsTheSame이 호출된다.
class TodoComparator : DiffUtil.ItemCallback<TodoModel>() {
override fun areItemsTheSame(oldItem: TodoModel, newItem: TodoModel): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: TodoModel, newItem: TodoModel): Boolean {
return oldItem.description == newItem.description
}
}
* 관련 트러블 슈팅 *
- 리사이클러뷰 리스트 어댑터를 사용할 때 리스트의 원소값을 변경했을 때 화면에 반영되지 않는 문제
> 할일을 수정하면 할일 리스트에 바뀐 내용이 떠야하는데 안 뜨고 다시 눌러서 들어가면 바뀐내용이 써있더라
> 그래서 튜터님이 올려주신 링크와 스택오버플로우를 보면서 해결방법인 거 같은 걸 시도하는데 계속 안 됐다.
> 같은 주소일 때는 갱신이 안 되기에 uiState에 변경이 있어서 업데이트가 필요할 때 toMutableList나 toList를 붙여 submitList 하는 방법이었다.
> 그래서 이리저리 시도하다가 튜터님을 찾아갔더니 원인은 다른 곳에 있었다.
> 바로 뷰모델에서 리스트의 원소값을 변경하는 방법이 문제였다.
> 내가 혼자 짤 때는 리스트에 원소 추가하는 함수를 복사해와서 this[position].title과 this[position].description을 변경했다.
> 하지만 이 부분을 객체 전체로 바꾸는 식으로 바꾸니 해결이 됐다.
> set 함수를 사용해서 인덱스를 가지고 todo 객체를 넣어서 교체했다.
8. newIntent 세분화
이런 방식은 생각해보지 못 했었는데 튜터님이 과제를 내주실 때 힌트로 주셔서 해보게 되었다.
등록을 하기 위해 부르는 newIntentForCreate, 수정 혹은 삭제를 하기 위해 부르는 newIntentForUpdate로 나눴다.
그리고 각각은 받는 매개변수가 다르다.
그래서 두 함수 모두에서 받지 않는 값은 nullable하게 세팅했다.
fun newIntentForCreate(
context: Context,
): Intent = Intent(
context,
ManageTodoActivity::class.java
).apply {
putExtra(EXTRA_ENTRY_TYPE, ManageTodoEntryType.CREATE)
}
fun newIntentForUpdate(
context: Context,
todo: TodoModel,
position: Int
): Intent = Intent(
context,
ManageTodoActivity::class.java
).apply {
putExtra(EXTRA_ENTRY_TYPE, ManageTodoEntryType.UPDATE.ordinal)
putExtra(EXTRA_TODO_MODEL, todo)
putExtra(EXTRA_TODO_POSITION, position)
}
* 관련 트러블 슈팅 *
- 엔트리타입이 CREATE로 고정되는 문제
> putExtra에서 enum class 엔트리타입을 그대로넘겨서 생긴 문제
> 엔트리타입을 넘기는 게 아니라 ordinal을 넘기니까 해결
9. 뷰모델 함수 추가
이전에는 등록만 있어서 하나만 있어도 됐지만 이제는 상황이 바뀌었다.
등록, 수정, 삭제가 필요하다.
그래서 먼저 ManageTodoActivity로부터 받아온 값을 다 뷰모델 onClick 함수로 보내줬다.
거기서 엔트리 타입에 따라 실행해야하는 함수를 호출한다.
fun onClick(todo: TodoModel?, entryType: ManageTodoEntryType, position: Int) {
when (entryType) {
ManageTodoEntryType.CREATE -> onClickRegister(todo)
ManageTodoEntryType.UPDATE -> onClickUpdate(todo, position)
ManageTodoEntryType.DELETE -> onClickDelete(todo)
}
}
등록은 저번과 동일...하지 않네?!
생각해보니까 등록도 튜터님의 코드를 복습하며 변경했었다.
TodoListUiState를 받는 라이브데이터의 value를 변경하는데 copy를 사용한다.
그래야 value가 변경되기 때문이다.
TodoListUiState 안의 내용 중 하나만 변경됐어도 value를 변경하는데 copy를 사용한다.
그래야 value가 변경되기 때문이다.
접근은 이전 챌린지 과제 대 했던 것과 동일하고 추가된건 orEmpty와 toMutableList가 있다.
라이브 데이터 안에 TodoListUiState에 접근하고 그걸 수정가능한 형태로 바꾸기 위해 추가되었다.
거기서 이제 TodoModel 객체를 add하는 것으로 등록은 마무리
private fun onClickRegister(todo: TodoModel?) {
if (todo == null) return
_uiState.value = uiState.value?.copy(
todoList = uiState.value?.todoList.orEmpty().toMutableList().apply {
add(todo)
}
)
}
수정은 거의 비슷 99퍼센트다.
다른 건 set을 사용하기 위해 position값을 받아온다는 거다.
private fun onClickUpdate(todo: TodoModel?, position: Int) {
if (todo == null) return
_uiState.value = _uiState.value?.copy(
todoList = uiState.value?.todoList.orEmpty().toMutableList().apply {
set(position, todo)
}
)
}
삭제도 거의 비슷 98퍼센트다.
다른 건 removeIf를 사용한다는 것이다.
private fun onClickDelete(todo: TodoModel?) {
if (todo == null) return
_uiState.value = _uiState.value?.copy(
todoList = uiState.value?.todoList.orEmpty().toMutableList().apply {
removeIf { it == todo }
}
)
}
'Android > StoreInfo' 카테고리의 다른 글
<정리> 심화 개인과제 3 (1) | 2024.01.29 |
---|---|
<정리> 심화 개인과제 2 (1) | 2024.01.25 |
<강의> 안드로이드 앱 개발 심화 - Retrofit (0) | 2024.01.25 |
<정리> 심화 개인과제 1 (0) | 2024.01.24 |
<강의> 안드로이드 앱 개발 심화 - 사용자 위치 얻기 (1) | 2024.01.24 |