본문 바로가기
Android/Android 핵심기술

[Android/Kotlin] 멀티뷰타입 리사이클러뷰 MVVM패턴으로 수정(Observer Pattern 사용)

by juwon2 2024. 4. 29.

 

 

 

 

 

https://coding-juuwon2.tistory.com/287

 

[Android/Kotlin] Multi View Type 리사이클러뷰 구현하기

일반적인 리사이클러뷰는 하나의 뷰형태만 보여주고 데이터만 달라지지만,리사이클러뷰 멀티뷰타입을 사용하면 다수의(다른) 뷰형태를 가지는 아이템을 보여줄 수 있다 이런식으로 멀티뷰타

coding-juuwon2.tistory.com

 
이때 만들었던 멀티뷰타입 리사이클러뷰를 MVVM패턴으로 수정해볼것이다
근데 아직 LiveData를 쓰지는 않고, Observer Pattern을 사용해서 수정해볼것이다
 
 
 
 

# 상수정의

// enum class로 열거형 클래스로 만들기 (코드 단순, 가독성 up)
enum class MultiViewEnum(val viewType : Int) {

    BlUE(0),
    LIGHTBLUE(1),
    ORANGE(2)
}

CardData.kt

 

# 데이터

@Parcelize
data class CardData(
    val id : Int,
    val name : String,       //이름
    val cardName : String,   //카드이름
    val number : String,     //카드번호
    val date : String,       //유효기간
    val price : Double,      //가격

    val type : MultiViewEnum   // 멀티뷰타입 -> enum class로 만든 클래스 선언
): Parcelable

CardData.kt

// 싱글톤
class DataSource {
    companion object{
        private var INSTANCE : DataSource? = null

        fun getDataSoures() : DataSource{
            // DatoSource::class 객체에 lock을 걸어 한번에 한 스레드에서만 실행 되도록 함
            return synchronized(DataSource::class) {
                // 싱글톤 객체를 한번 호출하고 없으면 DataSource반환, 있으면 생성된 인스턴스 반환
                val newInstance = INSTANCE ?: DataSource()
                INSTANCE = newInstance
                newInstance
            }
        }
    }

    // MVVM패턴에서 Model에 해당한다고 볼 수 있음
    fun getCardList() : List<CardData>{
        // 만들어놓은 데이터클래스 리턴
        return CardDataList()
    }




}

DataSource.kt

fun CardDataList() : ArrayList<CardData>{

    return arrayListOf(
        CardData(
            id = 1,
            name = "Juwon",
            cardName = "A Debit Card",
            number = "2423 3581 9503",
            date = "21/27",
            price = 3100.30,
            MultiViewEnum.BlUE
        ),
        CardData(
            id = 2,
            name = "Minju",
            cardName = "A Hybrid Card",
            number = "5423 3581 9503",
            date = "07/25",
            price = 4100.30,
            MultiViewEnum.LIGHTBLUE
        ),
        CardData(
            id = 3,
            name = "Hemin",
            cardName = "A Hi Card",
            number = "9423 3581 9503",
            date = "23/29",
            price = 4170.30,
            MultiViewEnum.ORANGE
        ),
    )

}

CardDataList.kt
 

 
 

# Adapter

