안드로이드 ListView, RecyclerView

PEPPERMINT100·2020년 12월 27일
1
post-thumbnail

서론

안드로이드에서 데이터를 보여주는 방식은 정말 여러가지가 있다. View를 통해 데이터를 보여주고 보여주는 방식을 Layout을 통해 정한다. 그 중에서 View에는 TextView, ImageView, View 등 많은 종류가 있지만 그 중에서 많이 사용되고 복잡한 ListViewRecyclerView에 대해 공부한 내용을 기록해보려고 한다.

ListView

이름에서도 알 수 있듯이 반복되는 뷰를 리스트화하여 나란히 보여준다.

<?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

그리고 어댑터를 만들어준다. 어댑터는 ListViewcustom_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.xmlListView를 가져온다음 adapter를 통해 붙여주면 된다.

이렇게 각각 데이터와 이미지들을 불러오는 것을 확인할 수 있다.

ListView의 단점

스마트폰의 리소스는 한정되어 있다. 따라서 최대한 적은 자원을 사용하여 프로그래밍을 해야하는데 만약 리스트 뷰에 들어갈 내용이 많다면 성능 저하의 원인이 된다. 그리고 이 전에 view binding에 대해서 다루었는데, ListView에서는 뷰 바인딩이 안되는 것으로 보인다. 액티비티에서 binding에 어댑터를 먹일 수 없었고 커스텀 뷰 홀더를 통해서 하려고 해도 ListView가 뷰 홀더를 가질 수 없었다.

view binding을 사용할 수 없다는 것은 결국 널 세이프하지 않는 프로그래밍이기 때문에 이 역시 좋지 않은 방법이라고 할 수 있겠다.

그렇다면 Recycler View는 뭐가 다를까.

리사이클러 뷰는 리스트 뷰와 다르게 생성되는 뷰를 이름 그대로 계속해서 재활용한다.

이 뷰에서 1번이 안보이도록 밑으로 스크롤링하면 1번 뷰가 다시 아래로 내려가서 새로운 데이터를 받아주는 뷰의 역할을 하게 된다. 즉 리스트 뷰보다 효율적인 방법이다.

Recycler View

이번엔 리사이클러 뷰를 통해 위와 동일한 뷰를 만들어보도록 하겠다. 먼저 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라는 이름을 가진 iddataModelname에 대응된다는 것을 알려준다. 이미지 뷰 역시 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 파일을 그대로 사용하기 위해 CustomListBindinginflate하였다.

getItemCount는 얼마나 많은 데이터가 있는지 알려주면 되고 onBindViewHolderonCreateViewHolder 다음에 실행되는 라이프 사이클로 뷰 홀더를 자동으로 파라미터로 받는다. 그리고 position이라는 변수를 통해 데이터의 인덱싱이 가능한데 뷰 홀더에서 만들었던 bind 메소드를 통해 데이터들을 전부 지정해주면 된다. 그리고 또 홀더는 itemView를 통해 뷰의 메소드들을 사용할 수 있는데, 클릭 리스너를 통해 간단히 리사이클러 뷰를 터치하면 정보를 Toast로 보여주도록 코드를 작성하면 된다.

결론

반복되는 데이터를 보여주는 안드로이드의 뷰에 대해 공부해보았다. 간단한 데이터는 리스트 뷰로 보여주면 되지만 인스타그램의 피드나 게시판의 게시물들은 굉장히 많아질 수 있으므로 뷰를 계속 재사용하는 리사이클러 뷰를 사용하는 것이 효과적이다.

한 번 MainActivityDataList의 개수 조정을 위해

  for(i in 1..2000) {
            DataList.add(DataModel(R.drawable.ic_launcher_foreground, "$i 번"))
        }

위 처럼 개수를 2000개로 늘리고 리스트 뷰와 리사이클러 뷰를 테스트 해보았는데 리스트뷰는 400개 인덱스를 넘어가자 조금씩 버벅거리기 시작하였지만 리사이클러 뷰는 끝까지 부드럽게 넘어가는 것을 확인할 수 있었다. 코틀린에 대해서도 잘 모르고 안드로이드에 대해서 잘 모르기 때문에 굉장히 허접한 코드이지만 깃허브도 여기에 첨부하도록 하겠다.

다른 코드가 그렇다고 딱히 좋지도 않은 것 같다.

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

0개의 댓글