[TIL] 선발대 5주차 정리

[TIL] 선발대 5주차 정리

4주차 과제

과제 1

Bookmark 탭에 추가하기

  • Switch를 만들어 Todo, Bookmark 탭에서 특정 아이템을 Switch를 On/Off를 할 수 있어야 한다.
  • Todo 탭에서 특정 Item을 Switch On 했을 경우 해당 Item은 Bookmark 탭에 추가해야 한다.
  • Bookmark 탭에 추가된 Item의 Switch Off 했을 경우 해당 Item이 삭제 되어야하고 Todo 탭의 Item은 Switch Off 되어야 한다.
  • 과제 1은 다음주까지 해보는데, 힘들다 이러면 한 주 더

과제 2

RecyclerView.AdapterListAdapter로 변경하기

[ListAdapterAndroid Developers](https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter)

[Android] ListAdapter

  • ListAdapter 에는 item 별로 유니크한 값이 필요함

    • ex) 상품의 id, 유저 id를 지칭할 수 있는 고유한 값이 필요함
  • 유니크 값은 for loop의 i 값이어도 됨, 그렇지만 더 좋은 값이 있으면 써도됨

    data class TodoModel(
    	val id: Int, // 유니크 값 
    	val title: String,
    	val description: String
    )
    

과제 3 (선택이지만 필수이지만 했음 좋겠음)

🚨과제 2까지 했다는 조건🚨

  • TodoFragment의 RecyclerView를 ViewModel를 통해 데이터를 서빙해보자.
class TodoViewModel : ViewModel() {
	
	val list = LiveData()
	val _list get() = list

	fun loadItems() {
		_list.value = item
	} 

}

-
class TodoFragment() {

	...

	fun initViewModel() {
		 viewModel.list.observe() { list ->
         listAdapter.submit(list)
     }
  }
	

  ...
}
  • 스터디
    • MVVM 아키텍쳐, AAC ViewModel, LiveData

1) MVVM

  • 아키텍처의 사용하고있는 패턴중 하나

  • 현업에서 많이 사용되고 요구 되어진다.

  • 이것을 알기 위해서는 UI Layer 계층을 알아야함

    • 이 역할은 , 어플리케이션 데이터를 표시, 사용자와 상호작용
    • 우리가 하고있는것들이 UI Layer
    • 데이터 레이어에서 가져온 애플리케이션 계층을 시각적으로 나타내는것
  • 뷰모델을 통한 데이터 처리는
    • LiveData
    • Flow
    • 종류가 있음
  • ViewModel은 데이터 레이어에 있는 데이터를 가져오는것
    • UI Layyerr 는 ViewModel에 데이터를 넘김
    • View Model은 액티비티와 독립적이다!
      • 액티비티가 시작하면 같이 시작하고 끝나는 동시에 끝남.

우리는 확장 가능한, 유지보수가 가능한 코딩을 하는것이 목표이다!

우리는 MVC구조에 맞게

솔리드=단일 체계원칙

하나의 책임만 기는것

MVC 와 MVVM의 차이점

MVC - 안드로이드의 기본구조/모델뷰컨트롤러 액티비티가 뷰와 컨드롤러를 동시에 담당함

MVVM - 뷰모델과 뷰를 나누고 비즈니스 모델이 담긴 해당 뷰모델을 언제든지 재사용 할수 있도록 하는 것 즉, 뷰모델과 액티비티를 나누고 비즈니스 로직이 담긴 뷰모델의 재사용성을 늘림

