본문 바로가기

프로그래밍/Android

[Kotlin] Android Studio RecyclerView 사용법 (feat. MVVM)

What is RecyclerView?

View를 재활용(recycle)해서 리스트를 보여주는 ViewGroup이다. view들을 담는 컨테이너라고 생각하면 된다.

 

RecyclerView 이전에는 ListView를 사용하여 스크롤되는 리스트를 보여줬다.

 

하지만 ListView는 데이터 아이템(리스트의 각 요소)만큼 view를 생성하고 삭제하기 때문에 메모리와 성능 측면에서 좋지 않다.

 

필요한 view들을 계속 생성하고 삭제하는 이런 단점을 보완하기 위해 view를 재활용하는 RecyclerView가 나오게 되었다.

 

물론 view 재활용 외에도 ListView는 수직 스크롤만 지원한다거나 기본 애니메이션 지원이 없다거나 하는 문제점들도 보완했다!

 

 

 

RecyclerView의 구성 요소

ViewHolder

화면에 표시될 아이템뷰를 저장하는 객체이다.

 

Adapter에 의해 생성되고, 내가 보여주고자 하는 데이터를 실제 레이아웃(view)와 연결시킨다.

 

또한, 스크롤해서 위로 올라간 View를 재활용하기 위해서는 해당 View를 기억하고 있어야 되는데, 그 역할을 수행한다.

 

Adapter

아이템뷰를 를 생성해서 RecyclerView에 연결시키는 객체이다.

 

데이터와 RecyclerView 사이의 통신을 위한 연결창구라고 생각하면 된다.

 

RecyclerView.Adapter을 상속받아서 어댑터를 생성하고, 다음 3개의 메서드를 오버라이드해서 구현한다.

  • getItemCount: 전체 아이템 개수를 리턴
  • onCreateViewHolder: 아이템 뷰를 위한 ViewHolder와 그에 연결된 View를 생성하고 초기화
  • onBindViewHolder:  ViewHolder를 데이터와 연결

 

구현 방법

1. 데이터 지정

 

2. 데이터를 넣을 item xml과 activity/fragment xml 안에 RecyclerView만들기

 

3. Adapter와 ViewHolder 구현

 

4. 레이아웃 배치와 형태 지정 

 

5. Adapter에 데이터 연결

 

간단하게 계산기 형태의 view를 만들어보도록 하겠다. MVVM 패턴과 관련한 정보는 하단 링크에서 확인하면 된다.

 

https://gummy119.tistory.com/25

 

Android Architecture Design Pattern - MVVM (feat. 한국천문연구원 특일정보 API)

MVVM (Model View ViewModel) MVC, MVP 보다 더 의존성을 줄인 패턴이다. 처음에 프로젝트에 적용할 때는 아무래도 방황을 좀 하게 될 것이다. 하지만 쓰다보면 선녀 같다는 느낌을 받을 것이다! 코드 수정

gummy119.tistory.com

 

1.  데이터 지정 : CalculatorDTO

data class CalculatorDTO(val number : String)

 

 

2-1. item.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="items"
            type="com.example.bottomnavigation.CalculatorDTO" />
    </data>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/calculator_item_view"
    android:layout_width="60dp"
    android:layout_height="60dp"
    android:background="@drawable/calculator_item"
    android:backgroundTint="@color/black"

    android:textColor="@color/white"
    android:textStyle="bold"
    android:textSize="15sp"
    android:text="@{items.number}"
    android:gravity="center"
    android:layout_margin="15dp">

</TextView>

</layout>

 

