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

kotlin Dialog안에서 리사이클러뷰 동적 추가 삭제 수정 방법

0 추천

 

이런식으로 다이얼로그에서 edittext를 실시간으로 저장하며 추가 삭제 수정이 가능하게 하려고 하는데 도대체 아무리 생각을 해보고 코드를 짜봐도 답이 안나오네요...

조언 좀 부탁드립니다.

edittext는 textwatcher를 써서 담을까 생각했는데 이것도 좋은 방법이 아닌거 같기도 하구요...

문제는 삭제인데 인터페이스로 포지션값을 넘겨서 삭제를 해도 이상하게 삭제가 되네요...

낚참이 (140 포인트) 님이 2022년 11월 6일 질문
작성하신 코드를 올려보세요.

2개의 답변

0 추천
class RequiredDialog (context: Context): Dialog(context), View.OnClickListener {

    private val TAG = "RequiredDialog"

    lateinit var optionListener: OptionListener

    //뷰 바인딩
    lateinit var binding: RequiredDialogBinding


    var option_value = ArrayList<String>()
    var requiredItemAdapter = RequiredItemAdapter(option_value, context)

    lateinit var optionDTO: OptionDTO

    fun setOnOptionListener(optionListener: OptionListener) {
        this.optionListener = optionListener
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = RequiredDialogBinding.inflate(layoutInflater)
        val view = binding.root

        window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
        window!!.requestFeature(Window.FEATURE_NO_TITLE)
        window!!.setGravity(Gravity.BOTTOM)

        setContentView(view)

        binding.btnSubAdd.setOnClickListener(this@RequiredDialog)
        binding.btnCancel.setOnClickListener(this@RequiredDialog)
        binding.btnOk.setOnClickListener(this@RequiredDialog)

        //리사이클러뷰
        binding.subRequiredRecycler.adapter = requiredItemAdapter

        requiredItemAdapter.itemDel(object : OnItemClickListener {
            override fun onOptionDel(position: Int) {
                Log.d(TAG, "지운 위치 => $position")
                Log.d(TAG, "지운 값 => ${option_value[position]}")

                option_value.removeAt(position)
                requiredItemAdapter.notifyItemRemoved(position)
            }
        })
    }

    override fun onClick(v: View?) {
        when(v?.id) {
            binding.btnSubAdd.id -> {
                option_value.add("")
                requiredItemAdapter.notifyDataSetChanged()
            }
            binding.btnCancel.id -> dismiss()
            binding.btnOk.id -> result()
        }
    }

    //확인 버튼
    private fun result() {
//        Log.d(TAG, "최종결과 값 => $option_value")
        val name = binding.etRequiredName.text.toString()
        if (name.isEmpty()) {
            Toast.makeText(context, "Please enter an option name.", Toast.LENGTH_SHORT).show()
        }else {
            optionDTO = OptionDTO(name, option_value, null, null, 1)
            optionListener.addOption(optionDTO)
            dismiss()
        }
    }
}





class RequiredItemAdapter(var datas: ArrayList<String>, val context: Context): RecyclerView.Adapter<RequiredItemAdapter.ViewHolder>() {


    private val TAG = "RequiredItemAdapter"

    lateinit var itemDel: OnItemClickListener

    fun itemDel(itemClickListener: OnItemClickListener) {
        itemDel = itemClickListener
    }

    override fun getItemCount(): Int {
        return datas.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = RequiredRecyclerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

//    override fun getItemViewType(position: Int): Int {
//        return position
//    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(datas[position], itemDel)

//        holder.binding.etSelectName.addTextChangedListener(object : TextWatcher {
//            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
//            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
//            override fun afterTextChanged(p0: Editable?) {
//                modifyItem(p0.toString(), holder.adapterPosition)
//            }
//        })
    }

    class ViewHolder (val binding: RequiredRecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(data: String, itemClickListener: OnItemClickListener) {
            binding.etSelectName.setText(data)

            binding.btnDel.setOnClickListener {
                itemClickListener.onOptionDel(adapterPosition)
            }
        }
    }

    fun addItem(data: String) {
        datas.add(data)
        notifyDataSetChanged()
    }

    fun removeItem(position: Int) {
        datas.removeAt(position)
        notifyItemRemoved(position)
    }
}


 

익명사용자 님이 2022년 11월 7일 답변
0 추천

두가지 포인트를 언급할게요.
1) RecyclerView.Adapter에 님과 같이 EditText같은 입력컴포넌트를 관리할 경우, 텍스트변경, 포커스 관리 등 처리해야할 일이 많아서 복잡해집니다. 그리고 변경된 아이템만 리프레시하려면 코드를 좀 더 작성하여야 하구요.
2) 데이터의 추가, 수정, 삭제는 어댑터나 뷰홀더가 아니라 외부에서 관리하는 리스트를 줌시으로 처리하고 RecyclerView는 이걸 보여주는 역할만 충실하도록 하는게 좋습니다.

