오늘은 by lazy, lateinit에 대한 것과, 리스트뷰, 그리드뷰, 리사이클러뷰를 공부했다. 오늘 하루 굉장히 집중해서 보냈는데, 하고 보니 많지도 않은 것 같다. 후...
lateinit과 by lazy에 대해서는 viewBinding 글에 추가하여 적어놓았다. by lazy는 오버헤드가 있다는 내용이다.
리스트뷰, 그리드뷰, 리사이클러뷰는 모두 어댑터뷰의 일종이다. 그러나 리스트뷰, 그리드뷰는 모두 legacy로 들어가있고, 리사이클러뷰가 유일신이다. 이전꺼는 스크롤에 따라 삭제, 생성을 하여 성능 문제가 있는지라 뷰를 재활용하는 리사이클러뷰가 더 좋다. 다만 어댑터만 설정하면 되는 리스트뷰, 그리드뷰와 달리 리사이클러뷰는 뷰홀더를 추가적으로 설정해야 한다. 또한 셋온아이템클릭리스너가 없는 것 같고, 어댑터에 onClick을 인터페이스로 만들고 메인에서 연결해서 아이템클릭 관련 이벤트를 처리할 수 있다.
메인 레이아웃은 적당히 짜준다. 상단에 출력용 텍뷰, 나머지 리사이클러뷰로 만들었다. (리사이클러뷰 디폴트가 버티컬 리니어인듯)
먼저 리사이클러뷰의 아이템 레이아웃을 짜준다. (좌측에 이미지뷰, 우측에 위아래로 텍스트뷰) (item_recyclerview.xml)
그리고 이에 맞춰 MyItem 데이터 클래스를 만든다.
data class MyItem(val resId: Int, val name: String, val real: Double)
이거면 끝이다. (데이터 클래스는 이렇게까지 간단할 수 있는건가..)
원본 데이터를 만든다.
val dataList = mutableListOf(
MyItem(R.drawable.developer, "dev1", 0.1),
MyItem(R.drawable.developer2, "dev2", 0.2),
... )
메인의 나머지는 아래가 전부다. (어댑터부터 만들어야 한다. 아이템 클릭 부분은 이후에 서술)
val adapter = MyAdapter(dataList)
binding.rv.adapter = adapter
binding.rv.layoutManager = LinearLayoutManager(this)
/** 아이템 클릭 이벤트로 메인의 뷰의 값을 변경하는 함수를 작성해 인터페이스 변수에 집어넣기 */
adapter.itemClick = object : MyAdapter.ItemClick {
override fun onClick(view: View, position: Int) {
val item = dataList[position]
binding.tv.text = "$position :: $view\n${item.name}, ${item.real}"
}
}
중요한 건 어댑터다. 전체 코드부터 구경하고, 찬찬히 살펴보자.
class MyAdapter(private val itemList: MutableList<MyItem>) : RecyclerView.Adapter<MyAdapter.Holder>() {
inner class Holder(binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {
val iv = binding.ivRv
val tvName = binding.tvRvName
val tvReal = binding.tvRvReal
}
/** 아이템 클릭에 대한 이벤트를 메인으로 넘겨주고 싶을 때, 이를 위한 인터페이스 */
interface ItemClick {
fun onClick(view: View, position: Int)
}
var itemClick: ItemClick? = null
// TODO: inflate의 매개변수들은 뭐지..
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder =
Holder(ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun getItemCount(): Int = itemList.size
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.itemView.setOnClickListener { itemClick?.onClick(it, position) }
val item = itemList[position]
holder.iv.setImageResource(item.resId)
holder.tvName.text = item.name
holder.tvReal.text = item.real.toString()
}
override fun getItemId(position: Int): Long = position.toLong()
}
가장 먼저 뷰홀더를 이너 클래스로 만든다.
inner class Holder(binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {
val iv = binding.ivRv
val tvName = binding.tvRvName
val tvReal = binding.tvRvReal
}
RecyclerView.ViewHolder(binding.root)를 상속받는다.
binding은 매개변수로, 아이템 레이아웃의 바인딩이다.
홀더는 아이템 레이아웃의 이미지뷰, 텍뷰들을 들고있는다. (onBindViewHolder에서 포지션에 따라 내용을 바꿔주기 위함)
class MyAdapter(private val itemList: MutableList<MyItem>) : RecyclerView.Adapter<MyAdapter.Holder>()
MyAdapter 클래스는 RecyclerView.Adapter<MyAdapter.Holder>()를 상속받는다.
매개변수로는 아이템 리스트를 받는다.
오버라이드 해야 한다는 경고를 따라가면 onCreateViewHolder, getItemCount, onBindViewHolder 가 자동 생성된다.
(예제에서는 getItemId도 오버라이드 해주었다)
(온클릭 부분의 itemClick은 아래에)
// inflate 매개변수에 왜 저렇게 들어가는지는 모르겠지만 일단 이렇게 적으면 된다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder =
Holder(ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun getItemCount(): Int = itemList.size
// 뷰가 재사용될 때, 내용을 바꿔준다.
// 온클릭리스너도 새로 넣어주는데, 메인에서 넣어준 인터페이스의 온클릭 함수에 뷰와 포지션을 넣어 실행하는 식이다.
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.itemView.setOnClickListener { itemClick?.onClick(it, position) }
val item = itemList[position]
holder.iv.setImageResource(item.resId)
holder.tvName.text = item.name
holder.tvReal.text = item.real.toString()
}
// 이건 없어도 돌아가던데, 왜 오버라이드 해주는지 모르겠다.
override fun getItemId(position: Int): Long = position.toLong()
// 메인에서 온클릭 함수를 정의해 원하는 동작을 하게끔 하기 위한 인터페이스
interface ItemClick { fun onClick(view: View, position: Int) }
// 메인에서 함수를 오버라이드하여 인터페이스에 담아 변수에 집어넣는다
var itemClick: ItemClick? = null
itemClick은 ItemClick 인터페이스 변수이며, 인터페이스는 onClick 함수를 담고 있다. 이 온클릭 함수는 메인에서 오버라이드되어 메인에서 동작할 함수를 집어넣어준다.
(온클릭 리스너 세팅을 어댑터가 (뷰홀더를 바인드할 때) 해주는데, 메인의 다른 위젯의 값을 변경하고자 하므로, 메인에서 인터페이스에 담아 넘겨주는 것이다)
그러면 이제 메인에서의 아이템클릭 부분을 이해할 수 있다.
/** 아이템 클릭 이벤트로 메인의 뷰의 값을 변경하는 함수를 작성해 인터페이스 변수에 집어넣기 */
adapter.itemClick = object : MyAdapter.ItemClick {
override fun onClick(view: View, position: Int) {
val item = dataList[position]
binding.tv.text = "$position :: $view\n${item.name}, ${item.real}"
}
}
어댑터는 뷰홀더를 다시 바인드할 때 리스너를 재등록하는데, 여기에서 포지션 값이 들어가고, 이 포지션 값을 메인이 가져오는 것처럼 작동된다. 그래서 포지션에 대응하는 아이템의 정보로 메인의 다른 뷰의 값을 변경하도록 한다.
이렇게 정리는 했지만, 아직 리사이클러뷰에서 이해하지 못한 부분도 있다. 이 부분들은 나중에 사용해보면서 차차 익혀야겠다.