2021.05.30 - [Android/AAC, MVVM] - [Android] 리사이클러뷰에서 DataBinding, LiveData 사용하기 (+ BindingAdapter)
2021.08.03 - [Android/기본] - [Android] 리사이클러뷰 갱신 효율성을 위한 ListAdapter 사용하기
예전에 위 두 글에서
BindingAdapter 와 ListAdapter 에 대해 다루었다.
이번엔 BindingAdapter, ListAdapter 를 둘 다 사용하여 좀 더 효율적으로 리사이클러뷰를 구현해보고자 한다.
android {
buildFeatures {
dataBinding true
}
}
데이터바인딩을 사용하기 위해 추가해주고
dependencies {
// Activity KTX for viewModels()
implementation "androidx.activity:activity-ktx:1.4.0"
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
}
액티비티에서 뷰모델을 쉽게 생성하기 위한 activity-ktx, 이미지를 불러오기 위한 glide를 추가한다.
data class User(
val name: String,
val age: Int,
val imageUrl: Any
)
리사이클러뷰 아이템으로 사용할 User data class를 정의해준다.
drawable에 있는 이미지, url 이미지를 둘 다 사용하기 위해 imageUrl는 Any 로 선언하였다.
위와 같은 아이템 뷰를 위해
<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.bindingadapterex.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"
image="@{user.imageUrl}"
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>
위와같은 XML을 작성했다.
여기서 ImageView의 image 속성은 BindngAdapter 에서 설명한다.
<?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="viewModel"
type="com.hanyeop.bindingadapterex.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<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_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/btn_add"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
items="@{viewModel.userList}">
</androidx.recyclerview.widget.RecyclerView>
<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:onClick="@{() -> viewModel.buttonClick()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
임의의 버튼을 하나 만들고
그 버튼 클릭 시 뷰모델의 buttonClick 함수를 호출하여 리사이클러뷰 아이템을 추가하려고 한다.
여기서 recyclerView의 items 속성도 BindngAdapter 에서 설명한다.
class MainViewModel : ViewModel() {
private val _userList = MutableLiveData<ArrayList<User>>()
val userList : LiveData<ArrayList<User>>
get() = _userList
// 테스트를 위한 값들
private var num = 1
private val url = R.drawable.dog
// TODO
private val url2 = "임의의 URL"
private val items = arrayListOf(
User("Han",25, url),
User("Lee",33, url)
)
init{
_userList.value = items
}
// 클릭 시 임의의 데이터 추가
fun buttonClick(){
val user = User("Test $num",20, url2)
items.add(user)
_userList.value = items
num++
}
}
이제 뷰모델에서
리사이클러뷰에 띄워줄 값을 가지고 있는 userList를 LiveData로 생성한다.
이미지는 자신이 원하는 값을 넣는다.
class MyAdapter()
: ListAdapter <User, MyAdapter.MyViewHolder> (diffUtil) {
// 생성된 뷰 홀더에 값 지정
inner class MyViewHolder(private 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(getItem(position))
}
// 리스트 갱신
override fun submitList(list: List<User>?) {
super.submitList(list)
}
// diffUtil 추가
companion object{
val diffUtil = object : DiffUtil.ItemCallback<User>(){
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
}
}
}
어댑터를 ListAdapter 로 선언하고 User 를 아이템으로 가지도록 한다.
ListAdapter 는 자체적으로 아이템 리스트를 관리하므로 별도의 아이템 리스트를 내부에 생성할 필요 없고,
getItem(position) 으로 원하는 위치의 값을 불러올 수 있다.
submitList에 아이템 리스트만 넘겨주면 diffUtil을 통해 바뀐 데이터를 알아서 갱신한다.
1차적으로 areItemsTheSame에서 객체 고유의 값 (엔티티 - primary key)을 비교하도록 하는 것이 좋은데
여기선 임의로 만든 리스트를 사용하므로 User의 name을 기준으로 잡았다.
2021.08.03 - [Android/기본] - [Android] 리사이클러뷰 갱신 효율성을 위한 ListAdapter 사용하기
ListAdapter에 대한 자세한 정보는 위 글에서 확인할 수 있다.
object MyBindingAdapter {
// 어댑터 아이템 연결, 갱신
@BindingAdapter("items")
@JvmStatic
fun setItems(recyclerView: RecyclerView, items : ArrayList<User>){
// 어댑터 최초 연결
if(recyclerView.adapter == null) {
val adapter = MyAdapter()
recyclerView.adapter = adapter
}
val myAdapter = recyclerView.adapter as MyAdapter
// 자동 갱신
myAdapter.submitList(items.toMutableList())
}
// 이미지 바인딩
@BindingAdapter("image")
@JvmStatic
fun setImage(imageView: ImageView, imageUrl: Any){
Glide.with(imageView.context)
.load(imageUrl)
.override(200,200)
.circleCrop().into(imageView)
}
}
이제 바인딩어댑터에서
아까 위의 xml에서 살펴본 items, image 속성을 정의한다.
setItems 은
어댑터가 연결되어 있지 않으면 연결하도록 하고
items로 입력받은 list가 변경될 때 어댑터의 submitList에 넘겨주어 자동으로 갱신하도록 한다.
setItems 에서 어댑터를 연결하는 작업은
위와 같이 바인딩어댑터가 아닌 MainActivity에서 진행하거나 Hilt로 의존성 주입을 사용해도 된다.
object MyBindingAdapter {
// 어댑터 아이템 연결, 갱신
@BindingAdapter("items")
@JvmStatic
fun RecyclerView.setItems(items : ArrayList<User>){
// 어댑터 최초 연결
if(this.adapter == null) {
val adapter = MyAdapter()
this.adapter = adapter
}
val myAdapter = this.adapter as MyAdapter
// 자동 갱신
myAdapter.submitList(items.toMutableList())
}
// 이미지 바인딩
@BindingAdapter("image")
@JvmStatic
fun ImageView.setImage (imageUrl: Any){
Glide.with(this.context)
.load(imageUrl)
.override(200,200)
.circleCrop().into(this)
}
}
위와 같이 확장함수로 선언하여 사용할 수도 있다.
https://bb-library.tistory.com/257
단 ListAdapter는 리스트의 주소가 계속 바뀐다는 것을 가정하고 비교하여 갱신하기 때문에
데이터베이스나 API로 받아온 값이 아닌 내부 임의의 리스트를 계속 넘겨주면 갱신이 되지 않는다.
따라서 테스트를 위해 items.toMutableList() 를 넘겨주었다.
일반적인 상황엔 쓰지 않아도 된다.
setImage 는
imageUrl로 입력받은 값을 통해 glide로 이미지를 불러온다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val mainViewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
binding.apply {
lifecycleOwner = this@MainActivity
viewModel = mainViewModel
}
}
}
메인액티비티에서는 lifecycleOwner를 종속시켜주고,
xml에 선언되어있는 viewModel에 mainViewModel을 바인딩하는 코드만 작성해주면 된다.
이제 바인딩어댑터에서 아이템 바인딩 처리를 다 해주기 때문에
메인액티비티나 리사이클러뷰 어댑터에서 아이템의 이미지, 갱신 등의 처리를 위해 별도의 로직을 작성하지 않아도 된다.
최초 실행 시 처음에 넣어놓은 2개의 아이템이 잘 출력되는 것을 확인할 수 있고
버튼 클릭 시마다 새로운 아이템이 생성되어 자동으로 리사이클러뷰에 보여진다.
전체 코드
https://github.com/HanYeop/AndroidStudio-Practice2/tree/master/BindingAdapterEx
참고
https://bb-library.tistory.com/257