- 이번엔 DataBinding과 BindingAdapter를 사용해 Room에서 가져온 이미지를 ImageView에 보여주는 연습을 해보겠습니다. 이 글의 예제 코드들은 제가 진행 중인 프로젝트에서 가져온 코드라 다소 뜬금없는 부분이 있을 수 도 있습니다.
🎉 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를 바인딩
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 (0) | 2024.01.28 |
[PROJECT]프로젝트 리팩토링 (0) | 2024.01.16 |