2) 과제 2 - RecyclerAdapter에서 ListAdapter로 교체하는 방법

  • DiffUtil사용 하기위해 기존 리사이클러 어댑터를 리스트 어댑터로 변경해준다.

    • DiffUtil 이란?
    class TodoListAdapter(
        private val onClickItem: (Int, TodoModel) -> Unit
    ) : ListAdapter<TodoModel, TodoListAdapter.ViewHolder>(
      
        object : DiffUtil.ItemCallback<TodoModel>() {
            override fun areItemsTheSame(
                oldItem: TodoModel,
                newItem: TodoModel
            ): Boolean {
                return oldItem.id == newItem.id
            }
      
            override fun areContentsTheSame(
                oldItem: TodoModel,
                newItem: TodoModel
            ): Boolean {
                return oldItem == newItem
            }
        }
    ) {
    
    • 이렇게 하면 id에 오류가 나는데 TodoModel에 id 추가해주기

      @Parcelize
      data class TodoModel(
          val id: Int,//id 추가
          val title: String?,
          val description: String?
      ) : Parcelable
      
      • 이렇게 하면 다른 함수들을 변경해줄 수 있음
      • 💡리스트 어댑터 특징! ListAdapter는 서브밋을 할때 새로운 array 인스턴스를 생성해서 넣어줘야 한다.
      fun addItem(todoModel: TodoModel?) {
          val list=currentList.toMutableList()
          list.add(todoModel)
          submitList(list)
      }
      
      fun modifyItem(
              position: Int?,
              todoModel: TodoModel?
          ) {
              if (position == null || todoModel == null) {
                  return
              }
              val list=currentList.toMutableList()
              list[position] = todoModel
              submitList(list)
          }
      
      fun removeItem(
          position: Int?
      ) {
          if (position == null) {
              return
          }
          val list=currentList.toMutableList()
          list.removeAt(position)
          submitList(list)
      }
      

3) 과제 3 - View Model 만들기

package com.jess.camp.todo.home

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.concurrent.atomic.AtomicLong

class TodoViewModel : ViewModel() {

    private val _list: MutableLiveData<List<TodoModel>> = MutableLiveData()
   val list: LiveData<List<TodoModel>> get() = _list

    //live= 액티비티나 프래그먼트에서 읽기 전용
    //_live= 뷰모델 내부에서 컨트롤 전용
    val live: LiveData<List<TodoModel>> get() = _list

    // id 를 부여할 값
    private val idGenerate = AtomicLong(1L)

    init {
        _list.value = arrayListOf<TodoModel>().apply {
            for (i in 0 until 3) {
                add(
                    TodoModel(
                        idGenerate.getAndIncrement(),
                        "title $i",
                        "description $i"
                    )
                )
            }
        }
    }

}
  • viewmodel을 사용하려면 id 값이 필요하니 그것을 넣어주는 함수를AtomicLong를 사용해준다.

  • 리스트에서 todomodel을 생성할때 마다 id값을 부여해주도록 작성

  • TodoModel에 id : Long, 를 추가하고

    @Parcelize
    data class TodoModel(
         val id: Long? = -1,
        val title: String?,
        val description: String?
    ) : Parcelable
    
  • 데이터를 만들면 뿌려야 하기때문에 TodoFragment에서 뷰모델을 위한 init 클래스를 만들어준다.

    • 뷰모델 생성시 뷰모델 프로바이덜을 통해 만들줘야함
    private val viewModel: TodoViewModel by lazy {
            ViewModelProvider(this)[TodoViewModel::class.java]
        }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            initView()
            initViewModel()//뷰모델을 위한 클래스
        }
      		
    		//init 생성자
        private fun initViewModel()= with(viewModel) {
            list.observe(viewLifecycleOwner){
                listAdapter.submitList(it)
            }
        }
    
    • viewLifecycleOwner 를 통해 리스트에 있는 데이터를 가져와야한다.
    • 라이브 데이터의 viewLifecycleOwner를 썻을때 화면이 있을때만 보여지고, 사라졌을때는 무시를 해줌

    • 옵저브를 만들어서
      • 프래그먼트에서 - 뷰라이프사이클 오너를 사용
      • 액티비티에서- this로 라이프사이클오너를 사용

    💡 라이브데이터의 특징: 라이플사이클 오너를 넣을때 화면이 사라졌을때는 , 무시해줌, 옵저빙을 안해줌.

    즉, 화면이 죽었을때는 옵저빙이 활성화되지 않음!

