본 글에는 공개되어있는 RecyclerView git 코드를 보면서 이해하고 공부한 내용을 정리한다.
package com.example.recyclersample_practice.flowerList
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.recyclersample_practice.data.DataSource
import com.example.recyclersample_practice.data.Flower
import kotlin.random.Random
class FlowersListViewModel(val dataSource: DataSource) : ViewModel() {
val flowerLiveData = dataSource.getFlowerList() //TODO: livaData
// name, description이 제대로 들어오면 새로운 flower를 만들어 데이터소스에 추가함
fun insertFlower(flowerName: String?, flowerDescription: String?) {
if (flowerName == null || flowerDescription == null) {
return
}
val image = dataSource.getRandomFlowerImageAsset()
val newFlower = Flower(
Random.nextLong(),
flowerName,
image,
flowerDescription
)
dataSource.addFlower(newFlower)
}
}
class FlowerListViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(FlowersListViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return FlowersListViewModel(
dataSource = DataSource.getDataSource(context.resources)
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
이런식으로 뷰모델과 팩토리 클래스를 작성할 수 있다.
팩토리에서는 FlowersListViewModel의 생성자 인자로 dataSource를 받아와 처리하게끔 커스텀된 것을 볼 수 있다.
팩토리 클래스가 좀 이해가 안돼서 더 살펴봤는데, 여전히 이해는 못 했다.
override fun <T : ViewModel> create(modelClass: Class<T>): T
if (modelClass.isAssignableFrom(FlowersListViewModel::class.java))
결론적으로 이 팩토리는,
create 함수가 매개변수로 modelCalss를 받아오고,
요청된 ViewModel 타입이 FlowersListViewModel일 경우에만 객체를 생성하여 반환한다.
이때 T 타입을 사용하는 이유는 ViewModel의 하위 클래스들을 받아와서 처리를 해줘야하므로 보다 유연한 처리를 위해 사용하는 것이다.
private val flowersLiveData = MutableLiveData(initialFlowerList)
DataSource에 선언된 LiveData는 변경이 될 수 있으므로 MutableLiveData를 사용한다.
val flowerLiveData = dataSource.getFlowerList()
ViewModel에 선언된 LiveData는 그저 소스에서 가져온 데이터일 뿐이다.
둘 다 LiveData의 메서드이며, 데이터를 읽고 업데이트하는 데에 사용된다.
//새로운 꽃을 liveData와 post value를 추가함
fun addFlower(flower: Flower) {
val currentList = flowersLiveData.value // 이 부분
if (currentList == null) {
flowersLiveData.postValue(listOf(flower))
} else {
val updatedList = currentList.toMutableList()
updatedList.add(0, flower)
flowersLiveData.postValue(updatedList) // 이 부분
}
}
이 코드를 보면 currentLis
에 LiveData를 읽어오고,
flowersLiveData.postValue(updatedList)
에서 데이터를 변경하고 있다.
데이터를 관리하는 클래스와 ViewModel에서 LiveData가 선언되고 뭔가 처리를 하는 데 사용할 수 있게끔 작성이 되었으면, 이후에는 사용할 액티비티나 프래그먼트에서 .observe 하여 사용한다.
flowersListViewModel.flowerLiveData.observe(this, {
it?.let {
flowerAdapter.submitList(it as MutableList<Flower>)
headerAdapter.updateFlowerCount(it.size)
}
})
이 코드는 flowerLiveData를 observe하여 데이터의 변화를 감지한다.
observe는 이렇게 생겼는데,
이렇게 매개변수를 받고 있다.
다시 코드를 살펴보면,
it?.let {...}
flowerAdapter.submitList(it as MutableList<Flower>)
headerAdapter.updateFlowerCount(it.size)
'RecyclerView의 데이터를 관리를 효율적으로 도와주는 클래스' 라고 한다.
특히, DiffUtil
라는 유틸리티 클래스를 사용하여 새로운 목록과 이전 목록을 비교하여 변경된 부분만 업데이트 할 수 있다.
class FlowersAdapter(private val onClick: (Flower) -> Unit) :
ListAdapter<Flower, FlowersAdapter.FlowerViewHolder>(FlowerDiffCallback) {
이런식으로 adapter 클래스를 작성할 때 ListAdapter를 상속 받는다.
ListAdapter는 <> 기호를 사용하여 정의할 때 타입을 파라미터화 할 수 있다고 하는데,
코드에서는,
첫 번째 인자로 Flower라는 데이터 클래스를
두 번째 인자로 FlowersAdapter.FlowerViewHolder 뷰홀더 클래스를 넣어줬다.
(FlowerDiffCallback)
이 부분은 ListAdapter의 생성자에 전달되는 매개변수로 사용되는데, 코드는 아래와 같다.
object FlowerDiffCallback : DiffUtil.ItemCallback<Flower>() {
override fun areItemsTheSame(oldItem: Flower, newItem: Flower): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Flower, newItem: Flower): Boolean {
return oldItem.id == newItem.id
}
}
이 코드는 DiffUtil.ItemCallback<Flower>
인터페이스를 구현한 객체이다.
이 객체는 ListAdapter가 데이터 변경을 비교하고 업데이트 하는 데에 사용된다.
딸려온 areItemsTheSame, areContentsTheSame 함수를 override하여 사용하면 된다.
FlowerAdapter의 ViewHolder 안에는 이런 코드가 있다.
class FlowerViewHolder(
binding: FlowerItemBinding, onClick: (Flower) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
private var flowerTextView = binding.flowerText
private var flowerImageView = binding.flowerImage
private var currentFlower: Flower? = null
init {
binding.root.setOnClickListener {
currentFlower?.let {
onClick(it)
}
}
}
//이하 생략
이 부분을 보면서 init 블록의 역할이 궁금했다. 왜 저기에서 init을 해줘야 하지?
init은 클래스의 초기화를 실행하는 블록이다.
즉, 클래스 인스턴스가 생성될 때 init 안에 있는 내용으로 초기화가 된다.
위 코드에서는 init 안에서 클릭 이벤트 처리를 등록하고 이벤트 핸들러에 대한 설정을 한다.
이렇게 init 블록을 사용하면 클래스 초기화 과정을 명시적으로 정의할 수 있다고 한다. 특히 코드 가독성에 좋고 유지 보수할 때에도 init 블록을 따로 보면서 수정 작업을 할 수도 있겠다.
위에서 다뤘던 flowerAdapter와 headerAdapter는 액티비티에서 아래와 같이 사용된다.
val headerAdapter = HeaderAdapter()
val flowerAdapter = FlowersAdapter { flower -> adapterOnClick(flower) }
val concatAdapter = ConcatAdapter(headerAdapter, flowerAdapter)
val recyclerView: RecyclerView = binding.recyclerView
recyclerView.adapter = concatAdapter
ConcatAdapter는 여러 개의 어댑터를 결합하여 하나의 큰 어댑터로 만드는 역할을 한다.
먼저 표시할 아이템을 매개변수 자리에 넣어주고, 보여줄 view의 adapter로 연결해주면 된다.
//FAB 버튼 클릭 시 꽃 추가
private fun fabOnClick() {
val intent = Intent(this, AddFlowerActivity::class.java)
startActivityForResult(intent, newFlowerActivityRequestCode)
}
이런식으로 인텐트와 코드를 함께 넘긴다.
그 전에는 startActivity만 사용해봐서 좀 낯설었는데, 역할은 비슷한데 +@된 기능이 있다.
startActivityForResult는 intent를 보낼 액비비티와 상호작용이 필요할 때 사용한다.
즉, 호출된 액티비티가 시작된 후 작업을 완료한 다음 결과를 호출한 액비티비로 돌려보내는 것이다.
호출된 액비비티에서는 intent로 데이터를 받아 처리를 하는데,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityAddFlowerBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.doneButton.setOnClickListener {
addFlower()
}
addFlowerName = binding.addFlowerName
addFlowerDescription = binding.addFlowerDescription
}
private fun addFlower() {
val resultIntent = Intent()
if (addFlowerName.text.isNullOrEmpty() || addFlowerDescription.text.isNullOrEmpty()) {
setResult(Activity.RESULT_CANCELED, resultIntent)
} else {
val name = addFlowerName.text.toString()
val description = addFlowerDescription.text.toString()
resultIntent.putExtra(FLOWER_NAME, name)
resultIntent.putExtra(FLOWER_DESCRIPTION, description)
setResult(Activity.RESULT_OK, resultIntent)
}
finish()
}
위 코드를 보면 addFlower() 안에서 setResult() 메서드를 사용하여 데이터를 호출한 액티비티로 반환한다.
위 코드를 통해 데이터를 다른 액티비티로 넘기고, 받아온 결과를 처리하기 위해 사용하는 함수이다.
이때 requestCode, resultCode를 사용하여 처리할 결과를 확인한다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//꽃 정보를 뷰모델에 전달
if (requestCode == newFlowerActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.let {
val flowerName = it.getStringExtra(FLOWER_NAME)
val flowerDescription = it.getStringExtra(FLOWER_DESCRIPTION)
flowersListViewModel.insertFlower(flowerName, flowerDescription)
}
}
}
코드를 보면,
resultCode == Activity.RESULT_OK
로 결과가 정상적으로 반환되었으면,
받아온 data에 대한 let 이하 처리를 한다.
여기서 let 안에서는 ViewModel로 데이터를 보내 insertFlower 처리를 요청한다.
fun insertFlower(flowerName: String?, flowerDescription: String?) {
if (flowerName == null || flowerDescription == null) {
return
}
val image = dataSource.getRandomFlowerImageAsset()
val newFlower = Flower(
Random.nextLong(),
flowerName,
image,
flowerDescription
)
dataSource.addFlower(newFlower)
}
insertFlower에서는 DataSource로 요청할 데이터를 다듬는다.
보면 마지막 줄에 addFlower(...) 처리를 dataSource에 요청하는데,
fun addFlower(flower: Flower) fun addFlower(flower: Flower) `{
val currentList = flowersLiveData.value
if (currentList == null) {
flowersLiveData.postValue(listOf(flower))
} else {
val updatedList = currentList.toMutableList()
updatedList.add(0, flower)
flowersLiveData.postValue(updatedList)
}
}
DataSource에서는 데이터를 받아와 실제적으로 DataSource 클래스에서 관리하는 데이터에 변화를 준다.
companion object {
private var INSTANCE: DataSource? = null
fun getDataSource(resources: Resources): DataSource {
return synchronized(DataSource::class) {
val newInstance = INSTANCE ?: DataSource(resources)
INSTANCE = newInstance
newInstance
}
}
}
synchronized(DataSource::class)
이부분에 멤버 참조 문법이 사용되었다.멤버 참조 문법을 써서 컴파일러가 안전하게 DataSource의 클래스를 처리할 수 있도록 한 것이다. 물론 코드를 더 간결하게 만드는 것도 있다.
만약 멤버 참조 문법을 쓰지 않는다면?
synchronized("DataSourceLock")
이런식으로 임의의 문자열을 넣을 수도 있다.
흠.. 뭔가 synchronized() 안에 문자열을 넣든 멤버 참조로 KClass를 넣든 상관은 없는 것 같다.
요점은 인스턴스를 한 번만 생성해두고 사용한다는 부분인 것 같다.
data class Flower (
val id: Long,
val name: String,
@DrawableRes // TODO: 이 애너테이션의 역할
val image: Int?,
val description: String
)
@DrawableRes
는 리소스 타입을 지정해주는 역할을 한다. 즉, imaga에 대한 리소스는 Drawable 타입이어야 한다.class FlowerListViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(FlowersListViewModel::class.java)) {
@Suppress("UNCHECKED_CAST") // TODO: suppress 애너테이션의 역할
return FlowersListViewModel(
dataSource = DataSource.getDataSource(context.resources)
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
@Suppress
는 컴파일러에게 특정 경고를 무시하라고 지시하는 것이다.as T
로 리턴하는 뷰모델을 제네릭 타입으로 캐스팅하고 있다.[TIL-240419]