관리 메뉴

나만을 위한 블로그

[Android] BindingAdapter란? BindingAdapter 사용법 본문

Android

[Android] BindingAdapter란? BindingAdapter 사용법

참깨빵위에참깨빵_ 2022. 9. 12. 23:01
728x90
반응형

데이터 바인딩은 중요하다. 어떻게 쓰느냐에 따라서 코드량이 확 줄어들 수 있다.

리사이클러뷰에 데이터 바인딩을 적용할 경우 서버에서 이미지를 받아와 리사이클러뷰에 뿌려야 할 때도 있다. 그럴 땐 보통 이미지 로드 라이브러리를 사용해서 이미지뷰나 둥근 이미지뷰에 붙일 것이다.

리사이클러뷰를 사용한다면 보통 onBindViewHolder() 안에서 Glide를 사용해 이미지뷰에 이미지들을 붙일 것이다.

 

override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
    val item = list[position]
    holder.bind(item)

    Glide.with(context)
        .load(item.imageUrls.regular)
        .into(binding.ivImageTest)
}

 

이는 액티비티, 프래그먼트에서도 비슷하게 사용할텐데, 제목에 적은 BindingAdapter란 걸 사용하면 귀찮게 Glide 코드를 적지 않아도 이미지가 이미지뷰에 데이터 바인딩된다.

 

먼저 BindingAdapter가 무엇인지 공식문서부터 확인한다.

 

https://developer.android.com/topic/libraries/data-binding/binding-adapters?hl=ko 

 

결합 어댑터  |  Android 개발자  |  Android Developers

결합 어댑터 알림 이 페이지를 개발자 프로필에 저장하여 중요 업데이트에 대한 알림을 받으세요. 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 결합 어댑

developer.android.com

결합 어댑터는 적절한 프레임워크를 호출해 값을 설정하는 작업을 담당한다. 한 가지 예로 setText()를 호출하는 것과 같이 속성 값을 설정하는 작업을 들 수 있다. 또는 setOnClickListener()를 호출하는 것과 같이 이벤트 리스너를 설정하는 작업이 있다. 데이터 바인딩 라이브러리를 쓰면 값을 설정하기 위해 호출되는 메서드를 지정하고 고유한 결합 로직을 제공하며 어댑터를 사용함으로써 반환된 객체의 유형을 지정할 수 있다
결합된 값이 바뀔 때마다 생성된 결합 클래스는 결합 표현식을 사용하여 뷰에서 setter를 호출해야 한다. 데이터 바인딩 라이브러리에서 메서드를 자동으로 결정하거나, 명시적으로 선언하거나, 맞춤 로직을 제공해 메서드를 선택하도록 허용할 수 있다...(중략)...일부 속성에는 맞춤 결합 로직이 필요하다. 예를 들어 android:paddingLeft 속성에는 연결된 setter가 없다. 대신 setPadding(left, top, right, bottom)이 제공된다. BindingAdapter 주석이 있는 정적 결합 어댑터 메서드를 사용하면 속성의 setter가 호출되는 방식을 맞춤설정할 수 있다. 안드로이드 프레임워크 클래스의 속성에는 BindingAdapter 주석이 이미 생성돼 있다. 예를 들어 다음 예는 paddingLeft 속성의 결합 어댑터를 보여준다
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}
매개변수 유형은 중요하다. 첫 번째 매개변수는 속성과 연결된 뷰의 유형을 결정한다. 두 번째 매개변수는 지정된 속성의 결합 표현식에서 허용되는 유형을 결정한다. 결합 어댑터는 다른 유형의 맞춤설정에 유용하다. 예를 들어 맞춤 로더는 작업자 쓰레드에서 호출되어 이미지를 로드할 수 있다. 개발자가 정의하는 결합 어댑터는 충돌이 발생하면 안드로이드 프레임워크에서 제공하는 기본 어댑터보다 우선 적용된다. 또한 다음 예와 같이 여러 속성을 받는 어댑터도 있을 수 있다
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}
다음 예와 같이 레이아웃에서 어댑터를 사용할 수 있다. 리소스를 @{}로 묶으면 유효한 결합 표현식이 된다...(중략)
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

 

이외에도 안드로이드 공식문서에 유용한 내용들이 많으니 한번 쭉 읽어보는 것도 좋다.

