이번엔
- RecyclerView
- ViewPager
- image
들을 어떻게 하면 Databinding을 사용할 수 있는지 적어보도록 하겠습니다.
보통 RecyclerView를 사용하려면
- Adapter
- 반복될 Layout
- RecyclerView를 사용하는 곳에서의 선언
들이 필요한데 Databinding을 이용하면 약간씩 달라지게 된다. (기본적인 RecyclerView를 사용할 수 있다는 전제하에 진행하겠습니다 🙇♂️)
1. 가장 먼저 반복하여 사용할 Layout을 만들어줍니다.
<?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="item"
type="com.example.project.data.remote.response.kakao.local.AddressResponse" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<TextView
android:id="@+id/item_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="15dp"
android:fontFamily="@font/noto_medium"
android:text="@{item.addressName}"
android:textColor="@color/font_black"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/address" />
<TextView
android:id="@+id/item_road_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:fontFamily="@font/noto_regular"
android:text="@{item.roadAddress.addressName}"
android:textColor="@color/font_gray"
app:layout_constraintBottom_toTopOf="@+id/view8"
app:layout_constraintStart_toStartOf="@+id/item_address"
app:layout_constraintTop_toBottomOf="@+id/item_address"
tools:text="도로명 주소" />
<View
android:id="@+id/view8"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/line_lightGray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
위와 같이 바인딩으로 묶어주고 사용할 item(data class) 모델을 variable로 선언 해줍니다.
선언한 item으로 각 View에 넣어줍니다.
(카카오 주소찾기 api를 사용함 🕺🏼)
2. Adapter를 세팅해줍니다.
class FindAddressAdapter : RecyclerView.Adapter<FindAddressAdapter.MyViewHolder>() {
var addressClick: AddressClick? = null // 클릭시 이벤트 발생을 위한 listener
var addressList: List<AddressResponse>? = listOf() // 추후 bindingAdapter에서 추가해주기 위한 빈 리스트
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(addressList?.get(position))
}
inner class MyViewHolder(
val binding: ItemKakaoAddressBinding,
private val addressClick: AddressClick?
) : RecyclerView.ViewHolder(binding.root) {
fun bind(get: AddressResponse?) {
binding.item = get
if (addressClick != null) {
binding.root.setOnClickListener {
addressClick.selected(binding.item?.addressName)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
ItemKakaoAddressBinding.inflate( // xml을 바인딩으로 묶어준 후 선언
LayoutInflater.from(parent.context), parent, false
), addressClick
)
}
override fun getItemCount(): Int = addressList?.size ?: 0
// 깜박임 방지
override fun getItemId(position: Int): Long = position.toLong()
// 클릭시 이벤트 발생을 위한 listener
interface AddressClick {
fun selected(address: String?)
}
}
3. bindingAdpater을 만들어준다
만드는 이유 → 기존에 Activity나 Fragment에서 설정해주던 코드들을 이 곳에서 설정한다.
object FindAddressBindingAdapter { // 이름은 아무렇게나 설정해도 상관X
//region RecyclerView 관련
@BindingAdapter("address") // 해당 이름으로 xml 코드에서 사용하기 때문에 다른 함수명과 같게 설정하면 안 된다.
@JvmStatic // BindingAdapter 어노테이션을 사용한 함수의 경우 static으로 접근이 가능해야 하기 때문에 선언
fun setAddress(recyclerView: RecyclerView, items: List<AddressResponse>?) {
if (recyclerView.adapter == null) {
val adapter = FindAddressAdapter()
adapter.setHasStableIds(true) // 깜박임 방지
recyclerView.adapter = adapter
}
val myAdapter = recyclerView.adapter as FindAddressAdapter
items.let {
myAdapter.addressList = it // 여기서 리스트를 넣어준다.
myAdapter.notifyDataSetChanged() // 리스트를 재설정하는 코드
}
}
}
4. RecyclerView를 사용하는 Layout에서 bindingAdapter을 설정한다.
<?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>
<import type="android.view.View" />
<variable
name="vm"
type="com.work_guide.hwacha.viewModel.JoinViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.onBoarding.join.FindAddressFragment">
...
<-- 이 부분을 확인하면 된다.-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:visibility="@{vm.getKaKaoAddress.data.meta.totalCount == 0 ? View.GONE : View.VISIBLE}"
app:address="@{vm.getKaKaoAddress.data.documents}" <- bindingAdapter 사용
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" <- 이 코드도 꼭 넣어줘야 한다.(아니면 Activity나 Fragment에서 선언해줘야한다. 근데 굳이?...)
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_find_address"
app:layout_constraintVertical_bias="0.0"
tools:listitem="@layout/item_kakao_address" />
<-- 요기까지~ -->
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
위까지 설정한다면 끝이난다.
5. ClickListener를 달아야한다면 여기까지 진행하면 된다.
class FindAddressFragment : Fragment(), FindAddressAdapter.AddressClick {
private lateinit var binding: FragmentFindAddressBinding
private lateinit var navController: NavController
private val viewModel: JoinViewModel by sharedViewModel()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentFindAddressBinding.inflate(inflater, container, false).apply {
lifecycleOwner = this@FindAddressFragment
executePendingBindings()
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
binding.run {
vm = viewModel
// 이런 식으로 리스너를 직접 넣어준다.
(recyclerView.adapter as FindAddressAdapter).addressClick = this@FindAddressFragment
}
}
override fun selected(address: String?) { // 상속 받은 후 클릭 이벤트 처리
viewModel.businessAddress.value = address
navController.popBackStack()
}
}
이렇게 하면 RecyclerView를 Databinding 처리해서 View 처리를 다른 곳에서 가능하게 되어 Fragment(or Activity) 코드가 짧아지게 되어 MVVM 성질을 더욱 잘 띄어지게 된다.
ViewPager도 똑같이 사용하면 된다.
→ 3번 BindingAdapter에서 RecyclerView로 선언했던걸 ViewPager2로 선언해주면 똑같이 가능하다.(ViewPager2가 아닌 ViewPager을 사용한다면 그에 따른 코드를 선언해주면 된다. → Adapter도…)
🎆 이번엔 Image를 설정하는 법을 알아보도록 하자 📸
저는 Glide를 사용했기 때문에 Picasso나 다른 라이브러리를 사용한다면 코드를 보면서 그에 따른 코드를 적용시켜주면 된다.
6. bindingAdapter를 만들어준다.
object JobBindingAdapter {
...
@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(view: ImageView, url: String?) {
if (!url.isNullOrEmpty()) {
Glide.with(view.context) // view에 있는 context 사용.
.load(url) // 여기에 추가해주고.
.transform(MultiTransformation(CenterCrop(), RoundedCorners(5.dp))) // 이미지 커스텀 구간.(가운데를 기준으로 1:1 비율에 맞추기 / 코너를 5dp 만큼 둥글게)
.into(view) // view에 넣어준다.
}
}
...
}
7. 실제 layout에서 사용한다.
<?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>
<import type="android.text.Html" />
<import type="android.view.View" />
<variable
name="vm"
type="com.work_guide.hwacha.viewModel.JobViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.main.search.SearchDetailFragment">
...
<ImageView
android:id="@+id/imageView6"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginTop="20dp"
android:contentDescription="@string/reference_image"
app:imageUrl="@{vm.jobDetailResponse.data.brandImage}" <- 이 부분을 주목하자.
app:layout_constraintStart_toStartOf="@+id/guideline4"
app:layout_constraintTop_toBottomOf="@+id/textView46"
app:srcCompat="@drawable/icon_x_gray" />
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
이러면 끝이다! 너무 간단하지 않은가? (아뇨...🤦♂️)
따로 Activity, Fragment 또 Adapter에서(RecyclerView에서 사용할 때) 선언을 해주지 않아도 view에서 관리가 가능하다.
오히려 이게 불편하다고 할 수 있다. 코드를 쓰는건 개발자 마음이다. 하지만 유지 보수(미래의 나에게…)를 위해 각각 관리하는 곳이 정확하게 나뉘어져 있으면 정말 편해질 것이다.
현재 Android는 Compose를 더 밀면서 계속해서 개발이 되고 큰 기업에서도 부분적으로 또는 전면을 Compose를 사용해서 더 MV형태(Model, View)의 구조를 만들어가는 것으로 알고있다. 그 전에 이 구조를 사용하면서 넘어가기 전에 사용해보는 것도 좋을 것 같다.
❌ Error도 엄청 많이 받아봤다.
(계속해서 추가할 예정)
Required DataBindingComponent is null in class ActivityMainBindingImpl
위와 같은 에러가 나타날 경우가 있다.
BindingAdapter 어노테이션을 사용한 함수의 경우static으로 접근이 가능해야 한다.
@JvmStatic을 사용하거나아래와 같이 사용하면 된다.
코틀린에서는 클래스 내부가 아닌 바로 함수를 사용하게 될 경우, static으로 선언한 것이 된다.
'Development > Android' 카테고리의 다른 글
🛠 사용자들이 앱을 잠시 못 쓰게 할 수 있나? - Firebase Remote Config (0) | 2022.09.18 |
---|---|
Databinding 에서 어떻게 데이터들을 가공할까? 🍽 (1) | 2022.09.18 |
📦 Databinding을 쓰면서 - 1 (0) | 2022.09.08 |
의존성 주입의 Koin🪙 두 닢을 - 2 (0) | 2022.09.08 |
의존성 주입의 Koin🪙 한 닢을 - 1 (0) | 2022.09.08 |