전체 코드
MainActivity
class MainActivity : AppCompatActivity() {
val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val data: MutableList<Memo> = loadData()
var adapter = CustomAdapter()
adapter.listData = data
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)
}
fun loadData(): MutableList<Memo> {
val data: MutableList<Memo> = mutableListOf()
for (no in 1..100) {
val title = "${no}번째 제목"
val date = System.currentTimeMillis()
var memo = Memo(no, title, date)
data.add(memo)
}
return data
}
}
class CustomAdapter(val listData: MutableList<Memo>) : RecyclerView.Adapter<CustomAdapter.Holder>() {
override fun onCreateViewHolder(parent: viewGroup, viewType: Int): Holder {
val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return Holder(binding)
}
override fun getItemCount() = listData.size
override fun onBindViewHolder(holder: Holder, position: Int) {
val memo = listData.get(position)
holder.setMemo(memo)
}
class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root) {
lateinit var currentMemo: Memo
init {
binding.root.setOnClicklistener {
Toast.makeText(binding.root.context, "클릭된 아이템 : ${currentMemo.title}", Toast.LENGTH_SHORT).show
}
}
fun setMemo(memo: Memo) {
currentMemo = memo
with (binding) {
textNo.text = "${currentMemo.no}"
textTitle.text = currentMemo.title
val sdf = SimpleDateFormat("yyyy-MM-dd")
val formatDate = sdf.format(memo.timestamp)
textDate.text = formatDate
}
}
}
}
Memo
data class Memo(var no: Int, var title: String, var timestamp: Long)
데이터 클래스 생성
✔ 리사이클러뷰에서 하나의 아이템 안에 들어가는 뷰 요소가 두 개 이상일 때, 뷰 요소들을 묶어서 하나의 데이터 클래스로 만들어 줍니다.
✔ 우리 예제에서는 하나의 아이템에 3개의 TextView(번호, 제목, 시간)를 담을 것입니다.
data class Memo(var no: Int, var title: String, var timestamp: Long)
아이템에 출력할 데이터 작성
✔ 위의 데이터 클래스의 형식에 맞춰 100개의 데이터를 작성할 것입니다.
✔ MainActivity 클래스 내부, onCreate() 메서드 아래에 하단 코드를 작성합니다.
fun loadData() : MutableList<Memo> {
val memoList = mutableListOf<Memo>() // memoList의 타입은 MutableList<Memo> 임을 알 수 있음
for (no in 1..100) {
val title = "${no}번째 제목"
val date = System.currentTimeMillis()
val memo = Memo(no, title, date)
}
return memoList
}
RecyclerView.Adapter를 상속받는 CustomAdapter 클래스 작성
✔ CustomAdapter(RecyclerView.Adapter)의 역할 : 리사이클러뷰에 띄워줄 데이터 전체를 관리
✔ Holder(ViewHolder)의 역할 : 아이템 하나하나를 관리. 즉, 개별 아이템에 대한 값들을 세팅하기 위한 용도
class CustomAdapter(val listData: MutableList<Memo>) : RecyclerView.Adapter<CustomAdapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
}
override fun getItemCount(): Int {
}
override fun onBindViewHolder(holder: Holder, position: Int) {
}
class Holder(binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root) {
}
}
1. CustomAdapter 클래스 선언하기
💬
◽ 위의 역할에 따라, CustomAdapter(이하 어댑터)는 Holder(이하 홀더)를 사용해서 아이템 레이아웃 값을 세팅합니다.
◽ 이를 표현하기 위해서 홀더를 RecyclerView.Adapter의 제네릭 타입으로 설정해줄 것입니다.
◽ 홀더를 제네릭 타입으로 설정하기 위해서는 홀더 클래스의 껍데기를 먼저 만들어야 합니다.
◽ 이때 홀더 클래스는 어댑터 클래스 밖에서 사용할 일이 없기 때문에 중첩 클래스(nested class)로 어댑터 클래스 내부에 작성합니다.
2. Holder 클래스 선언하기
💬
◽ 홀더의 생성자로 홀더가 사용하는 뷰를 넘겨줘야 합니다.
◽ 이때, 뷰 바인딩을 사용하기 때문에 item_recycler.xml layout에 대한 뷰 바인딩 ItemRecyclerBinding을 넘겨줍니다. 그리고 이를 상속한 RecyclerView.ViewHolder에게 전달해줍니다.
◽ 이제 홀더 껍데기가 만들어졌으므로, 상속받는 RecyclerView.Adapter의 제네릭 타입에 홀더을 입력해줍니다.
3. CustomAdapter 오버라이드 메서드 3개 구현하기
💬
◽ 이후에, [ ctrl ] + [ I ] 로 오버라이딩 할 메서드 3개를 불러옵니다.
◽ 이제 어댑터에 있던 빨간색 줄이 사라졌습니다.
4. 사용할 데이터를 생성자로 전달받기
💬
◽ 이제, 어댑터에서 사용할 데이터 목록을 줘야 하는데, 이는 가상의 데이터를 만들어서 어댑터를 생성할 때 넘겨주도록 primary constructor에 넣어서 만듭니다.
◽ 또한, 변수명 앞에 val 키워드를 붙여서 어댑터 클래스 안에서 모두 사용할 수 있도록 만든다.
5. getItemCount 메서드 구현하기
// override fun getItemCount(): Int {
// return listData.size
// }
override fun getItemCount() = listData.size
◽ 전체 아이템 뷰의 개수를 불러오는 getItemCount 메서드를 불필요한 코드를 축약해서 구현합니다.
6. onCreateViewHolder 메서드 구현하기
override fun onCreateViewHolder(parent: viewGroup, viewType: Int): Holder {
val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return Holder(binding)
}
◽ 화면에 보이는 아이템 레이아웃을 하나하나 생성하는 역할을 하는 메서드입니다.
◽ 하지만 만약 아이템 뷰가 100개 있다고 해서 목록 100개를 모두 생성하지는 않고, 화면에 보이는 개수만 생성합니다.
◽ 스크롤을 내리면 화면에 보이지 않던 홀더는 화면에 보였다가 사라지는 홀더를 재사용합니다.
💬
◽ 이제 바인딩을 해줄 차례입니다.
◽ 화면 전체를 띄우는 액티비티는 layoutInflater만 넘겨주는 것과는 다르게,
◽ 어댑터의 홀더는 목록의 일부에만 사용하며 아이템 뷰의 정보도 함께 넘겨줘야 하기 때문에 inflate 메서드의 파라미터를 몇 개 더 추가합니다.
◽ CustomAdapter 클래스에는 context가 없기 때문에 액티비티의 context에 있는 layoutInflater를 사용합니다.
◽ 해당 메서드의 파라미터들은 같은 형식으로 다른 코드에서도 똑같이 사용됩니다.
◽ 반환값으로는 홀더를 넘겨줘야 하는데, 아래의 홀더 클래스의 생성자가 binding이므로 위의 변수 binding을 생성한 것입니다. 이렇게 함으로써 홀더 클래스가 생성되면서 안드로이드에 전달됩니다.
7. onBindViewHolder 메서드 구현하기
override fun onBindViewHolder(holder: Holder, position: Int) {
// 1. 사용할 데이터를 꺼내고,
val memo = listData.get(position)
// 2. 홀더에 데이터를 전달
holder.setMemo(memo)
}
◽ 화면에 아이템 뷰들을 뿌려줄 때, 목록의 개수만큼 onBindViewHolder가 호출되면서 목록 아이템의 위치(position)을 전달해줍니다.
◽ 해당 메서드의 구현 방법은 우선, 사용할 데이터를 아이템 뷰의 위치(position)에 맞춰서 불러옵니다.
◽ 이후, 홀더에 데이터를 전달합니다. 이를 전달받은 홀더에서는 받은 데이터를 화면에 출력하는 순서로 이루어집니다.
class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root) {
// 3. 받은 데이터를 화면에 출력한다.
fun setMemo(memo: Memo) {
with (binding) {
// binding.textNo.text = "${memo.no}" -> with을 사용해서 코드 최적화
textNo.text = "${memo.no}"
textTitle.text = memo.title
val sdf = SimpleDateFormat("yyyy-MM-dd") // 날짜 형식 맞춰주기
// 숫자로 출력되는 timestamp를 날짜 형식에 맞춰서 문자열로 바꿔줍니다.
val formatDate = sdf.format(memo.timestamp)
textDate.text = formatDate
}
}
}
◽ 이때, binding을 통해서 뷰에 접근해야 하므로 클래스 내에서 전역변수로 사용하기 위해서 파라미터에 val 키워드를 추가합니다.
8. MainActivity에서 리사이클러뷰에 어댑터 연결해주기
class MainActivity : AppCompatActivity() {
val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}
override fin onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// 1. 데이터를 불러옵니다.
val data = loadData()
// 2. 어댑터를 생성합니다.
val customAdapter = CustomAdapter(data)
// 3. 화면의 RecyclerView와 연결합니다.
binding.recyclerView.adapter = customAdapter
// 4. 레이아웃 매니저를 설정합니다.
binding.recyclerView.layoutManager = LinearLayoutManager(this)
}
// 코드 생략
}
◽ 코드에 적혀있는 순서대로 리사이클러뷰를 띄워줄 액티비티에서 리사이클러뷰에 데이터 클래스의 데이터를 목록 순서대로 출력해주기 위해서 어댑터를 연결해줍니다.
◽ 이때 RecyclerView에 adapter라는 속성이 있기 때문에 이를 통해 어댑터를 설정(연결)해줍니다.
9. 아이템 뷰 클릭 이벤트 리스너 구현하기
class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root) {
lateinit var currentMemo: Memo
init {
binding.root.setOnClicklistener {
// val title = binding.textTitle.text
Toast.makeText(binding.root.context, "클릭된 아이템 : ${currentMemo.title}", Toast.LENGTH_SHORT).show
}
}
fun setMemo(memo: Memo) {
currentMemo = memo
with (binding) {
textNo.text = "${currentMemo.no}"
textTitle.text = currentMemo.title
val sdf = SimpleDateFormat("yyyy-MM-dd") // 날짜 형식 맞춰주기
// 숫자로 출력되는 timestamp를 날짜 형식에 맞춰서 문자열로 바꿔줍니다.
val formatDate = sdf.format(memo.timestamp)
textDate.text = formatDate
}
}
}
◽ 클릭 리스너는 주로 홀더에 작성합니다.
◽ 여기서 주의할 것은 클릭 처리는 init에서만 한다는 것입니다. 왜 그럴까요?
◽ 만약 setMemo() 안에서 리스터를 호출하면 드래그해서 새로운 아이템 뷰가 나올 때마다 계속 리스너가 호출되어 버립니다. 그렇게 되면 심각한 자원낭비이며, 앱이 죽을 수도 있기 때문입니다.
💬
◽ 리스너가 binding.root에 설정되었는데, 이는 홀더가 관리하는 하나의 큰 아이템 뷰 전체를 의미하기 때문입니다.
◽ 즉, binding.root가 아닌 binding.textNo 등으로 설정될 경우에는 textNo라는 id를 가진 텍스트를 클릭했을 때만 이벤트가 발생하게 되는 것이죠.
◽ 따라서, binding.root로 설정하게 되면 위에서 설정한 3개의 텍스트가 모두 담겨있는 전체 아이템 뷰를 클릭했을 때의 이벤트 처리를 위한 것입니다.
💬
◽ CustomAdapter와 비슷하게 Holder 클래스도 context가 존재하지 않습니다.
◽ 이때 Toast.makeText 메서드의 첫 번째 파라미터인 context를 사용하기 위한 방법으로는 Toast 메세지가 출력될 액티비티와 연결된 binding(layout)에서 꺼내서 사용할 수도 있습니다.
💬
◽ 위의 Holder 클래스와 다른 점이 또 하나 있습니다.
◽ Memo 타입의 currentMemo를 선언하고 onBindViewHolder에서 받은 Memo를 넣어두었습니다.
◽ 이는 나중에 Memo 데이터(TextView 등)가 많아지고, 데이터 중 몇 개는 화면에 뿌려주지 않을 경우에도 변수를 사용하기 위한 방법입니다.
'Android > Concept' 카테고리의 다른 글
[Android] [Kotlin] SharedPreferences (0) | 2021.09.19 |
---|---|
[Android] [Kotlin] SQLite (0) | 2021.09.14 |
[Android] [Kotlin] ViewPager (뷰페이저) (0) | 2021.09.13 |
[Android] [Kotlin] 인텐트 (0) | 2021.09.12 |
[Android] [Kotlin] 생명주기 (0) | 2021.09.12 |