[PROJECT] 프로젝트에 DataBinding & @BindingAdapter 사용해보기

2024. 1. 24. 02:41·PROJECT

  • 이번엔 DataBinding과 BindingAdapter를 사용해  Room에서 가져온 이미지를 ImageView에 보여주는 연습을 해보겠습니다. 이 글의 예제 코드들은 제가 진행 중인 프로젝트에서 가져온 코드라 다소 뜬금없는 부분이 있을 수 도 있습니다.

안드로이드 공식 문서

 

데이터 결합 라이브러리  |  Android 개발자  |  Android Developers

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 데이터 결합 라이브러리 Android Jetpack의 구성요소. 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선

developer.android.com

🎉 DataBinding이란 ?

XML Layout 파일과 데이터를 직접 연결해 주는 기술입니다. DataBinding을 사용하면 Activity나 Fragment에서 View를 직접 정의하여 Data를 전달하지 않아도 UI에 데이터를 랜더링 할 수 있습니다. 또한 Observable 객체를 지원하여 데이터의 변경을 감지하고 자동으로 UI를 업데이트할 수 있습니다.

 

즉, 데이터와 뷰 연결 작업을 레이아웃에서 처리하기 위해 등장한 게 DataBinding입니다.

🎉 DataBinding 사용하기

1. app 수준의 build.gradle에 다음 코드를 입력해 줍니다.

buildFeatures {
    viewBinding true
    dataBinding true
}

 

2. 레이아웃의 루트태그를 < layout >으로 감싸주고 그 안에 < data >태그를 추가합니다.

  • <layout> 태그는 데이터와 뷰를 감싸주는 역할만 할 뿐 ConstraintLayout이나 LinearLayout처럼 Android 앱 개발에 직접 사용되는 Layout은 아니며 실제 레이아웃을 포함해야 합니다.
  • <data> 태그는 해당 xml 파일에서 데이터 연결을 위해 변수처럼 사용할 것들을 정의하는 태그입니다.
  • <variable> 태그의 속성으로 name과 type을 지정합니다.
    • name : viewModel을 호출할 변수의 이름
    • type : viewModel
  • 태그 안에서 정의해 준 변수들을 사용하려면 @{ } 구문을 사용
<layout
    tools:context=".view.intro.myStore.MyStoreFragment"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="vm"
            type="com.myproject.cloudbridge.viewModel.MyPageViewModel" />
    </data>
    
    (생략...)
    
<TextView
    android:id="@+id/store_ceo_name_textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:fontFamily="@font/rubik_bold"
    android:textColor="@color/shrine_pink_900"
    android:text="@{vm.myStore.storeName}"
    android:textSize="20sp" />
    
    (생략...)

 

3. DataBinding 호출 및 XML File Inflate 

  • DataBindingUtil Class의 inflate 메서드를 호출해 Layout을 inflate 시켜줍니다.
  • viewModel을 지정하고 현재 Activity나 Fragment의 lifeCycle에 따라 UI를 업데이트하고 관리할 수 있도록 프래그먼트의 라이프사이클 소유자(LifecycleOwner)를 설정합니다.
class MyStoreFragment : Fragment() {
    private var _binding: FragmentMyStoreBinding? = null
    private val binding: FragmentMyStoreBinding
        get() = _binding!!
    private val viewModel: MyPageViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // LayoutInflate
        _binding = DataBindingUtil.inflate(inflater, R.layout.fragment_my_store, container, false)
        // viewModel 지정
        binding.vm = viewModel
        // lifecycleOwner 지정
        binding.lifecycleOwner = this

        viewModel.getMyStoreInfo()
        return binding.root
    }

