마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

중첩 리사이클러뷰 아이템 업데이트 이렇게 사용해도 될까요..

0 추천

Imgur: The magic of the Internet

버튼을 누르면 동적으로 리사이클러뷰 아이템이 추가됩니다.

이 추가된 하나의 아이템은 내부에 상세정보를 담고있는 리사이클러뷰가 하나 더 있는

중첩리사이클러뷰 형태입니다. (사진 참조)

중첩리사이클러뷰아이템(이하 자식아이템)도 아이템을 추가 및삭제 가능합니다.

 

MVVM의 패턴을 적용하려고 하고있습니다. 그리고 아직 DB는 로컬 DB를 사용할지 서버를 사용할지

파이어베이스를 사용할지 결정을 못했습니다..

아무튼 결과만 말씀드리면 일단 동작은 잘합니다. 부모 아이템의 추가 및 삭제도 잘되고

자식 아이템도 추가 및 삭제가 잘됩니다.

 

그런데 자식 아이템을 추가하는 과정에서의 방식이 올바른지 모르겠습니다.

처음에 자식 아이템의 추가,삭제 코드(addDetail,deleteDetail)를 짰을때 아무 반응이 없길래 왜 그런가 했는데,

프래그먼트에서 items를 observe하는데 items의 변화를 관찰하는게 items의 추가/삭제에 관해서만

관찰을 하더라구요..즉 viewmodel 클래스의 addDetail에서 진행하는 해당 표지션의 부모 아이템의 자식 아이템 추가는 관찰하지 못해서 아무런 반응이 없었어요..

그래서 이걸 강제로 observe 시키기 위해서 _items.value = _items.value 라는 코드를 넣어

프래그먼트에서 강제로 items를 관찰하도록 했더니 잘 동작하더라구요..

_items.value = _items.value 코드가 예전에 스택오버플로우에서 모르는거 검색하다가 추가한 코드인데..

지금은 삭제하니 잘돼서.. 뭐때문에 추가했는지 기억은 잘안납니다.

아무튼 이게 과연 올바른 방식인지 모르겠습니다.. 조언좀 부탁드려요

 

부모아이템.kt

data class RoutineModel(
    val workout: String, // 운동 종목
    val unit: String, // 무게 단위 (kg or lbs)
    var routineDetail: ArrayList<RoutineDetailModel> = arrayListOf()
) {
    init {
        // 루틴 생성시 최소 1개의 상세 아이템을 가지고 있게 하기 위함
        val detail = RoutineDetailModel("10","test")
        routineDetail.add(detail)
    }

    fun getSubItemList() : ArrayList<RoutineDetailModel> = routineDetail

    fun getSubItemSize() = routineDetail.size

    fun addSubItem(item: RoutineDetailModel) {
        routineDetail.add(item)
    }

    fun deleteSubItem() {
        routineDetail.removeLast()
    }
}

 

Fragment

class WriteRoutineFragment : Fragment() {
    private lateinit var adapter : RoutineAdapter
    private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }

   override fun onCreateView(inflater: LayoutInflater,
                                  container: ViewGroup?,
                                  savedInstanceState: Bundle?): View? {
            _binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)

           adapter = RoutineAdapter(::addDetail, ::deleteDetail)
            binding.rv.adapter = this.adapter
            return binding.root
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // LiveData를 observe()하는 것이므로 onViewCreated()가 적합한 위치
        getTabPageResult()

        // 추가된 리사이클러뷰 아이템 업데이트, 
        // 테스트위해 notifyDataSetChanged() 사용 추후 DiffUtil로 변경 예정
        vm.items.observe(viewLifecycleOwner) { updatedItems ->
            adapter.setItems(updatedItems)
        }
    }

    private fun getTabPageResult() {
        val navController = findNavController()
        navController.currentBackStackEntry?.also { stack ->
            stack.savedStateHandle.getLiveData<String>("workout")?.observe(
                viewLifecycleOwner, Observer { result ->
                    vm.addRoutine(result) // 루틴 추가
                    stack.savedStateHandle?.remove<String>("workout")
                }
            )
        }
    }

    private fun addDetail(pos: Int) {
        vm.addDetail(pos) // 상세추가
    }

    private fun deleteDetail(pos: Int) {
        vm.deleteDetail(pos) // 상세삭제
    }
}

 

ViewModel

class WriteRoutineViewModel : ViewModel() {
    private var _items: MutableLiveData<ArrayList<RoutineModel>> = MutableLiveData(arrayListOf())

    val items: LiveData<ArrayList<RoutineModel>> = _items

    fun addRoutine(workout: String) {
        val item = RoutineModel(workout, "TEST")
        _items.value?.add(item)
//        _items.value = _items.value
    }

    fun addDetail(pos: Int) {
        val detail = RoutineDetailModel("TEST", "TEST")
        _items.value?.get(pos)?.addSubItem(detail) // 부모아이템의 데이터만 변화시켜서는 관찰하지못함
        _items.value = _items.value // 올바른 방식인가
    }

    fun deleteDetail(pos: Int) {
        if(_items.value?.get(pos)?.getSubItemSize()!! > 1) // Detail 아이템이 1일경우에는 Routine을 삭제
            _items.value?.get(pos)?.deleteSubItem() // 올바른 방식인가
        else
            _items.value?.removeAt(pos)
        _items.value = _items.value // 올바른 방식인가
    }
}

 

Adapter