3) 아이템을 추가시켜줄때

  • TodoFragment 에서 변경해주기
fun setDodoContent(todoModel: TodoModel?) {
        viewModel.addTodoItems(todoModel)
    }
  • addTodoItems을 커맨드+엔터 하여 뷰모델에 함수를 만들어주자

    fun addTodoItem(
            todoModel: TodoModel?
        ) {
            if (todoModel == null) {
                return
            }
      
            val currentList = list.value.orEmpty().toMutableList()
            _list.value = currentList.apply {
                add(
                    todoModel.copy(
                        id = idGenerate.getAndIncrement()
                    )
                )
            }
        }
    
  • TodoListAdapter 에서 getItemCount() 삭제후 아래함수 수정

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)//변경해주기
        holder.bind(item)
    }
    

코드를 설명하자면

  1. mainactivity 에서 런쳐를 통해ㅔ 데이터가 들어 올것이고

  2. todoFragment 에 setDodoContent 를 넘겨서
  3. 뷰 모델을 생성하고 데이터 addTodoItem 로 넘어온 다음 데이터 처리를 해줌!

4) 수정을 해줄때

  • 프래그 먼트에 있는 함수를 수정해주기

    private fun modifyTodoItem(
            position: Int?,
            todoModel: TodoModel?
        ) {
            viewModel.modifyItem(
                position,
                todoModel
            )
        }
    
    • viewmodel에 modifyItem 함수 생성해주기
  • 생성한 함수를 작성해주기

    fun modifyItem(
            position: Int?,
            todoModel: TodoModel?
        ) {
    				//만약 position을 찾지 못했을때
            fun findIndex(item: TodoModel?): Int {
                val currentList = list.value.orEmpty().toMutableList()
                // 같은 id 를 찾음
                val findTodo = currentList.find {
                    it.id == item?.id
                }
      
                // 찾은 model 기준으로 index 를 찾음
                return currentList.indexOf(findTodo)
            }
      
            if (todoModel == null) {
                return
            }
      
            // position 이 null 이면 indexOf 실시
            val findPosition = position ?: findIndex(todoModel)
            if (findPosition < 0) {
                return
            }
      
            val currentList = list.value.orEmpty().toMutableList()
            currentList[findPosition] = todoModel
            _list.value = currentList
        }
    

5) 삭제를 해줄때

  • 어댑터에 있는 삭제 함수를 복사해와 뷰 모델에 넣어주고 수정하기

    • 포지션을 받아오고, 널처리와 -1이 아닌지 확인한다음
    • 뮤터블 리스트로 바꿔준 후
    • removeAt 으로 position 삭제후 리스트에 반영
    fun removeItem(
            position: Int?
        ) {
            if (position == null || position <0) {
                return
            }
            val currentList= list.value.orEmpty().toMutableList()
            currentList?.removeAt(position)
            _list.value=currentList
        }
    
  • fragment 에 있는 함수를 뷰모델로 보내준다

    private fun removeItemTodoItem(position: Int?) {
            viewModel.removeItem(position)
        }
    
  • 어댑터에 사용하지 않는 삭제 함수를 지워준다!

6) 과제 1 - Switch On/Off 반영하기

  • 리사이클러뷰 아이템에 스위치 넣어주기 -> id값 부여

    • TodoModel 에 스위치 추가
    var isBookmark: Boolean = false
    
  • TodoListAdapter에 클릭onBookmarkChecked 바인딩 해주기

class TodoListAdapter(
    private val onClickItem: (Int, TodoModel) -> Unit,
    private val onBookmarkChecked: (Int, TodoModel) -> Unit
)
  • onCreateViewHolder에도 넣어주기
  • ViewHolder 작성
