728x90

viewModel 기반에서 RecyclerView 처리하는 예제를 작성해보고 있는데 생각만큼 쉽지 않다.

viewBinding 은 기본으로 사용해야 하는 것 같아 viewBinding 처리했고 viewModel 부분은 많은 연습을 해야 사용법이 익숙해질 듯 하다.

 

 
import com.link2me.android.recyclerviewmodel.adapter.ContactsListAdapter
import com.link2me.android.recyclerviewmodel.databinding.ActivityMainBinding
import com.link2me.android.recyclerviewmodel.model.ContactData
import com.link2me.android.recyclerviewmodel.model.ContactDataDto
import com.link2me.android.recyclerviewmodel.retrofit.RetrofitService
import com.link2me.android.recyclerviewmodel.viewmodel.MainViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
 
class MainActivity : AppCompatActivity() {
    private val TAG = this.javaClass.simpleName
 
    lateinit var mContext: Context
    private lateinit var mAdapter: ContactsListAdapter // 리스트뷰에 사용되는 ListViewAdapter
 
    private val addressItemList = mutableListOf<ContactData>() // 서버에서 가져온 원본 데이터 리스트
    private val searchItemList = mutableListOf<ContactData>() // 서버에서 가져온 원본 데이터 리스트
 
    private lateinit var backPressHandler: BackPressHandler
 
    val binding by lazy { ActivityMainBinding.inflate(layoutInflater)  }
 
    lateinit var viewModel: MainViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        //setContentView(R.layout.activity_main)
        mContext = this@MainActivity
 
        Log.e(TAG, "onCreate")
 
        backPressHandler = BackPressHandler(this// 뒤로 가기 버튼 이벤트
 
        // 뷰모델 가져오기
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
 
        // 관찰하여 데이터 값이 변경되면 호출
        viewModel.getContactsData().observe(this, listUpdateObserver)
    }
 
    var listUpdateObserver: Observer<List<ContactData>> =
        Observer {
            mAdapter = ContactsListAdapter(mContext) // Adapter 생성
            binding.myRecyclerView.adapter = mAdapter // 어댑터를 리스트뷰에 세팅
            binding.myRecyclerView.layoutManager = LinearLayoutManager(this)
            mAdapter.submitList(it)
 
            addressItemList.clear()
            addressItemList.addAll(it)
 
            binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String): Boolean {
                    // 문자열 입력을 완료했을 때 문자열 반환
                    return false
                }
 
                override fun onQueryTextChange(newText: String): Boolean {
                    // 문자열이 변할 때마다 바로바로 문자열 반환
                    filter(newText)
                    return false
                }
            })
 
            mAdapter.setItemClickListener(object : ContactsListAdapter.OnItemClickListener {
                override fun onClick(v: View, position: Int) {
                    val contacts = addressItemList[position]
                    Toast.makeText(mContext, "클릭한 이름은 ${contacts.userNM} 입니다.", Toast.LENGTH_SHORT)
                        .show()
                }
            })
 
        }
 
    fun filter(charText: String) {
        // ListAdapter 에서 처리하는 방법을 알게되면 수정할 예정
        var charText = charText
        charText = charText.toLowerCase(Locale.getDefault())
        searchItemList.clear()
        if (charText.length == 0) {
            mAdapter.submitList(addressItemList)
        } else {
            for (wp in addressItemList) {
                if (Utils.isNumber(charText)) { // 숫자여부 체크
                    if (wp.mobileNO.contains(charText) || wp.officeNO.contains(charText)) {
                        // 휴대폰번호 또는 사무실번호에 숫자가 포함되어 있으면
                        searchItemList.add(wp)
                        mAdapter.submitList(searchItemList)
                    }
                } else {
                    val iniName = HangulUtils.getHangulInitialSound(wp.userNM, charText)
                    if (iniName!!.indexOf(charText) >= 0) { // 초성검색어가 있으면 해당 데이터 리스트에 추가
                        searchItemList.add(wp)
                        mAdapter.submitList(searchItemList)
                    } else if (wp.userNM.toLowerCase(Locale.getDefault()).contains(charText)) {
                        searchItemList.add(wp)
                        mAdapter.submitList(searchItemList)
                    }
                }
            }
        }
        mAdapter.notifyDataSetChanged()  // 이거 없애면 어플 비정상 종료 처리됨
    }
 
    override fun onBackPressed() {
        backPressHandler.onBackPressed()
    }
}

단순하게 데이터를 서버에서 받아서 RecyclerView 에 출력하는 것은 어렵지 않다.

검색 기능을 추가하려니까 기존 사용하던 방식의 RecyclerView 와 약간 다른 부분을 처리하기 위해 ArrayList 를 여러개 생성해서 하는 것이 비효율적인 듯 싶다.

 

package com.link2me.android.recyclerviewmodel.viewmodel
 
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.link2me.android.recyclerviewmodel.model.ContactData
import com.link2me.android.recyclerviewmodel.model.ContactDataDto
import com.link2me.android.recyclerviewmodel.retrofit.RetrofitService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
 
// 데이터의 변경사항을 알려주는 라이브 데이터를 가지는 뷰모델
class MainViewModel : ViewModel() {
    private val TAG = this.javaClass.simpleName
 
