
안드로이드에서 데이터를 보여주는 방식은 정말 여러가지가 있다. View를 통해 데이터를 보여주고 보여주는 방식을 Layout을 통해 정한다. 그 중에서 View에는 TextView, ImageView, View 등 많은 종류가 있지만 그 중에서 많이 사용되고 복잡한 ListView와 RecyclerView에 대해 공부한 내용을 기록해보려고 한다.
이름에서도 알 수 있듯이 반복되는 뷰를 리스트화하여 나란히 보여준다.
<?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"
tools:context=".MainActivity">
<ListView
android:id="@+id/my_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
// activity_main.xml
먼저 간단히 리니어 레이아웃 안에 리스트 뷰를 넣어주고 id를 지정한다. 그리고 이 ListView에 반복될 뷰도 만들어준다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ImageView
android:id="@+id/list_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/list_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@id/list_image_view"
android:textSize="20sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="50dp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
// custom_list.xml
굉장히 유용한 constraint layout을 통해 반복될 요소를 만들어주었다. 이미지와 텍스트를 받고 id도 지정해준다.
package com.example.listrecyclerviewpractice
class DataModel (val profile: Int, val name: String)
그리고 간단히 데이터 모델을 클래스로 설정하여 반복되는 뷰 안에 들어갈 데이터들을 저장할 수 있도록 한다. 이 부분에서 신기했던 것은 drawble 내의 이미지를 Int로 받을 수 있다는 점이었다.
package com.example.listrecyclerviewpractice
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
class CustomAdaptor (val context: Context, val DataList: ArrayList<DataModel>): BaseAdapter() {
private val TAG: String = "로그"
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
Log.d(TAG, "CustomAdaptor - getView() called");
val data = DataList[position]
val view: View = LayoutInflater.from(context).inflate(R.layout.custom_list, null)
val profile = view.findViewById<ImageView>(R.id.list_image_view)
val name = view.findViewById<TextView>(R.id.list_text_view)
profile.setImageResource(data.profile)
name.text = data.name
return view
}
override fun getItem(position: Int): Any {
return DataList[position]
}
override fun getItemId(position: Int): Long {
return 0L
}
override fun getCount(): Int {
return DataList.size
}
}
// CustomAdater.kt
그리고 어댑터를 만들어준다. 어댑터는 ListView와 custom_list.xml을 연결해주는 역할을 한다. 코드를 보면 DataModel의 배열을 받아서 각각 뷰에 연결해준다.
BaseAdapter를 implements하여 가져오는 메소드들에는 position이라는 정수 변수가 있는데 이를 통해 데이터를 인덱싱할 수 있다.
LayoutInflater를 통해 custom_list.xml을 가져오고 그 xml에서 findViewById를 통해 뷰를 가져온 후 각각 들어온 데이터들을 등록해주면 된다.
package com.example.listrecyclerviewpractice
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ListView
import com.example.listrecyclerviewpractice.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var DataList: ArrayList<DataModel> = ArrayList<DataModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
for(i in 1..10) {
DataList.add(DataModel(R.drawable.ic_launcher_foreground, "$i 번"))
}
var myListView = findViewById<ListView>(R.id.my_list_view)
myListView.adapter = CustomAdaptor(this, DataList)
}
}
// MainActivity.kt
메인 액티비티에서는 먼저 DataList라는 이름으로 더미 데이터를 생성하고 activity_main.xml의 ListView를 가져온다음 adapter를 통해 붙여주면 된다.

이렇게 각각 데이터와 이미지들을 불러오는 것을 확인할 수 있다.
스마트폰의 리소스는 한정되어 있다. 따라서 최대한 적은 자원을 사용하여 프로그래밍을 해야하는데 만약 리스트 뷰에 들어갈 내용이 많다면 성능 저하의 원인이 된다. 그리고 이 전에 view binding에 대해서 다루었는데, ListView에서는 뷰 바인딩이 안되는 것으로 보인다. 액티비티에서 binding에 어댑터를 먹일 수 없었고 커스텀 뷰 홀더를 통해서 하려고 해도 ListView가 뷰 홀더를 가질 수 없었다.
view binding을 사용할 수 없다는 것은 결국 널 세이프하지 않는 프로그래밍이기 때문에 이 역시 좋지 않은 방법이라고 할 수 있겠다.
그렇다면 Recycler View는 뭐가 다를까.
리사이클러 뷰는 리스트 뷰와 다르게 생성되는 뷰를 이름 그대로 계속해서 재활용한다.