class CardViewAdpater(var cardList : List<CardData>, private val onClick : (CardData) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {


    // 레이아웃 연결
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)

        return when(viewType){

            MultiViewEnum.BlUE.viewType -> {
                val binding = RecyclerviewItem1Binding.inflate(inflater, parent, false)
                BlueTypeViewHolder(binding)
            }
            MultiViewEnum.LIGHTBLUE.viewType -> {
                val binding = RecyclerviewItem2Binding.inflate(inflater, parent, false)
                LightBlueTypeViewHolder(binding)
            }
            MultiViewEnum.ORANGE.viewType -> {
                val binding = RecyclerviewItem3Binding.inflate(inflater, parent, false)
                OrangeTypeViewHolder(binding)
            }
            else -> throw IllegalAccessException("Invalid view type")
        }
    }

    // 아이템 개수 리턴
    override fun getItemCount(): Int {
        return cardList.size
    }


    // 멀티뷰타입은 getItemViewType을 오버라이딩 해줘야함
    // postion에 따라 어떤 뷰타입을 가져야되는지 연결해줘야함
    override fun getItemViewType(position: Int): Int {
        return when(position){
            0 -> MultiViewEnum.BlUE.viewType
            1 -> MultiViewEnum.LIGHTBLUE.viewType
            2 -> MultiViewEnum.ORANGE.viewType
            else -> throw IllegalAccessException("Invalid view type")
        }
    }


    // 데이터 연결
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val cardlist = cardList[position]

        when(holder.itemViewType){

            MultiViewEnum.BlUE.viewType -> {
                val blueHolder = holder as BlueTypeViewHolder
                blueHolder.bind(cardlist)

                // MULTI_VIEWTYPE1 클릭 이벤트 처리 (멀티뷰타입마다 데이터가 완전 다른 경우도 있기 때뮨에 그런경우에는 이렇게 클릭이벤트를 주는게 유용함)
                holder.itemView.setOnClickListener {
                    onClick(cardlist)
                }
            }

            MultiViewEnum.LIGHTBLUE.viewType -> {
                val lightBlueHolder = holder as LightBlueTypeViewHolder
                lightBlueHolder.bind(cardlist)

                // MULTI_VIEWTYPE2 클릭 이벤트 처리
                holder.itemView.setOnClickListener {
                    onClick(cardlist)
                }
            }

            MultiViewEnum.ORANGE.viewType -> {
                val orangeHolder = holder as OrangeTypeViewHolder
                orangeHolder.bind(cardlist)

                // MULTI_VIEWTYPE3 클릭 이벤트 처리
                holder.itemView.setOnClickListener {
                    onClick(cardlist)
                }
            }

        }


    }


    class BlueTypeViewHolder(private val binding : RecyclerviewItem1Binding) : RecyclerView.ViewHolder(binding.root){

        fun bind(card : CardData){
            with(binding){
                AndersonTv.text = card.name
                debitCardTv.text = card.cardName
                cardNumberTv.text = card.number
                cardDateTv.text = card.date
                cardPriceTv.text = "$" + DecimalFormat("#,##0.00").format(card.price).toString()
            }
        }
    }


    class LightBlueTypeViewHolder(private val binding : RecyclerviewItem2Binding) : RecyclerView.ViewHolder(binding.root){

        fun bind(card : CardData){
            with(binding){
                AndersonTv2.text = card.name
                debitCardTv2.text = card.cardName
                cardNumberTv2.text = card.number
                cardDateTv2.text = card.date
                cardPriceTv2.text = "$" + DecimalFormat("#,##0.00").format(card.price).toString()
            }
        }
    }


    class OrangeTypeViewHolder(private val binding : RecyclerviewItem3Binding) : RecyclerView.ViewHolder(binding.root){

        fun bind(card : CardData){
            with(binding){
                AndersonTv3.text = card.name
                debitCardTv3.text = card.cardName
                cardNumberTv3.text = card.number
                cardDateTv3.text = card.date
                cardPriceTv3.text = "$" + DecimalFormat("#,##0.00").format(card.price).toString()   // 확장함숧 뽑아보기
            }
        }
    }

}

CardViewAdpater.kt
 

 
 

# ViewModel

위에까지는 저번에 작성했던것과 완전히 똑같다
저번 코드와 가장 큰 차이가 ViewModel을 사용했다는 것인데, 원래는 DataSoure로 만들어서 바로 가져왔다면 이걸 ViewModel로 만들어서 적용해준다. 
 
일단 viewModel 사용하려면 build.gradle에 의존성 등록해줘야한다

// viewModel 추가!!
implementation("androidx.activity:activity-ktx:1.8.2")

build.gradle

 

class CardViewModel(dataSource: DataSource) : ViewModel() {

    // data list observing을 사용하는 방법! (LiveData 사용안하고)

    // dataSource.getCardList() 가져오기
    val cardData = dataSource.getCardList()

    fun getCardModel(cardData: CardData) : CardData{
        return cardData
    }

}


class CardViewModelFactory : ViewModelProvider.Factory{

    // 어떤 타입의 뷰모델이 와도 가능하게끔 (데이터타입도 어떤 데이터타입이든지 가능하도록)
    override fun <T : ViewModel> create(modelClass: Class<T>): T {

        // modelClass(CardViewModel)가 유효한지 검사 (값이 있는지 체크)
        // 즉, CardViewModel::class.java가 modelClass와 동일한 클래스인지를 검사
        if (modelClass.isAssignableFrom(CardViewModel::class.java)){
            // 값이 있다면, CardViewModel객체를 생성 / 이때, CardViewModel은 dataSource를 인자로 받아 초기화된다
            // CardViewModel을 제네릭T타입으로 바꿔주겠다
            return CardViewModel(dataSource = DataSource.getDataSoures()) as T
        }

        throw IllegalAccessException("Unknown ViewModel Class")
    }
}

