안드로이드에서 데이터를 보여주는 방식은 정말 여러가지가 있다. 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개 인덱스를 넘어가자 조금씩 버벅거리기 시작하였지만 리사이클러 뷰는 끝까지 부드럽게 넘어가는 것을 확인할 수 있었다. 코틀린에 대해서도 잘 모르고 안드로이드에 대해서 잘 모르기 때문에 굉장히 허접한 코드이지만 깃허브도 여기에 첨부하도록 하겠다.
다른 코드가 그렇다고 딱히 좋지도 않은 것 같다.