위 내용을 정리하면 BindingAdapter는 뷰에 어떤 값을 적용할지 조작할 수 있는 클래스고 바인딩할 뷰, 뷰에 넣을 데이터의 타입을 파라미터로 갖는 함수를 만들면 이미지든 글자든 다 데이터 바인딩된다는 뜻이다. 이것만 놓고 보면 커스텀 setter를 만드는 클래스가 BindingAdapter라고 생각된다.

 

그럼 바로 예제코드를 보자. 무료 이미지 API를 사용할 것이다. Hilt와 코루틴 Flow를 사용하긴 했지만 이 예제에선 필요없는 내용이니 리사이클러뷰와 BindingAdapter 관련 코드만 올린다. 먼저 이 예제에서 사용한 무료 이미지 API는 아래 링크의 것을 사용했다.

 

https://api.unsplash.com/

 

Unsplash Image API | Free HD Photo API

Codepen To make adding images into prototypes and code examples, Codepen integrates the Unsplash library, making finding and adding an image super easy.

unsplash.com

 

사용법은 구글링하면 바로 나오니 생략한다.

먼저 아이템 XML로 사용할 item_image.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">

    <data>
        <variable
            name="model"
            type="com.example.kotlinprac.recyclerview.bindingadapter.ImageUrls" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/ivImageTest"
            android:layout_width="200dp"
            android:layout_height="200dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

중간의 <variable> 태그에 있는 ImageUrls는 내가 무료 이미지 API를 사용하기 위해 만든 data class 이름이고 그 앞의 bindingadapter까지는 ImageUrls 파일의 전체 경로를 의미한다. 저 ImageUrls의 내부 코드는 아래와 같다.

 

import com.google.gson.annotations.SerializedName

data class ImageDTO(
    val id: String,
    @SerializedName("urls") var imageUrls: ImageUrls
)

data class ImageUrls(
    val raw: String,
    val full: String,
    val regular: String,
    val small: String,
    val thumb: String,
    val small_s3: String
)

 

ImageUrls 중 regular만 사용할 거라는 걸 미리 말해두고 간다. 다음은 BindingAdapter 코드다.

 

import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy

object BindingAdapter {
    @BindingAdapter("imageUrl")
    @JvmStatic
    fun bindImageToView(imageView: ImageView, url: String?) =
        Glide.with(imageView.context)
            .load(url)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .skipMemoryCache(true)
            .into(imageView)
}

 

BindingAdapter를 쓰기 위한 최소한의 어노테이션으로 함수를 구성했다. 참고로 저 함수명은 내가 맘대로 짓기만 하고 내가 어디서 호출해 사용하지는 않는다. 저렇게 지어만 두면 데이터 바인딩 라이브러리가 알아서 사용한다. Glide 옵션도 여기서 마음대로 설정해주자.

@BindingAdapter() 안의 문자열 역시 본인 마음대로 정해준다. 나중에 아이템 XML에서 호출해 사용할 것이다.

 

먼저 bindImageToView() 위의 어노테이션 2가지가 뭔지부터 짚고 넘어간다. 아래는 안드로이드 디벨로퍼에서 설명하는 @BindingAdapter다.

 

https://developer.android.com/reference/android/databinding/BindingAdapter

 

BindingAdapter  |  Android Developers

Notifications Save this page to your Developer Profile to get notifications on important updates. Stay organized with collections Save and categorize content based on your preferences. BindingAdapter public abstract @interface BindingAdapter implements Ann

developer.android.com

@BindingAdapter는 표현식이 있는 값을 뷰로 설정하는 방법을 조작하는 데 사용되는 메서드에 적용된다. 가장 간단한 예는 뷰와 설정할 값을 사용하는 공용 정적 메서드를 사용하는 것이다.
@BindingAdapter("android:bufferType")
 public static void setBufferType(TextView view, TextView.BufferType bufferType) {
     view.setText(view.getText(), bufferType);
 }
위 예에서 android:bufferType이 TextView에서 사용될 때 setBufferType()이 호출된다. 이전 값이 먼저 나열되는 경우 이전에 설정한 값을 사용할 수도 있다
바인딩 어댑터가 여러 속성을 가질 수도 있는 경우 바인딩 어댑터와 연결된 모든 속성에 연결된 바인딩 표현식이 있는 경우에만 호출된다. 이는 속성 간에 비정상적인 상호작용이 있을 때 유용하다
매개변수 순서는 BindingAdapter에 있는 값의 속성 순서와 일치해야 한다. 바인딩 어댑터는 선택적으로 DataBindingComponent를 확장하는 클래스를 첫 번째 매개변수로도 상요할 수 있다...(중략)

 