CardViewModel.Kt
 

 
 

# MainActivity

MainActivity에서 아까 만들어준 ViewModel을 선언해서 가져와주고, 
원래 DataSource를 사용해서 가져왔던 부분을 ViewModel을 사용해서 가져오도록 수정해준다
그리고 Intent로 이동할때 Intent부분을 확장함수로 따로 빼서 사용할 수 있도록 수정해줬다

class MainActivity : AppCompatActivity() {

    private val binding : ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    // onClick메소드가 실행되면 람다식이 바로 실행되도록
    private val cardAdapter : CardViewAdpater by lazy {
        CardViewAdpater(cardList = ArrayList<CardData>()) { card ->
            // DetailActivity로 이동하는 함수 실행
            adapterClick(card)
        }
    }

    // ViewModel 선언
    private val cardViewModel by viewModels<CardViewModel>{
        CardViewModelFactory()
    }


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


        // DataSource를 통해 가져왔던부분을 ViewModel을 사용해서 받아와줌
        val cardLists = cardViewModel.cardData      // val dataSource = DataSource.getDataSoures().getCardList()
        cardAdapter.cardList = cardLists                          // cardAdapter.cardList = dataSource


        with(binding.recyclerview){
            adapter = cardAdapter   // 리사이클러뷰와 어뎁터 연결
            layoutManager = LinearLayoutManager(this@MainActivity)
        }

        binding.priceTv.text = "$ ${DecimalFormat("#,##0.00").format(285856.20)}"

    }


    private fun adapterClick(card : CardData){

        // Intent부분을 확장함수로 빼서 DetailActivity로 이동하도록 구현
        launchActivity<DetailActivity>(
            DetailActivity.EXTRA_CARD to card
        )


    }

}

MainActivtiy.kt

 
 

# Intent 확장함수로 빼기

Intent부분과 데이터 전달하는 부분까지 확장함수로 뽑아서 MainActivity에서 더 편리하게 사용할 수 있도록해주었다

// Intent부분 확장함수로 뽑기
inline fun <reified  T : Any> newIntent(context: Context) : Intent =
    // Intent(this@MainActivity, DetailActivity::class.java) 에 해당하는 부분
    Intent(context, T::class.java)


// 데이터 bundle로 전달하고, startActivity하는 부분도 확장함수로 뽑기
inline fun <reified T : Any> Context.launchActivity(

    // key값은 String , value값은 어떤타입이든 올수있도록 지정
    // vararg(가변인자)로 설정하면 인자를 여러개 넣을 수 있음
    vararg pair: Pair<String, Any>){
    val intent = newIntent<T>(this)
    intent.putExtras(bundleOf(*pair))
    startActivity(intent)
}

Intent.kt
 
 

# DetailActivity

DetailActivity에서도 ViewModel을 선언해서 가져와주고, 그 ViewModel을 사용해서 데이터를 받아와준다

class DetailActivity : AppCompatActivity() {

    private val binding: ActivityDetailBinding by lazy {
        ActivityDetailBinding.inflate(layoutInflater)
    }

    // 어디서나 이 키값을 사용할수있게끔 지정!
    companion object{
        const val EXTRA_CARD : String = "extra_card"
    }

    // ViewModel 선언
    private val cardViewModel by viewModels<CardViewModel>{
        CardViewModelFactory()
    }


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


        // viewModel 사용해서 데이터 받아옴
        val cardItem = intent?.getParcelableExtra<CardData>(EXTRA_CARD)     //데이터 받아오기
        val cardData = cardItem?.let { cardViewModel.getCardModel(it) }

        with(binding) {
            detailCardnameTv.text = "이름: ${cardData?.name}"
            detailCardnunberTv.text = "카드번호: ${cardData?.number}"
            detailDateTv.text = "유효기간: ${cardData?.date}"
            detailPriceTv.text = "가격: $${DecimalFormat("#,##0.00").format(cardData?.price)}"
        }
    }
}

DetailActivity.kt