    // 뮤터블 라이브 데이터 - 수정 가능, 라이브 데이터 - 값 변경 안됨
    private val liveData: MutableLiveData<List<ContactData>> by lazy {
        MutableLiveData<List<ContactData>>().also {
            loadContacts()
        }
    }
 
    fun getContactsData(): LiveData<List<ContactData>> {
        return liveData
    }
 
    // LiveData는 관찰 가능한 데이터들의 홀더 클래스
 
    private fun loadContacts(){
        RetrofitService.getInstance()?.getContactDataResult("1")?.enqueue(object :
            Callback<ContactDataDto> {
            override fun onResponse(call: Call<ContactDataDto>, response: Response<ContactDataDto>) {
                response.body()?.let {
                    if(it.status.contains("success")){
                        liveData.value = it.message
                    }
                }
            }
 
            override fun onFailure(call: Call<ContactDataDto>, t: Throwable) {
                Log.d(TAG,"Retrofit response fail.")
                t.stackTrace
            }
 
        })
    }
 
    fun searchNameChanged(name: String) {
 
    }
 
}
 
 

 

 

Adapter 코드는 포멧만 한번 만들어두면 몇가지만 코드만 수정하면 되기 때문에 재활용하는 것은 쉬운 편이다.

package com.link2me.android.recyclerviewmodel.adapter
 
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.telephony.PhoneNumberUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.link2me.android.recyclerviewmodel.databinding.ItemAddressBinding
import com.link2me.android.recyclerviewmodel.model.ContactData
import com.link2me.android.recyclerviewmodel.retrofit.RetrofitURL
import java.util.*
 
class ContactsListAdapter(val context: Context) : ListAdapter<ContactData, ContactsListAdapter.ViewHolder>(diffUtil) {
 
    // itemClickListener 를 위한 인터페이스 정의
    interface OnItemClickListener {
        fun onClick(v: View, position: Int)
    }
    private lateinit var itemClickListener: OnItemClickListener
 
    fun  setItemClickListener(itemClickListener: OnItemClickListener){
        this.itemClickListener = itemClickListener
    }
    //////////////////////////////////////////////////////////////////
 
    // val 예약어로 바인딩을 전달 받아서 전역으로 사용힌다. 그리고 상속받는 ViewHolder 생성자에는 꼭 binding.root를 전달해야 한다.
    inner class ViewHolder(private val binding: ItemAddressBinding) : RecyclerView.ViewHolder(binding.root){
        // View와 데이터를 연결시키는 함수
        fun bind(item: ContactData){
            //binding.executePendingBindings() //데이터가 수정되면 즉각 바인딩
 
            if (item.photo.contains("jpg")){
                val photoURL = RetrofitURL.Photo_URL + item.photo
                Glide.with(context).load(photoURL).into(binding.profileImage)
            }
 
            // 생성자에서 val로 받았기 때문에 홀더 내부 어디에서나 binding 사용가능
            binding.itemName.text = item.userNM
            binding.itemMobileNO.text = item.mobileNO
 
            val items = arrayOf("휴대폰 전화걸기""연락처 저장")
            val builder = AlertDialog.Builder(context)
            builder.setTitle("해당작업을 선택하세요")
            builder.setItems(items, DialogInterface.OnClickListener { dialog, which ->
                Toast.makeText(context, items[which] + "선택했습니다.", Toast.LENGTH_SHORT).show()
                when (which) {
                    0 -> {
                        if (item.mobileNO.length == 0) {
                            Toast.makeText(context, "전화걸 휴대폰 번호가 없습니다.", Toast.LENGTH_SHORT).show()
                            return@OnClickListener
                        }
                        val dialog1 = AlertDialog.Builder(context)
                            .setTitle(item.userNM)
                            .setMessage(PhoneNumberUtils.formatNumber(item.mobileNO) + " 통화하시겠습니까?")
                            .setPositiveButton("예",
                                { dialog, which ->
                                    val intent = Intent(Intent.ACTION_CALL,
                                        Uri.parse("tel:" + PhoneNumberUtils.formatNumber(item.mobileNO))
                                    )
                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                    context.startActivity(intent)
                                })
                            .setNegativeButton("아니오") { dialog, which -> dialog.dismiss() }.create()
                        dialog1.show()
                    }
                    1 -> Toast.makeText(context, "전화번호 저장하는 로직은 직접 구현하시기 바랍니다.", Toast.LENGTH_SHORT).show()
                }
            })
            builder.create()
            binding.itemBtn.setOnClickListener { builder.show() }
        }
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        //val rootView = LayoutInflater.from(parent.context).inflate(R.layout.item_address, parent,false)
        return ViewHolder(ItemAddressBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }
 
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출한다.
 
        holder.itemView.setOnClickListener {
            itemClickListener.onClick(it,position)
        }
 
        holder.apply {
            bind(currentList[position])
        }
    }
 
    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<ContactData>() {
            override fun areContentsTheSame(oldItem: ContactData, newItem: ContactData) =
                oldItem == newItem
 
            override fun areItemsTheSame(oldItem: ContactData, newItem: ContactData) =
                oldItem.idx == newItem.idx
        }
    }
 
}
 

 

전체 소스코드는 https://github.com/jsk005/KotlinProjects/tree/master/recyclerviewmodel_1 에 올려두었다.

관련 기능을 하나 하나 테스트하면서 익히고 기록해 두려고 한다.

 

 

 

블로그 이미지

Link2Me

,