class RoutineAdapter(val addDetailClicked: (Int) -> Unit,
                     val deleteDetailClicked: (Int) -> Unit)
    : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private var items: ArrayList<RoutineModel> = arrayListOf()
    private val viewPool = RecyclerView.RecycledViewPool()

    companion object {
        private const val TYPE_ROUTINE = 1
        private const val TYPE_ROUTINE_FOOTER = 2
    }

    fun setItems(items: ArrayList<RoutineModel>) {
        this.items = items
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        lateinit var itemView: View
        return when (viewType) {
            TYPE_ROUTINE -> {
                val binding = ItemRoutineBinding.inflate(LayoutInflater.from(parent.context), parent, false)
                RoutineViewHolder(binding) // return
            }
            else -> {
                itemView = LayoutInflater.from(parent.context).inflate(R.layout.add_routine_button, parent, false)
                RoutineFooterViewHolder(itemView) // return
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when(getItemViewType(position)) {
            TYPE_ROUTINE -> {
                val item = items[position]
                holder as RoutineViewHolder
                holder.bind(items[position])
                initDetailLayoutManager(holder, item, position)
            }
            else -> holder as RoutineFooterViewHolder
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when(position) {
            items.size -> TYPE_ROUTINE_FOOTER
            else -> TYPE_ROUTINE
        }
    }

    override fun getItemCount(): Int = items.size + 1 // footer 때문에 +1

    private fun initDetailLayoutManager(holder : RoutineViewHolder, item: RoutineModel, pos: Int) {
        val lm = LinearLayoutManager( // Detail RV를 위한 lm
            holder.subRV.context,
            LinearLayoutManager.VERTICAL,
            false
        )
        lm.initialPrefetchItemCount = item.getSubItemList().size
        val detailAdapter = RoutineDetailAdapter(items[pos].getSubItemList())
        holder.subRV.layoutManager = lm
        holder.subRV.adapter = detailAdapter
        holder.subRV.setRecycledViewPool(viewPool)
    }

    inner class RoutineViewHolder(private val binding: ItemRoutineBinding) : RecyclerView.ViewHolder(binding.root) {
        val subRV = binding.rv

        init {
            binding.add.setOnClickListener {
                addDetailClicked(adapterPosition)
            }
            binding.delete.setOnClickListener {
                deleteDetailClicked(adapterPosition)
            }
        }

        fun bind(list: RoutineModel) {
            binding.workout.text = list.workout
        }

    }

    inner class RoutineFooterViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        private val button: ConstraintLayout = itemView.findViewById(R.id.add_routine)

        init {
            button.setOnClickListener { view ->
                view.findNavController().navigate(R.id.action_writeRoutineFragment_to_workoutListTabFragment)
            }
        }
    }
}

 

codeslave (3,940 포인트) 님이 2021년 8월 31일 질문
codeslave님이 2021년 8월 31일 수정

1개의 답변

0 추천

딱히 문제는 아닐 듯한데 코드를 조금만 수정하면 좀 더 의도가 명확해질 듯 합니다. Routine을 추가, 수정, 삭제하는 로직은 ViewModel에 있는 것보다는 별도의 클래스에 있는 것이 맞을 듯 합니다. 추후에 Dabase로 옮길 경우에도 좀 더 마이그레이션이 명확해 지구요. 

interface RoutineRepository {
    fun getRoutines(): List<RoutineModel>
    fun getRoutine(pos: Int): RoutineModel?
    fun addDetail(pos: Int): Boolean
    fun deleteDetail(pos: Int): Boolean
}

class InMemoryRepository: RoutineRepository {
     private val routines = arrayListOf<RoutineModel>()
    
     override fun getRoutines() = routines

     override fun getRoutine(position: Int) = routines.getOrNull(pos)

     override fun addDetail(pos: Int): Boolean {
        val routine = getRoutine(pos) ?: return false
        val detail = RoutineDetailModel("TEST", "TEST")
        routine.addSubItem(detail)
        return true
    }
 
    override fun deleteDetail(pos: Int): Boolean {
        val routine = getRoutine(pos) ?: return false
        if(routines.getSubItemSize() > 1) {
            routine.deleteSubItem()
       } else {
           routines.removeAt(pos)
      }
      return true
    }
}

// ViewModel

private val routineRepository by { InMemoryRepository() }

fun addDetail(pos: Int) {
       if (!routineStore.addDetail(pos)) {
             // 에러처리
             return
       }
        _items.value = routineStore.getRoutines()
}
 
fun deleteDetail(pos: Int) {
        if (!routineStore.deleteDetail(pos)) {
              // 에러처리
             return
        }
      _items.value = routineStore.getRoutines()
}

 

spark (224,800 포인트) 님이 2021년 8월 31일 답변
감사합니다. DB얘기가 나온김에 여쭈어볼게 있어서요.
DB가 흔히 로컬 DB랑 서버, 그리고 파이어베이스를 이용하는 방식 세가지정도로 나뉘는것같은데요..
로그인 기능도 없고(아직 계획은 없고) 메모장처럼 그냥 들어가서 작성하는 앱수준이라 로컬 DB를 사용할까 헀는데요..
혹시나 앱을 삭제후에 다시 설치하는 경우에는 작성한 기록들이 다 날아가게 되어서
서버 DB에다가 저장하고 API를 이용해서 (자바스크립트맞나요?) 불러오는 그러한 방식을 사용할까 헀는데 생각해보니 제가 서버를 만들 능력도 안되고해서
파이어베이스를 사용할까하는데 적절한지 잘모르겠습니다..
파이어베이스가 그런 경우에는 적절한 대안이 될 수 있죠. DB는 FireStore, 인증은 FireAuth를 사용하시면 되구요. 어차피 안드로이드 앱은 Crashlytics랑 Analytics 등을 Firebase로 많이 사용하기 때문에 도움이 될거예요. TestLab을 사용하시면 Firebase AI가 자동으로 앱테스트도 해줍니다.
...