이 뷰에서 1번이 안보이도록 밑으로 스크롤링하면 1번 뷰가 다시 아래로 내려가서 새로운 데이터를 받아주는 뷰의 역할을 하게 된다. 즉 리스트 뷰보다 효율적인 방법이다.
이번엔 리사이클러 뷰를 통해 위와 동일한 뷰를 만들어보도록 하겠다. 먼저 xml 파일먼저 작성을 하도록하겠다.
<?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">
<Button
android:id="@+id/to_recycler_view_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="To RecyclerView"
android:textAllCaps="false"
/>
<ListView
android:id="@+id/my_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
// activity_main.xml
리사이클러 뷰가 있는 액티비티로 이동해주는 버튼을 하나 만든다.
<?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">
<Button
android:id="@+id/to_recycler_view_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="To RecyclerView"
android:textAllCaps="false"
/>
<ListView
android:id="@+id/my_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
// activity_sub.xml
그리고 서브 액티비티용 xml 파일을 만들어주고 SubActivity 파일을 만들어주고 manifest에 등록해주도록 한다. 서브 액티비티에서는 뷰 바인딩을 통해 뷰를 생성해보도록 하겠다.
그리고 MainActivity에
var toRecyclerViewButton = findViewById<Button>(R.id.to_recycler_view_button)
toRecyclerViewButton.setOnClickListener{
var intent = Intent(this@MainActivity, SubActivity::class.java)
intent.putExtra("dataList", DataList as Serializable)
startActivity(intent)
}
다음과 같은 코드를 추가하여 서브 액티비티로 이동할 수 있도록 해준다. intent를 통해 데이터를 전달해줄때 오류가 발생했는데 위에서 만든 DataModel을
package com.example.listrecyclerviewpractice
import java.io.Serializable
class DataModel (val profile: Int, val name: String): Serializable
위와 같이 Serializable를 상속받아야만 한다. 왜냐하면 우리가 전달해주는 DataList는 커스텀 배열 객체이기 때문이다.
이제 리사이클러 뷰를 만들 준비가 되었다. 먼저 RecyclerViewHolder, RecyclerViewAdapter라는 클래스 파일들을 만들어준다.
그리고 SubActivity를 아래와 같이 작성해준다.
package com.example.listrecyclerviewpractice
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.listrecyclerviewpractice.databinding.ActivitySubBinding
class SubActivity : AppCompatActivity() {
lateinit var binding: ActivitySubBinding
lateinit var recyclerViewAdapter: RecyclerViewAdapter
private val TAG: String = "로그"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySubBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toListViewButton.setOnClickListener{
val intent = Intent(this@SubActivity, MainActivity::class.java)
startActivity(intent)
}
val dataList = intent.getSerializableExtra("dataList") as ArrayList<DataModel>
recyclerViewAdapter = RecyclerViewAdapter(dataList)
binding.myRecyclerView.apply {
layoutManager =LinearLayoutManager(this@SubActivity, LinearLayoutManager.VERTICAL, false)
adapter = recyclerViewAdapter
}
}
}
천천히 살펴보면 먼저 뷰 바인딩을 해주고 메인 액티비티의 intent로 부터 넘어온 dataList를 받는다. 그리고 어댑터에 우리가 보여줄 dataList를 넣어주고 바인딩된 리사이클러 뷰를 apply 메소드를 통해 설정을 한다.
먼저 layoutManager를 통해 수직으로 나열할지 수평으로 나열할지 정하고 마지막 false는 나열 순서를 뒤집을 건지 정하는 파라미터이다. 그리고 어댑터를 정해준다.
이제 어댑터를 작성하기 전에 먼저 커스텀 뷰 홀더를 만들 것이다. 뷰 홀더는 이 뷰에 어떤 데이터가 어떤 뷰에 대응되는지 정해준다.
package com.example.listrecyclerviewpractice
import androidx.recyclerview.widget.RecyclerView
import com.example.listrecyclerviewpractice.databinding.CustomListBinding
class RecyclerViewHolder(binding: CustomListBinding):RecyclerView.ViewHolder(binding.root){
private val listImageView = binding.listImageView
private val listTextView = binding.listTextView
fun bind(dataModel: DataModel){
listTextView.text = dataModel.name
listImageView.setImageResource(dataModel.profile)
}
}
// RecyclerViewHolder.kt
먼저 바인딩을 생성자로 받아와주고 bind 메소드를 만들어준다. 이를 통해 list_text_view라는 이름을 가진 id는 dataModel의 name에 대응된다는 것을 알려준다. 이미지 뷰 역시 setImageResource를 통해 정해주면 된다.
그리고 이번엔 어댑터를 만들어보자
package com.example.listrecyclerviewpractice
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.example.listrecyclerviewpractice.databinding.CustomListBinding
class RecyclerViewAdapter(dataList: ArrayList<DataModel>): RecyclerView.Adapter<RecyclerViewHolder>() {
var dataList: ArrayList<DataModel> = dataList
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder(CustomListBinding.inflate(LayoutInflater.from(parent.context), parent,false))
}
override fun getItemCount() = this.dataList.size
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
holder.bind(this.dataList[position])
holder.itemView.setOnClickListener {v ->
Toast.makeText(v.context, this.dataList[position].name, Toast.LENGTH_SHORT).show()
}
}
}
어댑터는 RecyclerView.Adaper<VH>를 상속받는데 VH에는 우리가 만든 뷰 홀더를 넣어주면 된다. 그리고 필수 메소드들을 오버라이딩해준다. 먼저 onCreateViewHolder에서는 어떤 레이아웃을 사용하는지 알려준다. 우리는 ListView를 만들 때 사용했던 xml 파일을 그대로 사용하기 위해 CustomListBinding을 inflate하였다.
getItemCount는 얼마나 많은 데이터가 있는지 알려주면 되고 onBindViewHolder는 onCreateViewHolder 다음에 실행되는 라이프 사이클로 뷰 홀더를 자동으로 파라미터로 받는다. 그리고 position이라는 변수를 통해 데이터의 인덱싱이 가능한데 뷰 홀더에서 만들었던 bind 메소드를 통해 데이터들을 전부 지정해주면 된다. 그리고 또 홀더는 itemView를 통해 뷰의 메소드들을 사용할 수 있는데, 클릭 리스너를 통해 간단히 리사이클러 뷰를 터치하면 정보를 Toast로 보여주도록 코드를 작성하면 된다.
반복되는 데이터를 보여주는 안드로이드의 뷰에 대해 공부해보았다. 간단한 데이터는 리스트 뷰로 보여주면 되지만 인스타그램의 피드나 게시판의 게시물들은 굉장히 많아질 수 있으므로 뷰를 계속 재사용하는 리사이클러 뷰를 사용하는 것이 효과적이다.
한 번 MainActivity의 DataList의 개수 조정을 위해
for(i in 1..2000) {
DataList.add(DataModel(R.drawable.ic_launcher_foreground, "$i 번"))
}
위 처럼 개수를 2000개로 늘리고 리스트 뷰와 리사이클러 뷰를 테스트 해보았는데 리스트뷰는 400개 인덱스를 넘어가자 조금씩 버벅거리기 시작하였지만 리사이클러 뷰는 끝까지 부드럽게 넘어가는 것을 확인할 수 있었다. 코틀린에 대해서도 잘 모르고 안드로이드에 대해서 잘 모르기 때문에 굉장히 허접한 코드이지만 깃허브도 여기에 첨부하도록 하겠다.
다른 코드가 그렇다고 딱히 좋지도 않은 것 같다.