코틀린 익스텐션으로 findViewById
를 사용하지 않아도 쉽게 레이아웃 파일에 선언한 id를 쉽게 가져올 수 있었습니다.
하지만 레이아웃에서 선언한 View가 코틀린 파일의 객체를 읽고 데이터를 출력해주지 않습니다. 코틀린에서 직접 데이터를 View에 설정해줘야 합니다.
DataBinding은 코틀린에서 레이아웃을, 레이아웃에서 코틀린의 데이터를 직접 참조하는 라이브러리입니다. 코틀린에서 레이아웃 파일에 의존적인 부분이 많이 사라지고 MVVM 등의 패턴과 함께 사용됩니다.
One-way Binding과 Two-way Binding
DataBinding에서 One-way binding
와 Two-way binding
라는 용어가 있습니다.
One-way binding
은 데이터의 흐름이 일방향을 말합니다. 만약 Youtube 리스트를 서버에서 받아와서 화면에 보여준다면 데이터의 흐름은
Code -> View
으로 일방향입니다.
Two-way binding
이라는 용어는 데이터 흐름이 양방향인데요. 위의 예제에서, 사용자가 'android'를 검색하면 android 관련 Youtube 리스트만 보여주도록 구현해야 한다면 양방향 바인딩으로 구현할 수 있습니다.
데이터 흐름은 Code -> View
, View -> Code
로 양방향입니다.
프로젝트 생성
이전 글 AndroidX RecyclerView 구현(kotlin)에서 만든 프로젝트를 DataBinding을 사용하여 리팩토링을 해보겠습니다.
프로젝트는 GitHub에서 다운받을 수 있습니다.
Gradle
의존성에 따로 추가할 내용은 없습니다. 하지만 App Gradle의 Android 태그 아래에 DataBinding 및 Java8을 사용하도록 옵션을 추가해야 합니다.
android {
compileSdkVersion 28
.....
dataBinding {
enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
RecyclerView
먼저 RecyclerView만 DataBinding으로 리팩토링하겠습니다.
먼저 xml파일의 형식을 조금 변경해야 합니다.
DataBinding에서 사용되는 레이아웃 파일들의 Root 태그는 <layout/>
이 되어야 합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" ...>
....
</layout>
그리고 코틀린의 어떤 객체를 참조하여 데이터를 출력할 지 <data>
를 정의해줘야 합니다.
youtubeItem
의 경우 YoutubeItem
객체를 참조하겠다는 내용입니다.
<data>
<variable
name="youtubeItem"
type="com.codechacha.sample.data.YoutubeItem"/>
</data>
데이터를 선언했으면, @{youtubeItem.title}
처럼 데이터를 View에 참조하도록 연결할 수 있습니다.
<TextView
...
android:textSize="20sp"
android:text="@{youtubeItem.title}"
리스너의 경우 @{youtubeItem::onClickListener}
처럼 설정이 가능합니다.
대신 YoutubeItem에 onClickListener
함수를 생성해야 합니다.
<androidx.constraintlayout.widget.ConstraintLayout
...
android:onClick="@{youtubeItem::onClickListener}">
수정된 list_item.xml
파일은 다음과 같습니다.
/res/layout/list_item.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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="youtubeItem"
type="com.codechacha.sample.data.YoutubeItem"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:background="?android:attr/selectableItemBackground"
android:onClick="@{youtubeItem::onClickListener}">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:adjustViewBounds="true"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@{youtubeItem.title}"
app:layout_constraintTop_toBottomOf="@+id/thumbnail"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
그리고 루트 패키지에 adapters
, data
폴더를 생성해주세요.
기존의 YoutubeItem.kt
는 data
폴더로 옴겨주시고 RecyclerAdapter.kt
는 adapters
폴더로 옴겨주세요.
BindingAdapter.kt
는 나중에 구현할 파일인데 미리 빈파일만 만들어주세요.
위의 작업이 끝난 후, 프로젝트 구조는 다음과 같습니다.
OnClickListener
리스너를 코틀린에서 직접 설정하지 않고 레이아웃 파일에서 설정하였습니다. YoutubeItem의 onClickListener로 설정했기 때문에 구현을 해줘야 합니다. xml에서 직접 리스너로 onClickListener를 설정하였기 때문에 View가 클릭이 되면 저 함수가 호출되면서 토스트가 발생합니다.
YoutubeItem.kt
class YoutubeItem(val image: Drawable, val title: String) {
fun onClickListener(view: View) {
Toast.makeText(view.context, "Clicked: $title", Toast.LENGTH_SHORT).show()
}
}
RecyclerAdapter
레이아웃 파일에서 RecyclerView
가 어떤 데이터를 참조할지 설정하였고, 이제 코틀린에서 DataBinding관련 설정을 하면 됩니다.
이전에는 RecyclerAdapter.kt
의 onCreateViewHolder
에서 ViewHolder
를 생성할 때 인자로 View를 넘겨줬습니다.
지금은 ListItemBinding
객체를 생성해서 넘겨주도록 변경하였습니다.
ListItemBinding
는 DataBinding에서 자동으로 생성해주는 클래스인데요.
기본적으로 Binding 클래스 이름은 레이아웃 파일의 이름을 파스칼 표기법(Pascal Case: 합성어의 첫 글자를 대문자로 표기)으로 변경하고 Binding을 접미사로 붙여줍니다.
따라서 ListItemBinding
는 list_item.xml
에서 underbar 제거 및 파스칼표기법으로 변경하고 마지막에 Binding을 붙여 생성되었습니다.
ListItemBinding
의 역할은 특정 View에 코틀린 코드와 레이아웃을 바인딩하며, View에 접근할 수 있는 객체입니다.
이전 코드와 비교해보면 xml을 inflate하는 것은 동일하지만, DataBindingUtil을 사용하여 ListItemBinding으로 넘겨주는 것이 차이점입니다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder {
return ViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context), R.layout.list_item, parent, false
)
)
}
그럼 ViewHolder의 인자도 ListItemBinding로 변경해야 합니다.
list_item.xml
에서 youtubeItem
에 실제 데이터를 입력해줘야 합니다.
ListItemBinding
으로 View에 접근할 수 있다고 했었는데요.
binding.youtubeItem
처럼 접근할 수 있고 여기에 데이터를 입력해주면 View 내에서 저 값을 참조하여 출력을 합니다.
이전 코드와 비교해보면, Listener를 설정하는 코드는 제거하였는데요. list_item.xml
에서 직접 listener를 설정해주었기 때문에 삭제했습니다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.apply {
bind(item)
itemView.tag = item
}
}
class ViewHolder(
private val binding: ListItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: YoutubeItem) {
binding.apply {
youtubeItem = item
}
}
}
수정된 RecyclerAdapter.kt
는 다음과 같습니다.
/adapter/RecyclerAdapter.kt
class RecyclerAdapter(private val items: ArrayList<YoutubeItem>) :
RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.apply {
bind(item)
itemView.tag = item
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder {
return ViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context), R.layout.list_item, parent, false
)
)
}
class ViewHolder(
private val binding: ListItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: YoutubeItem) {
binding.apply {
youtubeItem = item
}
}
}
}
이제 실행해보세요. 타이틀은 보이지만 이미지가 보이지 않을 거에요.
왜냐면 위에서 list_item.xml
수정할 때 이미지에 대한 참조를 설정하지 않았거든요.
TextView는 @{youtubeItem.title}
로, YoutubeItem의 title 객체를 참조하도록 하였는데요.
ImageView는 이렇게 설정하기 힘들었습니다. title은 문자열인 반면에 image는 Drawable로 사용하고 있거든요.
ImageView에 resourceId를 입력하는 속성은 있지만 Drawable을 입력하는 속성은 없어서 설정할 수 없었습니다.
<TextView
...
android:text="@{youtubeItem.title}"
이런 이유로, ImageView에서 Drawable을 받는 속성을 만들어주어야 합니다.
아까 /adapters/BindingAdapter.kt
를 만들었는데요.
여기에 다음 코드를 추가해주세요.
@BindingAdapter("imageDrawable")
fun bindImageFromRes(view: ImageView, drawable: Drawable?) {
view.setImageDrawable(drawable)
}
이 코드는 ImageView에서 app:imageDrawable
속성을 사용할 수 있게 합니다.
함수 이름은 중요하지 않고 @BindingAdapter
라는 annotation의 인자로 설정된 이름이 중요합니다.
이 이름으로 ImageView에서 사용할 수 있습니다. 함수의 인자들도 중요한데요.
view는 ImageView를 의미하고, drawable은 YoutubeItem의 image를 말합니다.
ImageView
에 app:imageDrawable
속성을 추가하고 @{youtubeItem.image}
를 넣어주세요.
그러면 위에서 구현한 BindingAdapter에서 이 객체를 읽고 ImageView에 설정해줍니다.
<ImageView
...
app:imageDrawable="@{youtubeItem.image}"
실행해보세요. 이제 이미지도 보입니다.
MainActivity
이제 MainActivity도 DataBinding으로 변경해보죠.
먼저 xml을 변경해주세요.
여기서는 코틀린의 데이터를 참조할 것이 없어서 <data>
는 선언하지 않았습니다.
/layout/activity_main.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"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/list_item"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MainActivity.kt
에서 ActivityMainBinding을 생성하면, 이 binding객체를 통해서 recyclerView
를 접근할 수 있습니다.
전체적인 코드는 다음과 같습니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this,
R.layout.activity_main)
val list = ArrayList<YoutubeItem>()
list.add(YoutubeItem(getDrawable(R.drawable.image01)!!, getString(R.string.title01)))
list.add(YoutubeItem(getDrawable(R.drawable.image02)!!, getString(R.string.title02)))
list.add(YoutubeItem(getDrawable(R.drawable.image03)!!, getString(R.string.title03)))
list.add(YoutubeItem(getDrawable(R.drawable.image04)!!, getString(R.string.title04)))
list.add(YoutubeItem(getDrawable(R.drawable.image05)!!, getString(R.string.title05)))
list.add(YoutubeItem(getDrawable(R.drawable.image06)!!, getString(R.string.title06)))
list.add(YoutubeItem(getDrawable(R.drawable.image07)!!, getString(R.string.title07)))
list.add(YoutubeItem(getDrawable(R.drawable.image08)!!, getString(R.string.title08)))
list.add(YoutubeItem(getDrawable(R.drawable.image09)!!, getString(R.string.title09)))
list.add(YoutubeItem(getDrawable(R.drawable.image10)!!, getString(R.string.title10)))
val adapter = RecyclerAdapter(list)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
}
}
실행해보세요. 이전과 동일하게 동작합니다.
정리
DataBinding 라이브러리를 이용하면 데이터가 변할 때 추가 코드 없이 View에 자동으로 반영되도록 할 수 있습니다. 일반적인 방식보다 추가 코드도 들어가고 구조가 복잡하다고 느낄 수 있는데요. View와 구현부의 의존성이 줄어드는 것이 장점입니다. 이런 장점으로 코드의 재활용 또는 테스트 가능한 코드를 짜는데 이전보다 쉬워질 수 있습니다.
참고
- 샘플 코드는 GitHub에 있습니다
- Data binding - Android developer
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력