class ViewHolder(
        private val binding: TodoItemBinding,
        private val onClickItem: (Int, TodoModel) -> Unit,
        private val onBookmarkChecked: (Int, TodoModel) -> Unit
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: TodoModel) = with(binding) {
            title.text = item.title
            description.text = item.description
            bookmark.isChecked = item.isBookmark

            // itemClick
            container.setOnClickListener {
                onClickItem(
                    adapterPosition,
                    item
                )
            }
            // 북마크 클릭
            bookmark.setOnCheckedChangeListener { _, isChecked ->
                // 현재 바인딩된 아이템과 checked 된 값 비교 후 전달
                //스위치가 off 상태에서 클릭했을때 만 추가가 되어야함
                if (item.isBookmark != true) { // 둘이 상태가 다를 경우 상태를 isChecked로 바꾼 item을 copy해와 전송
                    onBookmarkChecked(
                        adapterPosition,
                        item.copy(
                            isBookmark = isChecked
                        )
                    )
                    item.isBookmark =! item.isBookmark
                    //클릭이 안되시윛
                }
            }

        }
    }

}
  • TodoFragment 리스트어댑터 수정하기

    private val listAdapter by lazy {
        TodoListAdapter(
            onClickItem = { position, item ->
                editTodoLauncher.launch(
                    TodoContentActivity.newIntentForEdit(
                        requireContext(),
                        position,
                        item
                    )
                )
            },
            onBookmarkChecked = { position, item ->
                if (item.isBookmark){
                    addItemToBookmarkTab(item) // BookMarkTap에 Item 추가
                }else{
                    removeItemToBookmarkTab(item)
                }
                modifyTodoItem(item,position ) // BookMark가 check됐을 때 Item수정
      
            }
        )
      
    }
    
    • 추가, 삭제, 수정 함수 추가

      /** Bookmark Tab 에 아이템을 추가합니다.*/
          private fun addItemToBookmarkTab(item: TodoModel) {
              (activity as MainActivity).addBookmarkItem(item) //다음의 통해 진행하면 메모리 누수, 생명주기..? 등 다양한 문제가 발생할 수 있다.
              // -> 바로 MainActivity 뷰모델에 접근하는게 좋다.
              //MainViewModel.addBookmarkItem이런식
              // 형 변환을 통해 현재 호스팅이 MainActivity라는 것을 확신
          }
          
          /** Bookmark Tab 에 아이템을 삭제합니다.*/
          private fun removeItemToBookmarkTab(item: TodoModel) {
              (activity as MainActivity).removeBookmarkItem(item)
          }
          
          fun modifyTodoItem(
              item: TodoModel?,
              position: Int? = null
          ) {
              viewModel.modifyTodoItem(position, item)
          }
      
  • MainActivity에 추가, 삭제, 수정 함수 추가

fun addBookmarkItem(item: TodoModel) {
    val fragment =
        viewPagerAdapter.getFragment(1) as? BookmarkFragment //BookmarkFragment로 캐스팅을 해 다른 값이 넘어오면 널 반환
    fragment?.addItem(item.toBookmarkModel()) // TodoModel을 BookmarkModel로 변환하여 추가
}


fun modifyTodoItem(item: BookmarkModel) {
    val fragment = viewPagerAdapter.getFragment(0) as? TodoFragment
    val todoItem = item.toTodoModel()
    fragment?.modifyTodoItem(
        item = todoItem
    )
}

fun removeBookmarkItem(item: TodoModel) {
    val fragment =
        viewPagerAdapter.getFragment(1) as? BookmarkFragment //BookmarkFragment로 캐스팅을 해 다른 값이 넘어오면 널 반환
    fragment?.removeItem(
        item = item.toBookmarkModel()
    )
}
  • TodoModel -> BookMarkmodel 변경해주는 함수 추가하기
package com.example.mvvmtodolist.todo.home

import android.os.Parcelable
import com.example.mvvmtodolist.bookmark.BookmarkModel
import kotlinx.parcelize.Parcelize

@Parcelize
data class TodoModel(
    val id: Long? = null,
    val title: String?,
    val description: String?,
    var isBookmark: Boolean = false
) : Parcelable