2-2. fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <variable
            name="viewModel"
            type="com.example.bottomnavigation.ViewModel" />
    </data>

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        tools:context=".InputFragment">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center">


            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="30dp"
                android:background="@drawable/outline_black_round"
                android:backgroundTint="@color/black"
                android:orientation="vertical">

                <View
                    android:layout_width="60dp"
                    android:layout_height="10dp"
                    android:background="@drawable/outline_black_round"
                    android:backgroundTint="@color/neon_green"
                    android:layout_gravity="center"
                    android:layout_marginTop="15dp"/>

                <TextView
                    android:id="@+id/calculator_result"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"

                    android:padding="20dp"
                    android:text="@{viewModel.calculatedResult}"
                    android:textAlignment="textEnd"
                    android:textColor="@color/white"
                    android:textSize="40sp"
                    android:textStyle="bold" />

                <TextView
                    android:id="@+id/calculator_expression"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="20dp"
                    android:layout_marginEnd="20dp"

                    android:padding="10dp"
                    android:text="@{viewModel.inputExpression}"
                    android:textAlignment="textEnd"
                    android:textColor="@color/neon_green"
                    android:textSize="20sp"
                    android:textStyle="bold" />

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/calculator_recycleView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"

                    android:layout_margin="2dp"

                    android:background="@drawable/outline_black_round" />



            </LinearLayout>

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/input_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"

                android:layout_gravity="center"

                android:background="@drawable/outline_black_round"

                android:backgroundTint="@color/neon_green"
                android:text="등록하기"

                android:textColor="@color/black"
                android:textStyle="bold" />

        </LinearLayout>

    </FrameLayout>
</layout>

 

3. Adapter와 ViewHolder 구현

interface CalculatorClickListener {
    fun onItemClick(number : String)
}

 

class CalculatorAdapter(val clickListener: CalculatorClickListener)
    :RecyclerView.Adapter<CalculatorAdapter.CalculatorViewHolder>(){

    val TAG = "CalculatorAdapterTAG"

    private var calculateList = ArrayList<CalculatorDTO>()

    class CalculatorViewHolder(val binding : CalculatorItemBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalculatorViewHolder {
        val binding = CalculatorItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return CalculatorViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return calculateList.size
    }

    override fun onBindViewHolder(holder: CalculatorViewHolder, position: Int) {

        holder.binding.items = calculateList[position]

        holder.binding.calculatorItemView.setOnClickListener {
            clickListener.onItemClick(calculateList[position].number)
        }
    }

    fun setCalculator(calculateList : ArrayList<CalculatorDTO>){
        this.calculateList = calculateList
        notifyDataSetChanged()
    }

}

 

4. 레이아웃 배치와 형태 지정 및 Adapter에 데이터 연결

class InputFragment : Fragment() {

    lateinit var binding: FragmentInputBinding
    lateinit var viewModel : ViewModel
    private lateinit var context : Context
    lateinit var calculatorAdapter : CalculatorAdapter

    override fun onAttach(context: Context) {
        super.onAttach(context)
        this.context = context
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_input, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        init()
    }

    private fun init(){
        viewModel = ViewModelProvider(requireActivity()).get(ViewModel::class.java)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this

        viewModel.init()

        val calculatorList = viewModel.getCalculator()
        

        calculatorAdapter = CalculatorAdapter(object : CalculatorClickListener{
            override fun onItemClick(number: String) {
               //클릭했을 때 이벤트를 넣어주면 됨
            }
        })
		
        //Adapter 연결해주고
        binding.calculatorRecycleView.adapter = calculatorAdapter
        //Layout 지정
        binding.calculatorRecycleView.layoutManager = GridLayoutManager(context, 4)

        if(calculatorList != null){
        //Adapter에 데이터 연결
            calculatorAdapter.setCalculator(calculatorList)
        }
    }


}

 

ViewModel

class ViewModel : ViewModel() {

	private val _inputExpression = MutableLiveData<String>()
	val inputExpression : LiveData<String>
		get() = _inputExpression

	private val _calculatedResult = MutableLiveData<String>()
	val calculatedResult : LiveData<String>
		get() = _calculatedResult

	fun init(){
		 _calculatedResult.value = "0"
		_inputExpression.value = "0"

	}
	
    fun getCalculator() : ArrayList<CalculatorDTO>{
        val arrayList = ArrayList<CalculatorDTO>()

        (1..3).forEach { arrayList.add(CalculatorDTO("$it")) }
        arrayList.add(CalculatorDTO("/"))

        (4..6).forEach { arrayList.add(CalculatorDTO("$it")) }
        arrayList.add(CalculatorDTO("x"))

        (7..9).forEach { arrayList.add(CalculatorDTO("$it")) }

        arrayList.addAll(listOf(
            CalculatorDTO("-"),
            CalculatorDTO("DEL"),
            CalculatorDTO("0"),
            CalculatorDTO("="),
            CalculatorDTO("+")
        ))

        return arrayList

    }
}

 

 

 

 

 

https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ko#plan-your-layout