1)의 문제를 조금이나 수월하게 처리하기 위해 저는 RecyclerView.Adpater대신 androidx.recyclerview.widget.ListAdapter 를 사용할 겁니다. 이걸 사용하면  내부적으로 DiffUtil 이란걸 이용해서 변경되 아이템만 리프레시해주는 최적화를 수행합니다.
2)와 같이 데이터를 조회, 추가, 수정, 삭제하는 동작은 비지니스 로직에 해당합니다. 가장 보편적인 접근방법은 UseCase, Repository, Manager같은 비지니스 로직을 담당하는 클래스를 만들어서 처리하는 겁니다. 저는 클래스 숫자를 줄이기위해 이 부분은 생략하고 그냥 Dialog안에 필요한 코드를 작성하겠습니다. 클래스 분리는 따로 고민해 보시기 바랍니다.

먼저 Adpater에서 사용할 데이터 클래스를 하나 만듭니다. 여기서는 아이템을 구분해줄 ID와 Focus관리를 위한 Boolean  핃드를 하나 가집니다.

data class CookLevel(
    val id: Int,
    val name: String,
    val isFocused: Boolean = false
)

 

다음은 Recyclerviw Adapter입니다. ListAdapter를 사용합니다. 사용법은 개발자 문서를 보고 확인하세요.
 

class CookLevelAdapter(
    var listener: ItemListener? = null
) : ListAdapter<CookLevel, CookLevelAdapter.ViewHolder>(
    DIFF_CALLBACK
) {
    companion object {
        private const val TAG = "RequiredItemAdapter"

        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<CookLevel>() {
            override fun areItemsTheSame(oldItem: CookLevel, newItem: CookLevel): Boolean =
                oldItem == newItem

            override fun areContentsTheSame(oldItem: CookLevel, newItem: CookLevel): Boolean =
                oldItem == newItem
        }
    }

    interface ItemListener {
        fun onSaveItem(item: CookLevel)
        fun onDeleteItem(item: CookLevel)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = RequiredRecyclerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding, listener)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position) ?: return
        holder.bind(item)
    }

    class ViewHolder(
        private val binding: RequiredRecyclerItemBinding,
        private var listener: ItemListener?
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: CookLevel) {
            binding.apply {
                etSelectName.setText(item.name)
                // Handle tapping done button
                etSelectName.setOnEditorActionListener { _, actionId, _ ->
                    handleNameImeAction(actionId = actionId, item = item)
                }
                if (item.isFocused) {
                    etSelectName.requestFocus()
                }

                btnDel.setOnClickListener {
                    listener?.onDeleteItem(item)
                }
            }
        }

        private fun handleNameImeAction(actionId: Int, item: CookLevel): Boolean {
            if (actionId != EditorInfo.IME_ACTION_DONE) return false

            val name = binding.etSelectName.text.toString()
            listener?.onSaveItem(item.copy(name = name))
            return true
        }
    }
}

다음은 Dialog입니다.

class CookLevelDialog(context: Context) : Dialog(context), CookLevelAdapter.ItemListener {

    private lateinit var optionListener: OptionListener
    private lateinit var binding: RequiredDialogBinding

    private var cookLevelAdapter = CookLevelAdapter()
    private var cookLevels = emptyList<CookLevel>()

    private lateinit var optionDTO: OptionDTO

    fun setOnOptionListener(optionListener: OptionListener) {
        this.optionListener = optionListener
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = RequiredDialogBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setupViews()
    }

    private fun setupViews() {
        binding.apply {
            btnSubAdd.setOnClickListener {
                addCookLevel()
            }

            btnCancel.setOnClickListener {
                dismiss()
            }

            btnOk.setOnClickListener {
                showResult()
            }

            subRequiredRecycler.adapter = cookLevelAdapter
            cookLevelAdapter.listener = this@CookLevelDialog
        }
    }

    private var maxCookLevelId = 0

    private fun addCookLevel() {
        cookLevels = cookLevels.map {
            it.copy(isFocused = false)
        }.plus(CookLevel(id = ++maxCookLevelId, name = "", isFocused = true))
        cookLevelAdapter.submitList(cookLevels)
    }

    override fun onSaveItem(item: CookLevel) {
        cookLevels = cookLevels.map { cookLevel ->
            if (cookLevel.id == item.id) {
                cookLevel.copy(name = item.name)
            } else
                cookLevel
        }
        addCookLevel()
    }

    override fun onDeleteItem(item: CookLevel) {
        Log.d(TAG, "지운 값 => $item")
        cookLevels = cookLevels.minus(item)
        cookLevelAdapter.submitList(cookLevels)
    }

    //확인 버튼
    private fun showResult() {
        val name = binding.etRequiredName.text.toString()
        if (name.isEmpty()) {
            Toast.makeText(context, "Please enter an option name.", Toast.LENGTH_SHORT).show()
        } else {
            //optionDTO = OptionDTO(name, option_value, null, null, 1)
            optionListener.addOption(optionDTO)
            dismiss()
        }
    }
}

다듬어야 할 부분들이 군데 군데 있습니다. 이 부분을 잘 체크하셔서 정리하시면 좋겠네요.도움이 되시길 바랍니다.

spark (224,800 포인트) 님이 2022년 11월 7일 답변
spark님이 2022년 11월 7일 수정
...