🎉 @BindingAdapter 란?

  • 개발자가 직접 정의한 XML 속성을 View에 바인딩하는 기능입니다.
  • 반드시 정적(static) 메서드로 정의되어 싱글턴 패턴(객체가 단 한 번만 생성)으로 작성되어야 합니다.
    • DataBinding은 코드를 컴파일하는 시점에 Layout 파일에서 참조한 View에 대한 바인딩 클래스를 성성합니다.
    • 이 바인딩 클래스에는 바인딩 어댑터 메소드에 대한 참조가 저장되어 있습니다.
    • 만약 BindingAdapter Method가 인스턴스 메소드로 정의되어 있다면 해당 메소드 호출을 위해 바인딩 클래스의 인스턴스(객체)를 생성해야 합니다.
    • 정적(Static) 메소드로 정의되어 있다면 인스턴스(객체)를 생성하지 않고도 직접 접근할 수 있습니다.
    • Kotlin에선 object Class를 통해 정의합니다.
    • object Class 대신 companion Object를 사용한다면 @JvmStatic 어노테이션으로 자바와 코틀린 코드 간의 상호 운용성을 보장해줘야 합니다.
    • @JvmStatic : 코틀린에서 정의한 정적 메소드를 자바에서도 정적 메소드로 사용
  • app수준의 build.gradle 파일에 kapt를 등록해줘야 합니다.
    • kapt( Kotlin Annotation Processing Tool) : 코틀린 어노테이션 프로세서(Annotation Processor)를 실행하기 위한 도구
    • 코틀린 어노테이션 프로세서 : Kotlin 코드 컴파일 시 소스 코드에 있는 어노테이션을 분석하고 그에 따라 코드를 생성하거나 변환
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.gms.google-services'
    id 'androidx.navigation.safeargs.kotlin'
    id 'kotlin-kapt'
}

📕 ImageView DataBinding

ImageView에 DataBinding으로 이미지를 전달하기 위해선 다음과 같은 주의사항이 있습니다.

  • android:src 속성은 res폴더 안에 있는 이미지만 보여줄 수 있습니다.
  • 이 글에선 서버에서 Room에 저장한 이미지를 가져와 보여주기 때문에 src 속성을 사용할 수 없습니다.
  • @BindingAdapter을 사용해 직접 원하는 속성을 만들어 이미지를 바인딩할 수 있습니다.

이미지뷰에 바인딩하는 과정에 들어가기 앞서 서버에서 데이터를 가져오는 과정은 지금 새벽 세시라 생략하고 Room에서 의 프로세스는 코드만 올리겠습니다! 예! 겁나 졸려요!

1️⃣ XML Setting

  • 데이터 바인딩을 위해 layout으로 감싼 뒤 data영역과 뷰 영역을 만들었습니다.
  • myStoreImage="@{vm.myStore.image}" 이 부분은 밑에서 설명하겠습니다.🤗
<layout
    tools:context=".view.intro.myStore.MyStoreFragment"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="vm"
            type="com.myproject.cloudbridge.viewModel.MyPageViewModel" />
    </data>
    <ImageView
        android:id="@+id/main_image"
        android:layout_width="250dp"
        android:layout_height="250dp"
        myStoreImage="@{vm.myStore.image}"
        />
</layout>

 

2️⃣ Room에서 데이터 가져오기

  • 현재 데이터를 가져오는 과정은 아래와 같습니다. Repository에서 ViewModel을 걸치는 작업을 두 번 하게 되는데 이 과정을 줄일 계획이라 다음에 포스팅하겠습니다. 아마 Workmanager를 사용해서 Background에서 작업하면 될 거 같은데 과연?

@Dao
interface StoreDao {
    @Query("SELECT * FROM store_table")
    fun readAllStoreInfo() : Flow<List<StoreEntity>>

    @Query("SELECT * FROM store_table WHERE company_registration_number = :crn")
    fun readMyStoreInfo(crn: String) : Flow<StoreEntity>
}

////////////////////////////////////////////////////////////////////////////////////

@Entity(tableName = "store_table")
data class StoreEntity (
    @PrimaryKey
    @ColumnInfo(name = "company_registration_number")
    val crn: String,
    @ColumnInfo(name = "store_image")
    val image: Bitmap,
    @ColumnInfo(name = "store_name")
    val storeName: String,
    @ColumnInfo(name = "ceo_name")
    val ceoName: String,
    val contact: String,
    val address: String,
    val latitude: String,
    val longitude: String,
    val kind: String
)

//////////////////////////////////////////////////////////////////////////////////

@Database(entities = [StoreEntity::class], version=1)
@TypeConverters(ImageTypeConverter::class)
abstract class MainDatabase: RoomDatabase() {
    abstract fun storeDao(): StoreDao

