RecyclerView와 List를 이용해서 Affirmation 어플을 만든다.
코드를 파일 및 패키지로 구성해 코드를 논리적이고, 이해하기 쉽고, 유지관리하고, 확장할 수 있도록 구성할 수 있다.
사진 속 java 아래에 있는 세 폴더가 패키지이다.
com.example.affirmation
아래 model
패키지를 생성하고 그 아래 Affirmation.kt
파일을 생성했다. 개발자는 데이터를 모델링하거나 표현하는 클래스의 패키지 이름으로 model
을 사용하는 경우가 많다.
Affirmation.kt
의 내용을 다음과 같이 수정한다.
package com.example.affirmation.model
data class Affirmation(val stringResourceId: Int)
data
키워드를 추가해 데이터 클래스로 만든다.
속성은 하나 이상 정의되어 있어야 하기 때문에 생성자를 만든다. Affirmation 인스턴스를 만들 때 문자열 리소스 ID를 전달해야 한다.
data
패키지를 추가하고 그 아래 Datasource
클래스를 추가한다.
package com.example.affirmation.data
import com.example.affirmation.R
import com.example.affirmation.model.Affirmation
class Datasource {
fun loadAffirmation(): List<Affirmation>{
return listOf<Affirmation>(
Affirmation(R.string.affirmation1),
Affirmation(R.string.affirmation2),
. . .
Affirmation(R.string.affirmation10)
)
}
}
item
- 목록에 올라갈 단일 데이터 항목. 이 앱에서는 Affirmation 객체 하나를 나타낸다.adapter
- RecyclerView에 표시할 수 있도록 데이터를 가져와 준비한다.ViewHolders
- view들을 담는 풀. 리싸이클러뷰가 affirmation을 띄우기 위해 뷰를 사용하거나 재사용할 수 있는 poolRecyclerView
- Views on screenactivity_main.xml
에 RecyclerView
를 생성한다.<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">
</androidx.recyclerview.widget.RecyclerView>
layoutManager
는 리사이클러뷰를 선형 목록이나 그리드와 같은 다양한 방식으로 표시할 수 있도록 지원한다.scrollbars
속성도 설정해준다.각 항목의 레이아웃이 될 list_item.xml을 res>layout
에 생성한다. 코드는 다음과 같다.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
이 레이아웃은 RecyclerView
에 하위 요소로 추가되므로 ViewGroup
이 필요하지 않다.
adapter
패키지를 추가하고 그 아래 ItemAdapter.kt
파일을 생성한다.
ItemAdapter
Affirmation 목록을 어댑터에 전달할 수 있도록 생성자에 매개변수를 추가한다.
class ItemAdapter(private val dataset: List<Affirmation>){}
ItemAdapter
에 Context
매개변수를 추가한다. 앱에 대한 기타 정보는 ItemAdapter
인스턴스에 전달할 수 있는 Context
객체 인스턴스에 저장된다.class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>){}
ViewHolder
를 만든다.RecyclerView
는 뷰와 직접 상호작용하는 것이 아니라 ViewHolders
를 처리한다.ViewHolder
는 RecyclerView
의 목록 중 하나의 항목 뷰를 나타낸다. class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.item_title)
}
}
View
타입 변수 view를 매개변수로 추가한다.ItemViewHolder
클래스는 RecyclerView.ViewHolder
의 서브클래스가 된다. view 매개변수는 슈퍼클래스의 생성자에 전달한다.RecyclerView.Adapter
에서 ItemAdapter
를 확장한다.class ItemAdapter(
private val context: Context,
private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.item_title)
}
}
추상 메소드를 구현해야 한다. (이전 강의에서 추상 클래스에 대해 다룰 때 하위 클래스 생성 시 추상 메소드를 전부 구현해줘야 하는 것을 배웠다.)
ItemAdapter
에 커서를 놓고 Control+I를 눌러 구현해야 하는 메소드 목록getItemCount()
, onCreateViewHolder()
, onBindViewHolder()
을 확인할 수 있다.
메소드를 하나씩 구현해보자.
getItemCount()
데이터 세트의 크기를 반환한다.
// 방법1: 간결
override fun getItemCount() = dataset.size
// 방법2
override fun getItemCount(): Int {
return dataset.size
}
onCreateViewHolder()
새 뷰 홀더를 만들기 위해 레이아웃 관리자가 호출한다. (재사용할 수 있는 기존 뷰 홀더가 없는 경우)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val adapterLayout = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ItemViewHolder(adapterLayout)
}
parent
매개변수는 상위 요소 즉 RecyclerView
viewType
은 리싸이클러뷰에 여러 뷰 유형이 있을 때 이를 구분하기 위해 필요하다.parent
의 context
에서 LayoutInflater
인스턴스를 가져온다. LayoutInflater
는 XML 레이아웃을 뷰 객체의 계층 구조롤 확장하는 방법을 알고 있다.LayoutInflater
객체 인스턴스에 마침표를 추가하고 그 뒤에 다른 메서드 호출을 배치하여 실제 목록 항목 뷰를 확장한다.onBindViewHolder()
목록 항목 뷰의 콘텐츠를 바꾸기 위해 레이아웃 관리자가 호출한다.
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = dataset[position]
holder.textView.text = context.resources.getString(item.stringResourceId)
}
Datasource
클래스의 인스턴스를 생성하고 loadAffirmations()
메소드를 호출해 데이터셋을 가져온다.val myDataset = Datasource().loadAffirmations()
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = ItemAdapter(this, myDataset)
recyclerView.setHasFixedSize(true)
RecyclerView
의 레이아웃 크기가 변경되지 않는다는 것을 아는 경우 이 설정을 사용한다.class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myDataset = Datasource().loadAffirmation()
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = ItemAdapter(this, myDataset)
recyclerView.setHasFixedSize(true)
}
}
drawable에 image1 ~ image10 이미지 10장을 추가했다.
Affirmation
클래스 생성자의 매개변수로 이미지 리소스 ID를 추가해야 한다.
stringResourceId
와 imageResourceId
는 모두 정숫값이기 때문에 잘못된 순서로 인지를 전달 할 수 있다. 이를 방지하기 위해 리소스 주석을 사용한다.
@StringRes
주석을 문자열 리소스 ID 속성에, @DrawableRes
주석을 드로어블 리소스 ID 속성에 추가한다. 그러면 잘못된 유형의 리소스 ID를 제공하는 경우 경고가 표시된다.
data class Affirmation(
@StringRes val stringResourceId: Int,
@DrawableRes val imageResourceId: Int
)
Datasource도 두개의 리소스 ID를 전달하도록 수정한다.
Affirmation(R.string.affirmation1, R.drawable.image1),
목록의 단일 항목에 이미지와 텍스트가 들어가야 하므로 list_item.xml를 다음과 같이 수정한다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="194dp"
android:id="@+id/item_image"
android:importantForAccessibility="no"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
</LinearLayout>
다음과 같이 ItemViewHolder
와 onBindViewHolder
를 수정한다.
ItemViewHolder
가 레이아웃에서 참조할 item_image를 작성해준다.
onBinderViewHolder
에서는 imageResourceId
에 해당하는 이미지를 찾아 setImageResource
한다.
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view){
val textView: TextView = view.findViewById(R.id.item_title)
val imageView: ImageView = view.findViewById(R.id.item_image)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = dataset[position]
holder.textView.text = context.resources.getString(item.stringResourceId)
holder.imageView.setImageResource(item.imageResourceId)
}
MaterialCardView
를 사용해 카드뷰를 만든다.android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
처럼 스타일을 가져와 사용할 수 있다.colors.xml
과 themes.xml
을 수정한다.