[TIL] 선발대 5주차 정리
- 4주차 과제
- 1) MVVM
- 2) 과제 2 - RecyclerAdapter에서 ListAdapter로 교체하는 방법
- 3) 과제 3 - View Model 만들기
- 3) 아이템을 추가시켜줄때
- 4) 수정을 해줄때
- 5) 삭제를 해줄때
- 6) 과제 1 - Switch On/Off 반영하기
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.Adapter
를 ListAdapter
로 변경하기
[ListAdapter | Android Developers](https://developer.android.com/reference/androidx/recyclerview/widget/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) }
코드를 설명하자면
mainactivity 에서 런쳐를 통해ㅔ 데이터가 들어 올것이고
- todoFragment 에 setDodoContent 를 넘겨서
- 뷰 모델을 생성하고 데이터 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
}
}