리사이클러뷰에서 데이터바인딩과 라이브데이터를 사용하여
리스트의 변화를 감지하여 바로 리사이클러뷰를 갱신해주려고 한다.
[2022.04.20 추가]
2022.04.16 - [Android/AAC, MVVM] - [Android] 리사이클러뷰 BindingAdapter, ListAdapter 사용하여 데이터바인딩 하기
위 링크 글에서 더 최신 방법으로 다루었습니다. 위 글을 먼저 보시고 이해가지 않는 부분이 있다면
현재글을 참고해주세요 😀
buildFeatures {
dataBinding true
}
def lifecycle_version = "2.3.0"
// ViewModel - 라이프 사이클
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData - 데이터의 변경 사항을 알 수 있음
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
데이터바인딩과
뷰모델, 라이브데이터를 추가해준다.
2021.05.17 - [안드로이드/기본] - [Android] 자주쓰는 RecyclerView 사용하기 (+ ViewBinding)
2021.04.26 - [안드로이드/AAC, MVVM] - [Android] 안드로이드 ViewModel, LiveData (+DataBinding)
뷰모델과 라이브데이터, 리사이클러뷰는 이 글에서 다뤘으므로 간략히 적으려고 한다.
data class User(
val name: String,
val age: Int
)
리사이클러뷰의 아이템으로 사용할 데이터클래스를 만들어준다.
이런식으로 아이템뷰를 만들려고 하므로
<resources>
<string name="user_format">이름은 %s 나이는 %d</string>
</resources>
String Format를 만들어주고
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.hanyeop.recyclerviewdatabindingex.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="16dp"
android:text="@{user.name}"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/description_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(user.age)}"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/name_text"
app:layout_constraintTop_toBottomOf="@+id/name_text" />
<TextView
android:id="@+id/intro_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{@string/user_format(user.name, user.age)}"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/name_text"
app:layout_constraintTop_toBottomOf="@+id/name_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>
아이템뷰의 텍스트를 데이터클래스와 바인딩해서 작성해준다.
class MyAdapter()
: RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
var userList = mutableListOf<User>()
// 생성된 뷰 홀더에 값 지정
class MyViewHolder(val binding: MainItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(currentUser : User) {
binding.user = currentUser
}
}
// 어떤 xml 으로 뷰 홀더를 생성할지 지정
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = MainItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return MyViewHolder(binding)
}
// 뷰 홀더에 데이터 바인딩
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(userList[position])
}
// 뷰 홀더의 개수 리턴
override fun getItemCount(): Int {
return userList.size
}
}
리사이클러뷰 어댑터를 작성해주고
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
// private lateinit var mainViewModel: MainViewModel
private lateinit var myAdapter: MyAdapter
private val TAG = "test5"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
myAdapter = MyAdapter()
binding.recyclerView.adapter = myAdapter
myAdapter.userList = mutableListOf(
User("Han",25),
User("Lee",33)
)
binding.button.setOnClickListener {
myAdapter.userList.add(User("test",50))
Log.d(TAG, "onCreate: 실행")
}
}
}
메인에서 액티비티를 바인딩해주고,
어댑터를 연결하여준다.
버튼을 클릭하면 어댑터의 리스트에 임의의 값을 추가하도록 하였다.
화면에 각각 바인딩된 값이 잘 출력되는 것을 확인할 수 있다.
class MainViewModel : ViewModel() {
private val _userList = MutableLiveData<ArrayList<User>>()
val userList : LiveData<ArrayList<User>>
get() = _userList
private var items = ArrayList<User>()
init{
items = arrayListOf(
User("Han",25),
User("Lee",33)
)
_userList.value = items
}
fun buttonClick(){
val user = User("Test",20)
items.add(user)
_userList.value = items
}
}
userList를 가지는 뷰모델을 작성해준다.
여기서 아이템을 초기화해주며, 아이템 추가 기능도 이 곳으로 옮겼다.
<data>
<variable
name="viewModel"
type="com.hanyeop.recyclerviewdatabindingex.MainViewModel" />
</data>
데이터바인딩하여
뷰모델을 변수 추가 해주고,
android:onClick="@{() -> viewModel.buttonClick()}"
버튼에 뷰모델의 buttonClick을 바인딩해준다.
fun setData(data : ArrayList<User>){
userList = data
notifyDataSetChanged()
}
받아온 데이터 값을
유저리스트에 할당하여
뷰를 다시 갱신시키는 함수를 작성해주고
// 뷰모델 연결
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewModel = mainViewModel
mainViewModel.userList.observe(this, Observer {
myAdapter.setData(it)
})
만든 뷰모델을 연결해주고
옵저버를 만들어주어 리스트가 변경될 때 감지하여 setData를 호출해준다.
버튼을 클릭할 때 마다 잘 생성된다.
마지막으로, 바인딩 어댑터를 사용하면 setData 함수 호출 없이 리스트가 변동될 때 뷰를 갱신해 줄 수 있다.
plugins {
id 'kotlin-kapt'
}
어노테이션 사용을 위해 추가해준다.
object MyBindingAdapter{
@BindingAdapter("items")
@JvmStatic
fun setItems(recyclerView: RecyclerView, items : ArrayList<User>){
if(recyclerView.adapter == null)
recyclerView.adapter = MyAdapter()
val myAdapter = recyclerView.adapter as MyAdapter
myAdapter.userList = items
myAdapter.notifyDataSetChanged()
}
}
오브젝트로 바인딩 어댑터를 생성해주고
@BindingAdapter 어노테이션으로 함수를 작성해주면
커스텀한 속성을 xml에서 사용할 수 있다.
또한 @JvmStatic 어노테이션을 작성하여 스태틱으로 생성해준다.
여기서 어댑터도 연결해준다.
리사이클러뷰의 아이템을 설정하기 위한 속성을 정의하였다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:items="@{viewModel.userList}"/>
새로만든 items 속성을 사용하여
뷰모델의 유저리스트를 연결해준다.
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// 뷰모델 연결
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewModel = mainViewModel
// 뷰모델을 LifeCycle 에 종속시킴, LifeCycle 동안 옵저버 역할을 함
binding.lifecycleOwner = this
}
}
바인딩어댑터에서 어댑터를 연결했으므로 어댑터 연결하는 코드도 필요없고,
뷰모델을 연결하여 lifecycleOwner를 액티비티에 종속시키면,
옵저버 역할을 하여 라이브데이터와 동일한 값으로 뷰가 자동으로 갱신된다.
데이터 바인딩을 사용하여 코드가 굉장히 깔끔해진것을 확인할 수 있다.
허나 아이템을 바인딩하게되면
뷰가 notifyDataSetChanged 를 호출할때마다
깜빡이는 현상이 존재하게 되는데
object MyBindingAdapter{
@BindingAdapter("items")
@JvmStatic
fun setItems(recyclerView: RecyclerView, items : ArrayList<User>){
if(recyclerView.adapter == null) {
val adapter = MyAdapter()
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
}
val myAdapter = recyclerView.adapter as MyAdapter
myAdapter.userList = items
myAdapter.notifyDataSetChanged()
}
}
어댑터를 생성할때 setHasStableIds를 true로 해주고
override fun getItemId(position: Int): Long {
return position.toLong()
}
어댑터에서 getItemId를 오버라이드 해주면
깜빡이는 현상이 사라진다.
https://github.com/HanYeop/AndroidStudio-Practice2/tree/master/RecyclerViewDataBindingEx
참고
https://kangmin1012.tistory.com/16?category=879935
https://salix97.tistory.com/263