이 BindingAdapter는 2개의 퍼블릭 메서드를 갖는다.

 

  • fun requireAll(): Boolean - 모든 속성에 바인딩 표현식을 할당해야 하는지 또는 일부가 없을 수 있는지 여부
  • fun value(): String[]

 

requireAll()은 기본적으로 true인데 true라면 모든 값(@BindingAdapter 안의 value들)을 연결해줘야 한다. 만약 여러 값을 사용하는데 실제로 다 쓰진 않는 경우 이 값을 false로 설정하고 값들을 적으면 된다. 아래 형태로 적을 수 있다.

 

@BindingAdapter(
    value = ["profileUrl", "profilePlaceHolder"],
    requireAll = false
)
fun ImageView.setProfileUrl(url: String?, placeHolder: Drawable?) {
    val ph = placeHolder ?: ContextCompat.getDrawable(context, R.drawable.ic_default_profile)

    Glide.with(context)
            .load(url)
            .placeholder(ph)
            .apply(RequestOptions.circleCropTransform())
            .into(this)
}

 

만약 requireAll()을 false로 설정하고 나서 특정 인자에 값을 넣지 않으면 그 값은 null, false, 0 중 하나로 설정된다. 기본값을 설정해야 한다면 기본값을 설정하는 처리가 필요하다.

그리고 위 코드에는 @JvmStatic 어노테이션이 없는데, 확장 함수 형태로 만들어 사용할 경우 생략 가능하고 object 파일에 넣어서 사용할 필요도 없다.

@JvmStatic은 함수인 경우 추가 static 메서드를 만들어야 한다고 알려주는 어노테이션이다. 왜 필요한지 간단하게 예제를 찾아왔다.

 

class Bar {
    companion object {
        var barSize : Int = 0
    }
}

 

이 코드를 자바로 바꾸면 아래처럼 된다.

 

public final class Bar {
   private static int barSize;
   public static final class Companion {
      public final int getBarSize() {
         return Bar.barSize;
      }
      public final void setBarSize(int var1) {
         Bar.barSize = var1;
      }
   }
}

 

게터, 세터는 Companion이라는 클래스 안에 존재하고 barSize만 정적 인스턴스 변수로 선언돼 있다. 저 게터, 세터에 접근하려면 Bar.Companion.getBarSize() 형태로 접근해야 하는데 @JvmStatic을 붙이면 이야기가 달라진다.

 

class Bar {
    companion object {
        @JvmStatic var barSize : Int = 0
    }
}

 

이것을 자바로 컴파일하면 이렇게 된다.

 

public final class Bar {
   private static int barSize;
   public static final int getBarSize() {
      return barSize;
   }

   public static final void setBarSize(int var0) {
      barSize = var0;
   }

   public static final class Companion {
      public final int getBarSize() {
         return Bar.barSize;
      }
      public final void setBarSize(int var1) {
         Bar.barSize = var1;
      }
   }
}

 

정적 인스턴스 변수 밑에 정적 게터, 세터도 생긴 게 보인다.

만약 BindingAdapter 작성 시 확장 함수로 만들지 않았는데 @JvmStatic을 넣지 않았다면 빌드 도중 에러가 발생한다. 어떤 에러가 발생하는지는 직접 확인해보자.

 

본론으로 돌아와서 저렇게 BindingAdapter를 만들었다면 아이템 XML에 적용하면 그만이다. BindingAdapter에서 정의한 imageUrl을 그대로 가져와 사용하며, XML에서 사용할 때는 접두어로 app을 써야 한다. android를 쓸 경우 에러가 발생하는데 어떤 에러가 발생하는지는 역시 직접 확인해보자.

 

<?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">

    <data>
        <variable
            name="model"
            type="com.example.kotlinprac.recyclerview.bindingadapter.ImageUrls" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/ivImageTest"
            android:layout_width="200dp"
            android:layout_height="200dp"
            app:imageUrl="@{model.regular}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

이제 리사이클러뷰 어댑터를 구성한다.

 

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.kotlinprac.databinding.ItemImageBinding