    companion object{

        @Volatile
        private var INSTANCE: MainDatabase ?= null

        fun getDatabase(context: Context): MainDatabase{
            return INSTANCE ?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    MainDatabase::class.java,
                    "main_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

//////////////////////////////////////////////////////////////////////////////////

class DBRepository {
    val mainDB = MainDatabase.getDatabase(context).storeDao()
    suspend fun getMyStoreInfo(crn: String) = mainDB.readMyStoreInfo(crn)
}

2️⃣ ViewModel Setting

  • DataStore를 사용해 사업자등록번호 값을 저장해 Room에서 나의 매장을 식별하는 Primary Key로 사용
  • Room에서 받은 데이터는 Flow Type이기 때문에 StateFlow로 변환하기 위해 stateIn( ) 메소드 사용
    • StateFlow가 궁금하다면 이 글을 봐주세요
  • XML Layout은 myStore로부터 Data를 바인딩
 

[Kotlin] LiveData -> StateFlow

앞선 글에서 소개했듯이 프로젝트에 있던 LiveData들을 StateFlow로 대체하기 위해 공부하고 있습니다. 안드로이드에서는 어떤 이유로 Live Data를 StateFlow로 대체하라고 하는걸까요 ? [Kotlin] Coroutine Flow

chanho-study.tistory.com

class MyPageViewModel: ViewModel() {
    
    private val dbRepository = DBRepository()
    private val dataStore = MyDataStore()
    private var _myStore: MutableStateFlow<StoreEntity> = MutableStateFlow(createFirstData())
    val myStore: StateFlow<StoreEntity> get() = _myStore

    fun getMyStoreInfo() = viewModelScope.launch(Dispatchers.IO) {
        try {
            // 1. Datastore에 저장된 사업자 등록번호를 가지고
            dataStore.getCrn().collect{ crn->
                
                // 2. Room에서 나의 매장 정보를 가져와
                val response = dbRepository.getMyStoreInfo(crn)
                    
                // 3. flow로 반환된 데이터를 StateFlow로 변환해 저장
                response.stateIn(viewModelScope).collect{ myStore->
                    _myStore.value = myStore
                }

            }
        }catch (e: Exception){
            Log.d("MyPageViewModel","MyPageViewModel: $e")
        }
    }

3️⃣ BindingAdapter Setting

  • static 메서드를 가진 object 클래스 생성합니다.
  • @BindingAdapter("속성명")을 지정하고 이 속성명이 View에서 사용될 속성명입니다!
  • BindingAdapter 함수의 매개변수는 어댑터를 적용할 View와 Data를 지정합니다.
//기존 뷰들에 없는 새로운 xml 속성을 연결하는 기능 메소드를 가지는 객체
//보통 static 메소드를 가진 class로 사용
object MyStoreBindingAdapter {  //static 메소드를 가져야하기 때문에 class면 안됨 object로 명시

    // 1) 이미지뷰에 새로운 xml 속성 만들기
    // [속성명 : myStoreImage ]
    @BindingAdapter("myStoreImage") //어노테이션 해독기 필요 - 빌드 그래이들에 기능 추가 필요!(kapt)
    fun imageBindingAdapter(view: ImageView, image: Bitmap){
        Glide.with(view.context).load(image).into(view)
    }
}

'PROJECT' 카테고리의 다른 글

안드로이드 클린 아키텍처 도메인 레이어 설계  (0) 2024.07.13
[PROJECT] MulterError: Unexpected field  (1) 2024.02.06
[PROJECT] Fused Location Provider  (1) 2024.01.28
[PROJECT]프로젝트 리팩토링  (0) 2024.01.16
'PROJECT' 카테고리의 다른 글
  • 안드로이드 클린 아키텍처 도메인 레이어 설계
  • [PROJECT] MulterError: Unexpected field
  • [PROJECT] Fused Location Provider
  • [PROJECT]프로젝트 리팩토링
빨주노초잠만보
빨주노초잠만보
  • 빨주노초잠만보
    과거의 나를 통해 미래의 나를 성장시키자
    빨주노초잠만보
  • 전체
    오늘
    어제
    • 분류 전체보기 (98)
      • TEKHIT ANDROID SCHOOL (4)
      • Android Architecture (7)
      • Android (42)
      • PROJECT (5)
      • KOTLIN (10)
        • 코루틴의 정석 (3)
      • BACK END (12)
      • CS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    callbackflow
    Room
    flow
    Repository Pattern
    value class
    Livedata
    orbit
    retrofi response
    의존성 주입
    DataSource
    2025 드로이드 나이츠
    retrofit interface
    ThrottleFirst
    parseannotations
    DI
    MVI
    retrofit parseannotations
    retrofit awit
    android clean architecture
    retrofit service interface
    repository
    Throttle
    sealed class
    STATEFLOW
    httpservicemethod
    android Room
    retrofit call
    코틀린 코루틴의 정석
    Clean Architecture
    retrofit coroutine
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
빨주노초잠만보
[PROJECT] 프로젝트에 DataBinding & @BindingAdapter 사용해보기
상단으로

티스토리툴바