티스토리 뷰
리사이클러뷰를 사용할 때 성능향상을 위해 DiffUtil을 이용할 것이다. 그런데, DiffUtil을 사용하면서 주의해야할 중의 하나는 ArrayList나 var 멤버가 포함된 mutable한 데이터 타입을 사용하는 것이다. 결론적으로 말하면, 이런 데이터 타입을 사용할 경우 데이터를 변경했음 해도 불구하고 리사이클러뷰가 업데이트 되지 않게 되는 현상에 직면하게 된다.
관련 코드를 통해 해당 증상을 살펴보자. 아래와 같이 ListAdapter를 이용해 사용자를 추가하는 간단한 앱이 있다.
Add를 누르면 사용자를 리사이클러뷰에 추가할 것이다.
레이아웃과 관련코드이다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<Button
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/userList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/addButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupView()
}
private val userAdapter by lazy { UserAdapter() }
private val users = arrayListOf<User>()
private fun setupView() {
val userRecyclerView = findViewById<RecyclerView>(R.id.userList)
userRecyclerView.adapter = userAdapter
userAdapter.submitList(users)
val addButton = findViewById<Button>(R.id.addButton)
addButton.setOnClickListener {
addUser()
}
}
private fun addUser() {
users.add(getUser())
userAdapter.submitList(users)
}
private fun getUser(): User {
val nextId = users.size.inc().toString()
return User(
id = nextId,
name = "User $nextId",
email = "user$nextId@email.com"
)
}
}
data class User(
val id: String,
val name: String,
val email: String
)
class UserAdapter : ListAdapter<User, UserViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_2, parent, false)
return UserViewHolder(itemView)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = getItem(position) ?: return
holder.bind(user)
}
}
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val text1 by lazy { itemView.findViewById<TextView>(android.R.id.text1) }
private val text2 by lazy { itemView.findViewById<TextView>(android.R.id.text2) }
fun bind(user: User) {
text1.text = user.name
text2.text = user.email
}
}
Add버튼을 클릭할 때 User를 리사이클러뷰에 추가해는 아주 간단한 코드이다. 위의 코드를 실행해 보면 Add 버튼을 눌러도 화면에는 아무 변화가 없을 것이다. 그러나 디버깅을 해보면 users에는 User가 추가되어 있는 것을 알 수 있다.
그럼, 이번에는 users를 ArrayList대신에 List를 사용하고 대신 name과 email을 var타입으로 변경한 후 첫번째 User의 name과 email 을 변경한 후 submitList를 해보자.
data class User(
val id: String,
var name: String,
var email: String
)
class MainActivity : AppCompatActivity() {
...
private fun setupView() {
val userRecyclerView = findViewById<RecyclerView>(R.id.userList)
userRecyclerView.adapter = userAdapter
users.add(getUser())
users.add(getUser())
userAdapter.submitList(users)
val addButton = findViewById<Button>(R.id.addButton)
addButton.setOnClickListener {
updateUser()
}
}
private fun updateUser() {
val firstUser = users.first()
firstUser.name = "Alice"
firstUser.email = "alice@email.com"
userAdapter.submitList(users)
}
}
User를 2명 추가한 후 Add버튼을 클릭할 때 첫번째 User의 이름과 이메일을 업데이트 한 후 submitList를 해지만, 여전히 리사이클러뷰에는 아무런 변화가 없을 것이다.
어째서 이런 현상이 일어나는 걸까?
DiffUtil의 구현체인 AsyncListDiffer.java의 소스코드를 보면 아래와 같이 제일 먼저 동일 인스터인지 비교를 한다.
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
if (newList == mList) { // <-- !!!
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
...
}
따라서, ListAdapter를 사용할 때 MutableList 타입이나 var 멤버를 사용해서 아이템을 추가하거나 변경할 경우라도 == 비교는 동일한 인스터스의 경우 항상 true이므로 RecyclerView는 업데이트 되지 않게 된다. 그럼, 이에 대한 해결책을 무엇일까?
MutableList대신 List 데이터 타입을 쓰고 변경시 다른 List 인스턴스를 리턴하도록 만들면 된다.
private fun addUser() {
users = users.plus(getUser())
userAdapter.submitList(users)
}
private fun updateUser(user: User) {
users = users.map { userItem ->
if (userItem.id == user.id)
userItem.copy(name = user.name, email = user.email)
else
userItem
}
userAdapter.submitList(users)
}
'Android' 카테고리의 다른 글
DialogFragment handling from a Fragment using Result API (0) | 2022.02.17 |
---|---|
Android, how to use String Resource in an abstract way (1) | 2022.02.13 |
Fragment에서 ViewBinding 메모리누수 방지하기 (0) | 2021.07.30 |
빌드 환경에 따라 BaseURL 관리하기 (0) | 2020.09.26 |
DataBinding을 이용하여 MVVM 패턴 구현하기 (0) | 2016.10.03 |