class ImageAdapter(
    private val context: Context,
    private val list: List<ImageDTO>,
    private val clickListener: (ImageDTO) -> Unit
): RecyclerView.Adapter<ImageAdapter.ImageViewHolder>() {

    private lateinit var binding: ItemImageBinding

    inner class ImageViewHolder(
        private val binding: ItemImageBinding
    ): RecyclerView.ViewHolder(binding.root) {
        fun bind(item: ImageDTO) {
            binding.run {
                model = item.imageUrls
                root.setOnClickListener {
                    clickListener(item)
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
        binding = ItemImageBinding.inflate(LayoutInflater.from(context), parent, false)
        return ImageViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
        val item = list[position]
        holder.bind(item)
    }

    override fun getItemCount(): Int = list.size
}

 

fun bind()의 매개변수로 받은 DTO를 XML에 정의한 model 변수와 맺어주는 처리는 꼭 해줘야 한다. 그렇지 않으면 API 호출은 성공하더라도 이미지가 아무것도 나오지 않는다.

그리고 클릭 리스너를 간단하게 구성해서 클릭하면 로그가 찍히게 할 것이다. 

 

이제 액티비티에서 API를 사용하기 위한 accessKey를 넣어 함수를 호출한다.

전역 private 프로퍼티 중 ImageViewModel이 있고 이를 통해 API를 호출하는데 해당 부분만 본인에게 맞게 바꾸면 잘 작동할 것이다.

어댑터 초기화 시 위에서 만든 클릭 리스너를 구현하는 것도 잊지 말자.

 

import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ActivityBindingAdapterTestBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class BindingAdapterTestActivity : AppCompatActivity() {

    private val TAG = this.javaClass.simpleName
    private val imageViewModel: ImageViewModel by viewModels()

    private lateinit var binding: ActivityBindingAdapterTestBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_binding_adapter_test)
        binding.run {
            lifecycleOwner = this@BindingAdapterTestActivity

            getImages(1, "accessKey")
        }
    }

    private fun getImages(page: Int, clientId: String) {
        lifecycleScope.launch {
            imageViewModel.apply {
                this.getImages(page, clientId)
                imageResponse.collect {
                    when (it) {
                        is ApiState.Success -> {
                            it.data?.let { data ->
                                val result = data
                                binding.rvImage.apply {
                                    layoutManager = LinearLayoutManager(this@BindingAdapterTestActivity)
                                    setHasFixedSize(true)
                                    adapter = ImageAdapter(this@BindingAdapterTestActivity, result) { imageDTO ->
                                        Log.e(TAG, "regular : ${imageDTO.imageUrls.regular}")
                                    }
                                }
                            }
                            mImageResponse.value = ApiState.Loading()
                        }
                        is ApiState.Error -> {
                            Log.e(TAG, "에러 : ${it.message}")
                            mImageResponse.value = ApiState.Loading()
                        }
                        is ApiState.Loading -> {}
                    }
                }
            }
        }
    }

}

 

이제 빌드하면 아래처럼 실행 시마다 랜덤한 이미지를 보여주는 리사이클러뷰가 보일 것이다.

 

 

아이템 클릭 시 로그도 아래처럼 찍힐 것이다.

 

 

클릭 리스너가 액티비티에서 작동하는 걸 확인했으니 이제 화면 이동이든 뭐든 할 수 있다.

잘만 쓰면 코드량을 엄청나게 줄여버릴 수 있는 좋은 방법이기 때문에 데이터 바인딩 사용 중 리사이클러뷰를 구성할 일이 있다면 이렇게 하는 걸 추천한다.

 

참고한 사이트)

 

https://leveloper.tistory.com/177

 

[Android] DataBinding - BindingAdapter 활용하기

 DataBinding이란 xml 파일에 data를 연결(binding)해서 사용할 수 있게 도와주는 Android Jetpack 라이브러리에서 제공하는 기능 중 하나로써, 보통 MVVM 패턴을 구현할 때 LiveData와 함께 많이 사용하는 편이.

leveloper.tistory.com

 

https://velog.io/@suev72/JvmStatic%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

 

@JvmStatic가 무엇일까?

안드로이드 New->Fragment 로 프래그먼트를 생성해보면, 이렇게 newInstance함수를 자동으로 만들어주는데, @JvmStatic이라는 어노테이션을 볼 수 있다. @JvmStatic가 뭘까? > 결론: Java의 static 처럼 쓰기 위함

velog.io

 

반응형
Comments