화면에서 보이지 않는 부분을 생성하지 않고 화면에서 사라진 뷰를 재사용
동적으로 동일한 뷰 들이 늘어나는 화면 구조 사용
Adapter에 따라 다양한 뷰들이 존재
Adapter에 등록된 데이터들을 보여주는 뷰가 AdapterView로 종류는 다음과 같다.
Adapter 종류
Adapter은 내가 만든 아이템 홀더(아이템이 보여질 xml 파일)에 각각의 데이터들을 결합해 화면을 구성하게끔 해준다.
기본적으로 ArrayAdapter등의 adapter을 상속받아 만들 수 있다.
class MyAdapter(context: Context, val resource: Int, val objects: Array<String>) :
ArrayAdapter<String>(context, resource, objects) { //generic안에 adpater를 구성할 item의 타입을 넣어줌
//resource = item holder XML 파일
//objects = xml에 들어가는 아이템
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val inflater = LayoutInflater.from(context) //혹은 parent.context
val view = inflater.inflate(resource, null)
val tv = view.findViewById<TextView>(R.id.name)
tv.text = objects[position]
return view
}
}
Adapter은 기본적으로 context, resource, object를 생성자로 하여 만드는데
context는 뷰가 보여질 액티비티(this),
resource는 홀더의 XML,
object에는 홀더 안에 넣어줄 데이터들이 들어간다.
val adapter = MyAdapter(this, R.layout.item_row, COUNTRIES)
val listView = findViewById<ListView>(R.id.list)
listView.adapter = adapter
위 어댑터 객체를 만들어 listView에 연결해준다.
getView를 오버라이딩 하여 layout inflater에 resource를 연결해준다.
이 getView의 로그를 찍어보면 화면에서 스크롤 하면서 아이템 값들이 새로 보여질때마다 불린다.
리스트 뷰는 기본적으로 뷰를 재사용한다.
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val inflater = LayoutInflater.from(context) //혹은 parent.context
var view = convertView
if(view == null){
view = inflater.inflate(resource, null)
Log.d(TAG, "getView: inflate")
}
else{
Log.d(TAG, "getView: 재사용")
}
val tv = view!!.findViewById<TextView>(R.id.name)
tv.text = objects[position]
Log.d(TAG, "getView: $position")
return view
}
getView를 다음과 같이 코드를 수정하면 처음 보이는 화면들만 view에 inflate를 하고 그 이후는 스크롤을 하면 기존의 뷰에 데이터를 새로 넣는 방식으로 작동한다. 하지만 위 코드는 tv에 text를 넣기 위해 계속 findViewById를 하고 있다.
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
var holder: ViewHolder
if (view == null) { // if it's not recycled, initialize some attributes
val inf: LayoutInflater = LayoutInflater.from(parent.context)
view = inf.inflate(resource, null)
Log.d(TAG, "inflate")
//미리 findViewById 해줌
holder = ViewHolder()
holder.tv = view.findViewById(R.id.name)
//view에 태그로 달아줌
view.tag = holder
}
else{
holder = view.tag as ViewHolder
}
holder.tv.text = objects[position]
return view!!
}
}
class ViewHolder{
lateinit var tv:TextView
}
ViewHolder라는 클래스를 만들어 findViewById를 미리 해주고 일일히 달아줄 뷰에는 태그에 holder를 달아준다. 이 holder에 데이터를 할당해주면 view가 할당된 tag(holder)의 값을 가져와 보여준다.
이 Holder패턴을 매번 작성해주는 것을 방지하기 위해 리사이클러뷰를 사용한다.
리사이클러뷰는
어댑터는 3개의 메서드를 상속받아 작성한다.
class HelloListView : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = MyAdapter(COUNTRIES)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this,LinearLayoutManager.VERTICAL, false) // xml에서 app:layoutmanager로 설정 가능
recyclerView.adapter = adapter
}
class MyAdapter(var list: ArrayList<String>) :
RecyclerView.Adapter<MyAdapter.CustomViewHolder>() {
class CustomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var name = itemView.findViewById<TextView>(R.id.name)
fun bindInfo(data : String){
name.text = data
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
// view 생성 -> holder의 parameter로 넣어줌
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent)
return CustomViewHolder(view)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
// holder.name.text = list[position]
holder.bindInfo(list[position])
}
override fun getItemCount(): Int {
return list.size
}
}
뷰 바인딩으로 바꾸어 재작성
class MyAdapter(var list: ArrayList<String>) :
RecyclerView.Adapter<MyAdapter.CustomViewHolder>() {
class CustomViewHolder(binding: ItemRowBinding) : RecyclerView.ViewHolder(binding.root) {
var name = binding.name
fun bindInfo(data : String){
name.text = data
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
// view 생성 -> holder의 parameter로 넣어줌
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
val binding : ItemRowBinding = ItemRowBinding.inflate(LayoutInflater.from(parent.context))
return CustomViewHolder(binding)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
// holder.name.text = list[position]
holder.bindInfo(list[position])
}
override fun getItemCount(): Int {
return list.size
}
}
adapter 연결
val adapter = MyAdapter(COUNTRIES)
binding.recyclerView.layoutManager = LinearLayoutManager(this,LinearLayoutManager.VERTICAL, false) // xml에서 app:layoutmanager로 설정 가능
binding.recyclerView.adapter = adapter
//구분선 넣어주기
val deco = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
binding.recyclerView.addItemDecoration(deco)
onCreate에서 adapter를 만들어 넣어준다.
구분선을 넣기 위해 DividerItemDecoration을 사용했다.
onCreateViewHolder나 onBindViewHolder 등에 넣을 수 있다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
// view 생성 -> holder의 parameter로 넣어줌
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
val binding: ItemRowBinding =
ItemRowBinding.inflate(LayoutInflater.from(parent.context))
return CustomViewHolder(binding).apply {
binding.root.setOnClickListener{
Toast.makeText(parent.context, "선택됨",Toast.LENGTH_SHORT).show()
}
}
}
하지만 adapter의 역할은 근본적으로 아이템을 홀더에 넣어주는 것이기 때문에 외부에서 작성하는 것이 더 바람직할 것이다.
외부파일로 작성한 adapter에 activity가 이벤트 핸들링 작업을 하는 것이 각각의 파일의 역할에 맞을 것이다.
이를 위해서 adapter에 인터페이스를 만들어 구현체를 activity에 전달해준다.
interface ItemClickListener{
fun onClick( view:View, data:String, position:Int)
}
lateinit var itemClickListener: ItemClickListener
그리고 onBindViewHolder에서 각 아이템에 클릭 리스너를 달아준다.
fun bindInfo(data:String){
name.setText(data)
itemView.setOnClickListener(){
itemClickListener.onClick(it, data, layoutPosition )
}
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.apply{
bindInfo(list[position])
}
}
이제 액티비티에서 anonymous nested class 형태로 어댑터에 구현한 itemClickListener을 가져와 구현을 해준다.
adapter.itemClickListener = object : MyAdapter.ItemClickListener {
override fun onClick(view: View, data: String, position: Int) {
Toast.makeText(this@HelloListViewDeletable, "item clicked...${data}", Toast.LENGTH_SHORT).show()
}
}
adapter.notifyDataSetChanged()
로 삭제, 추가, 수정 이벤트마다 어댑터가 바뀐 데이터를 적용하도록 처리할 수 있다. 그밖에 itemChanged 등등 다른 구체적인 메소드들이 존재한다.
하지만 화면 표시 아이템이 많아지면 이는 비효율 적이다.
다수의 경우에서 이러한 불필요한 비용을 줄이기 위해 ListAdapter에서 DiffUitl을 구현해준다.
DiffUtil.itemCallback
을 구현하는데,
companion object StringComparator : DiffUtil.ItemCallback<String>(){
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
위 두 메서드를 구현해주어야 한다.
또한 ListAdapter에서 아이템 리스트를 받아 관리해주므로
class MyAdapter : ListAdapter<String, MyAdapter.CustomViewHolder>(StringComparator)
기존의 커스텀 adapter의 getItemCount 구현이 불필요하다.
이제 adapter 사용단에서 데이터를 연결해주려면
adapter.submitList(data.toMutableList())
submitList로 전달해주고, 데이터가 바뀔 때에도 똑같이 submitList로 전달하면 전체를 다시 그리는 것이 아닌 변경된 아이템만 업데이트 해준다.
전체코드
class HelloListView : AppCompatActivity(){
lateinit var adapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val data = COUNTRIES
adapter = MyAdapter()
// submitList로 데이터를 제공한다.
// 기존의 목록과 새로운 목록을 비교하기 때문에, 두 목록의 reference는 달라야 한다. toMutableList로 새로 생성.
adapter.submitList(data.toMutableList())
val rview = findViewById<RecyclerView>(R.id.recyclerView)
rview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
rview.adapter = adapter
adapter.itemClickListener = object : MyAdapter.ItemClickListener {
override fun onClick(view: View, data: String, position: Int) {
Toast.makeText(this@HelloListView, "item clicked...${data}", Toast.LENGTH_SHORT).show()
}
}
adapter.deleteListener = object : MyAdapter.DeleteListener {
override fun delete(position: Int) {
Log.d(TAG, "delete: $position")
data.removeAt(position)
// adapter.notifyDataSetChanged() //필요없어짐.
//data 의 reference가 바뀌어야 ListAdapter가 워킹함.
adapter.submitList(data.toMutableList())
}
}
//구분선 추가.
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
rview.addItemDecoration(dividerItemDecoration)
}
//ListAdapter에서 목록관리하므로, collection으로 받을 필요 없음.
class MyAdapter : ListAdapter<String, MyAdapter.CustomViewHolder>(StringComparator){
companion object StringComparator : DiffUtil.ItemCallback<String>(){
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
inner class CustomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnCreateContextMenuListener {
init{
itemView.setOnCreateContextMenuListener(this)
}
var name:TextView = itemView.findViewById<TextView>(R.id.name)
fun bindInfo(data:String){
name.setText(data)
itemView.setOnClickListener(){
itemClickListener.onClick(it, data, layoutPosition )
}
}
override fun onCreateContextMenu(menu: ContextMenu, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
val selected = itemView.findViewById<TextView>(R.id.name).text.toString()
Log.d(TAG, "onCreateContextMenu: ${selected}")
val menuItem = menu.add(0, 0 , 0, "delete");
menuItem?.setOnMenuItemClickListener {
deleteListener.delete(layoutPosition)
Toast.makeText(itemView.context, "Hello:$selected", Toast.LENGTH_LONG).show()
true
}
}
}
interface ItemClickListener{
fun onClick( view:View, data:String, position:Int)
}
lateinit var itemClickListener: ItemClickListener
interface DeleteListener{
fun delete( position:Int)
}
lateinit var deleteListener: DeleteListener
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : CustomViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
Log.d(TAG, "inflate")
return CustomViewHolder(view)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.apply{
//getItem : 위치의 값을 가져옴.
// bindInfo(myList[position])
bindInfo(getItem(position))
}
}
// getItemCount 필요없음.
// override fun getItemCount(): Int {
// return myList.size
// }
}
만들고 까먹는 리사이클러뷰,, 천천히 튜토리얼식으로 적어나가 보자.
리사이클러뷰에서 하나의 리스트 요소에 대응될 xml 파일을 만들어보자
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_custom"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/phoneman"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_custom"
android:layout_width="91dp"
android:layout_height="42dp"
android:text="이름"
android:gravity="center"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/iv_custom"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
png파일을 가져와 imageview안에 넣고 constraintView안에 이미지와 텍스트를 넣어서 간단하게 구성하였다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
메인 액티비티 xml 파일이다. 이 곳안에 리사이클러뷰를 넣어준다. 디자인 모드에서 팔레트를 켜서
Common의 recyclerview를 드래그 앤 드롭으로 가져오는 것으로 편하게 할 수 있다.
Component Tree의 LinearLayout 안으로 드래그 해준다.
먼저 리사이클러뷰를 띄울 액티비티에 layoutmanager을 정의한다.
recyclerView.layoutManager = LinearLayoutManager(this)//linearlayout매니저를 MainActivity가 통제하도록 함
이어서 어댑터를 만들어줘야 되는데, 리사이클러뷰의 해당하는 xml을 가져오고, 몇개의 뷰를 만들어야 되는지를 지정하는 클래스로, 새로 CustomAdapter.kt 코틀린클래스 파일을 만들어서 정의해준다.
recyclerView.adapter=CustomAdapter()
CustomAdapter.kt
package com.example.test30
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class CustomViewHolder(v : View) : RecyclerView.ViewHolder(v) {}//한 화면에 표시되는 리스트 요소들을 나타냄
class CustomAdapter : RecyclerView.Adapter<CustomViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val cellForRow = LayoutInflater.from(parent.context).inflate(R.layout.custom_list,parent,false)//custom_list 파일을 뷰로 만듦
return CustomViewHolder(cellForRow)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
}
override fun getItemCount(): Int {//뷰 홀더의 개수를 반환
return 100
}
}
3개의 멤버메서드 onCreateViewHolder, onBindViewHolder, getItemCount를 오버라이드 해줘야 하는데,
onCreateViewHolder는 LayoutInflater을 통하여 custom_list xml 파일을 하나의 뷰 요소로 만들어주고, getItemCount는 그 뷰가 몇개인지를 반환해준다.
CustomAdapter.kt 에서 Data 클래스를 만들어주고,
class Data(val profile:Int, val name:String)
mainActivity 안에 뷰의 데이터로 넣을 값들을 list로 지정해줬다.
val DataList = arrayListOf(
Data(R.drawable.phoneman, "0번"),
Data(R.drawable.phoneman, "1번"),
Data(R.drawable.phoneman, "2번"),
Data(R.drawable.phoneman, "3번"),
Data(R.drawable.phoneman, "4번"),
Data(R.drawable.phoneman, "5번"),
Data(R.drawable.phoneman, "6번"),
Data(R.drawable.phoneman, "7번"),
Data(R.drawable.phoneman, "8번"),
Data(R.drawable.phoneman, "9번"),
Data(R.drawable.phoneman, "10번")
)
이제 이 DataList를 CustomAdapter 코틀린 파일에서 뷰 안에 넣어주자.
먼저 customViewHolder에 프로필이미지와 name을 지정할 수 있도록 profile과 name 변수를 생성해주고 xml의 id 값들로 지정해준다.
class CustomViewHolder(v : View) : RecyclerView.ViewHolder(v) {//한 화면에 표시되는 리스트 요소들을 나타냄
val profile = v.iv_custom
val name = v.tv_custom
}
그리고 CustomAdapter 역시 main파일에 사용한 Datalist변수를 매개변수로 하여 getItemCount와 onBindViewHolder에 활용해준다.
class CustomAdapter(val DataList:ArrayList<Data>) : RecyclerView.Adapter<CustomViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val cellForRow = LayoutInflater.from(parent.context).inflate(R.layout.custom_list,parent,false)//custom_list 파일을 뷰로 만듦
return CustomViewHolder(cellForRow)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.profile.setImageResource(DataList[position].profile)
holder.name.text = DataList[position].name
}
override fun getItemCount(): Int {//뷰 홀더의 개수를 반환
return DataList.size
}
}