// TodoModel 확장함수로 TodoModel -> BookMarkModel 객체로 변환해줌.
fun TodoModel.toBookmarkModel() : BookmarkModel{
    return BookmarkModel(
        id = id ?: 0, //엘비스 연산자 사용 : id가 null일 경우 0 할당
        title = title,
        description =description,
        isBookmark = isBookmark
    )
}
package com.example.mvvmtodolist.bookmark

import android.os.Parcelable
import com.example.mvvmtodolist.todo.home.TodoModel
import kotlinx.android.parcel.Parcelize

@Parcelize
data class BookmarkModel(
    val id: Long,
    val title: String?,
    val description: String?,
    val isBookmark: Boolean = false
) : Parcelable

fun BookmarkModel.toTodoModel(): TodoModel {
    return TodoModel(
        id = id,
        title = title,
        description = description,
        isBookmark = isBookmark
    )
}
  • BookMarkFragment에 함수들 추가와, 리스트어댑터와 뷰모델, 옵저버 연결
fun removeItem(
    item:BookmarkModel,
    position: Int? = null
) {
    viewModel.removeBookmarkItem(item,position)
}

fun addItem(item : BookmarkModel) {
    viewModel.addBookmarkItem(item)
}

private fun modifyItemAtTodoTab(
    item: BookmarkModel
) {
    (activity as? MainActivity)?.modifyTodoItem(item)
}
companion object {
    fun newInstance() = BookmarkFragment()
}

private var _binding: BookmarkFragmentBinding? = null
private val binding get() = _binding!!
private val viewModel : BookmarkViewModel by viewModels()

private val listAdapter by lazy {
    BookmarkListAdapter { position, item ->
        removeItem(item, position)
        modifyItemAtTodoTab(item)
    }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    initView()
    initModel()
}

private fun initModel() = with(viewModel) {
    // viewModel 상 읽기용 list
    list.observe(viewLifecycleOwner) { // Fragment LV : observe(viewLifecycleOwner)
        listAdapter.submitList(it)
    }
}
  • BookMarkViewModel 작성하기
package com.example.mvvmtodolist.bookmark

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.mvvmtodolist.todo.home.TodoModel

class BookmarkViewModel : ViewModel() {

    private val _list: MutableLiveData<List<BookmarkModel>> = MutableLiveData()
    val list: LiveData<List<BookmarkModel>> get() = _list
    // getter메소드를 사용하지 않으면
    // 외부에서 list에 대한 변경 사항을 감지하지 못하고 _list를 통해 데이터를 변경해야 한다.
    // 이것은 LiveData의 핵심 장점 중 하나인 데이터 변경 감지 및 생명주기 관리 기능을 사용하지 않는 것과 같다.

    fun addBookmarkItem(bookmarkModel: BookmarkModel?) {
        if (bookmarkModel == null) {
            return
        }
        val currentList = list.value.orEmpty().toMutableList() // orEmpty -> list.value가 null일 때 빈 리스트 반환
        currentList.add(bookmarkModel)
        _list.value = currentList
    }

    fun removeBookmarkItem(
        bookmarkModel: BookmarkModel,
        position: Int?
    ) {
        fun findIndex(bookmarkModel: BookmarkModel): Int? {
            val currentList = list.value?.toMutableList()
            val findTodoById = currentList?.find { // 수정하고자 하는 todoModel의 id와 currentList의 id를 비교
                it.id == bookmarkModel.id
            }
            return currentList?.indexOf(findTodoById)//indexOf : 찾고자 하는 Array의 index를 반환
        }

        if (bookmarkModel == null) {
            return
        }

        val findPosition = position ?: findIndex(bookmarkModel) //position이 안들어왔을 경우 id 값으로
        if (findPosition == null || findPosition < 0) {
            return
        }

        val currentList = list.value.orEmpty().toMutableList()
        currentList.removeAt(findPosition)
        _list.value = currentList
    }
}

© 2023. All rights reserved.

AgileCatch