경험의 기록

 

2021.05.30 - [Android/AAC, MVVM] - [Android] 리사이클러뷰에서 DataBinding, LiveData 사용하기 (+ BindingAdapter)

 

[Android] 리사이클러뷰에서 DataBinding, LiveData 사용하기 (+ BindingAdapter)

리사이클러뷰에서 데이터바인딩과 라이브데이터를 사용하여 리스트의 변화를 감지하여 바로 리사이클러뷰를 갱신해주려고 한다. 1️⃣ 데이터 바인딩 종속성 추가 buildFeatures { dataBinding true } de

hanyeop.tistory.com

 

2021.08.03 - [Android/기본] - [Android] 리사이클러뷰 갱신 효율성을 위한 ListAdapter 사용하기

 

[Android] 리사이클러뷰 갱신 효율성을 위한 ListAdapter 사용하기

일반적으로 리사이클러뷰에선 아이템이 갱신될 때 마다 뷰가 갱신되기 때문에 아이템이 많을수록 효율성이 떨어지게 된다. 상식적으로 바뀐 뷰만 변경하는 것이 더 효율적이므로 안드로이드에

hanyeop.tistory.com

 

예전에 위 두 글에서

BindingAdapter 와 ListAdapter 에 대해 다루었다.

 

이번엔 BindingAdapter, ListAdapter 를 둘 다 사용하여 좀 더 효율적으로 리사이클러뷰를 구현해보고자 한다.

 


bulid.gradle (Module)

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를 추가한다.

 

User DataClass

data class User(
    val name: String,
    val age: Int,
    val imageUrl: Any
)

리사이클러뷰 아이템으로 사용할 User data class를 정의해준다.

drawable에 있는 이미지, url 이미지를 둘 다 사용하기 위해 imageUrl는 Any 로 선언하였다.

 

ItemView xml

위와 같은 아이템 뷰를 위해

<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 에서 설명한다.

 

MainActivity xml

<?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 에서 설명한다.

 

 

ViewModel

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로 생성한다.

이미지는 자신이 원하는 값을 넣는다.

 

ListAdapter

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 사용하기

 

[Android] 리사이클러뷰 갱신 효율성을 위한 ListAdapter 사용하기

일반적으로 리사이클러뷰에선 아이템이 갱신될 때 마다 뷰가 갱신되기 때문에 아이템이 많을수록 효율성이 떨어지게 된다. 상식적으로 바뀐 뷰만 변경하는 것이 더 효율적이므로 안드로이드에

hanyeop.tistory.com

 

ListAdapter에 대한 자세한 정보는 위 글에서 확인할 수 있다.

 

BindingAdapter

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의 작동 원리 및 갱신이 안되는 경우

개요 RecyclerView를 활용하여 목록을 리스팅할 때 흔히 사용하는 어답터로 RecyclerView.Adapter와 ListAdapter로 나뉜다. 전자는 아이템 목록을 직접 관리하며 값이 변경될 경우 변경된 범위, 항목에 대해

bb-library.tistory.com

 

단 ListAdapter는 리스트의 주소가 계속 바뀐다는 것을 가정하고 비교하여 갱신하기 때문에

데이터베이스나 API로 받아온 값이 아닌 내부 임의의 리스트를 계속 넘겨주면 갱신이 되지 않는다.

 

따라서 테스트를 위해 items.toMutableList() 를 넘겨주었다.

일반적인 상황엔 쓰지 않아도 된다.

 

setImage

imageUrl로 입력받은 값을 통해 glide로 이미지를 불러온다.

 

 

MainActivity

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

 

GitHub - HanYeop/AndroidStudio-Practice2: (2021.05.20~) 안드로이드 학습 내용 저장소

(2021.05.20~) 안드로이드 학습 내용 저장소. Contribute to HanYeop/AndroidStudio-Practice2 development by creating an account on GitHub.

github.com

 

 

 

 

참고

https://bb-library.tistory.com/257

 

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading