안드로이드 연구소

[MVVM만들기] DataBinding 본문

안드로이드 연구소/MVVM+AAC

[MVVM만들기] DataBinding

안드로이드 연구원 2023. 5. 12. 14:16

이전 게시물에서 MVVM패턴에서

안드로이드 아키텍처 컴포넌트(AAC) ViewModel와 LiveData를 사용하여

기본적인 구조를 설계하였습니다.

 

지난번 예제에서 엑티비티에서 아래와 같이 

myViewModel에 위치한 userData를 항상 관찰시켜서

변화가 가질될 때를 기다려 UI를 업데이트 시켜줬습니다.

myViewModel.userData.observe(this, { user ->
    // 로직3: 새 데이터 변화 관측 시 UI업데이트
    binding.nameTextView.text = user.name
    binding.ageTextView.text = user.age.toString()
})

근데 매번 이렇게 데이터 하나하나를 이렇게 관찰 observe를 세팅하고

UI를 업데이트 하는 로직을 사용하는건 

매우 매우 비효율적인 코드일 수 가 없습니다.

만약 데이터 100개라면 ...

 

이를 해결하기 위해서 우리는 어떻게 할 수 있을까요?

정답은 AAC의 DataBiding이라는 라이브러리입니다.

한번 DataBinding에 대해서 알아보아요.

 

 

Q1. Hi ChatGPT, 안드로이드 아키텍처 컴포넌트 DataBinding에 대해 설명해줘.

Data Binding은 앱의 UI 요소앱 코드의 데이터 소스직접 바인딩하여
보일러플레이트 코드를 제거한 라이브러리입니다.

이를 사용하여 UI 컴포넌트에 데이터 개체에 바인딩할 수 있으며,
기본 데이터 개체가 변경되면 데이터가 자동으로 업데이트됩니다.

간단히 말하면 처음에 데이터와 UI컴포넌트를 서로 바인딩해놓으면

알아서 데이터가 바뀐다는 그런 뜻이네요. 사용하면 너무 편하겠죠?

그렇다면 지난 예제에 DataBinding을 넣어볼까요?

 

 

Q2. 지난 예제에 DataBinding을 사용한 예제를 보여줘!

build.gradle에서 설정먼저

android {
    dataBinding {
        enabled = true
    }
}

 

첫번째로, 엑티비티와 연결된 xml에 최상위에 <layout>태그를 감싸줍니다.

 

두번째로, 그리고 <data>태그안에 type안에 MyViewModel.kt를 연결해주고

name으로 xml의 <data>이름을 생성합니다.

(LinearLayout이 아니여도 괜찮습니다.)

 

세번째로, nameTextView에서 text에서 @{viewModel.userData.name}을 사용하여

MyViewModel에 위치한 userData데이터의 name을 가져오도록 세팅을 해줍니다.

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="myViewModel"
            type="com.example.MyViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/nameTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.userData.name}" />

        <TextView
            android:id="@+id/ageTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.userData.age.toString()}" />

    </LinearLayout>

</layout>

그 다음 xml과 연결되있는 엑티비티에서
첫번째로, DataBindingUtil.setContentView 메서드를 사용하여 레이아웃 파일을 확장하고 활동에 바인딩합니다.
두번째로, lifecycleOwner와 엑티비티와 바인딩하여 LiveData가 변경될 때 UI가 업데이트 되도록 합니다.

세번째로, "xml에서 만든 <data>"와 "ViewModel 인스턴스"를 연결합니다.

class MyActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMyBinding
    private val myViewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // xml파일 Layout형태로 확장 후 엑티비티에 연결
        binding = DataBindingUtil.setContentView(this, R.layout.activity_my)
        // lifecycleOwner와 엑티비티와 연결
        binding.lifecycleOwner = this 
        // xml의 data와 viewModel 연결
        binding.myViewModel = myViewModel

        // viewModel 데이터 업데이트
        val newUser = User("John", 25)
        myViewModel.updateUser(newUser)
    }
}

DataBinding 끝!

모두 연결만 해놓기만 하면 되는거 아시겠죠?

만약에 여기서 데이터가 List여서 리싸이클러뷰로 출력하려면 어떻게 해야할까요?

 

 

Q3. 리스트 타입의 데이터를 Recyclerview에서 DataBinding을 사용한 예제를 보여줘!

item_user.xml 리싸이클러뷰 하나 하나 데이터를 보여주는 아이템인데요.

여기 아이템에서도 viewModel의 data를 연결할 수 있네요.

또 밑에서도 마찬가지로 Textview컴포넌트에 @{user.name}으로 데이터를 바인딩하였구요.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="user"
            type="com.example.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:id="@+id/nameTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />

        <TextView
            android:id="@+id/ageTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{user.age}" />
    </LinearLayout>
</layout>

 

activity_my.xml

여기서 리싸이클러뷰에서 

items에 @{viewModel.userList}에 데이터를 연결해줘서 이전 아이템에서 데이터를 쓸 수 있었나봅니다.

또 @{viewModel.userAdapter}로 adapter도 바로 연결하였군요. 

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.MyViewModel" />
    </data>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/userRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:padding="@dimen/padding_normal"
        android:scrollbars="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:items="@{viewModel.userList}"
        app:adapter="@{viewModel.userAdapter}" />
</layout>

 

UserAdapter.kt

어뎁터에서도 userList를 mutableListOf<User> 데이터를 이용한 것 말고 크게 다른게 없어보이네요.

class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
    private val userList = mutableListOf<User>()

    // 데이터 초기화 후 세팅
    fun setUserList(newUserList: List<User>) {
        userList.clear()
        userList.addAll(newUserList)
        notifyDataSetChanged()
    }

    // 뷰홀더 생성
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    // 뷰홀더에 데이터 바인딩
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val user = userList[position]
        holder.bind(user)
    }

    // 아이템 갯수
    override fun getItemCount() = userList.size

    inner class ViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(user: User) {
            binding.user = user
        }
    }
}

 

MyViewModel.kt

엑티비티에서 UserAdapter를 호출하지 않고 viewModel에서 사용되고 있군요.

이렇게 viewodel에서 UserAdapter인스턴스를 만들어놓기만해도

 

activity_my.xml의 recyclerview에서

app:adapter="@{viewModel.userAdapter}"를 세팅해놔서 바로 연결이 됩니다.

class MyViewModel : ViewModel() {
    private val _userList = MutableLiveData<List<User>>()
    val userList: LiveData<List<User>> = _userList

    val userAdapter = UserAdapter()

    fun updateUserList(newUserList: List<User>) {
        _userList.value = newUserList
    }
}

그럼 리스트 타입 데이터로 리싸이클러뷰 DataBinding도 끝!

 

<23.5.15일 내용 추가>

Q4. 리스트 타입의 데이터를 DiffUtil을 사용한 Recyclerview에서 DataBinding을 사용한 예제를 보여줘!

이 내용은 심화 과정입니다.

만약에 Adapter에 변화를 감지하여 UI업데이트를 해주는 DiffUtil에서는

어떻게 DataBinding을 이용할 수 있을까요?

 

첫번째로 UserAdater안에서 사용하던 DiffCallback 클래스를 함수로 분리하였습니다.

class UserDiffCallback : 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
    }
}

그리고 어뎁터에서도 파라미터로 diffcallback 인스턴스를 받게 하였구요.

class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : ListAdapter<User, UserAdapter.ViewHolder>(diffCallback) {

    // Rest of your code...
}

ViewModel에서 Adapter양식에 맞춰 인스턴스를 생성하였습니다.

class MyViewModel(application: Application) : AndroidViewModel(application) {
    val userList: LiveData<List<User>> = MutableLiveData()
    val userAdapter: UserAdapter = UserAdapter(UserDiffCallback())

    // Rest of your code...
}

app:adapter에서 아래처럼 사용하여 Databinding을 해주었습니다.

viewModel.userAdapter.setDiffCallback(new com.example.UserDiffCallback())

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/userRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:padding="@dimen/padding_normal"
    android:scrollbars="vertical"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    app:items="@{viewModel.userList}"
    app:adapter="@{viewModel.userAdapter.setDiffCallback(new com.example.UserDiffCallback())}" />

 

Comments