본문 바로가기
Development/Android

[Android] Kotlin + MVVM + AAC 로 Todo 앱 만들기 - 6 (ViewBinding, DataBinding)

by du.it.ddu 2021. 1. 20.
반응형

한참전에 DataBinding을 적용하기로 했었는데, 까맣게 잊고 있었다.
내 포스팅을 보고 공부하시는 분께서 DataBinding을 요청하셔서 추가로 포스팅한다.
(게으른 나를 반성..)

귀찮으신 분들을 위한 최종코드 Github : github.com/DuItDDu/Android-Codelabs/tree/master/MyTodo


1. ViewBinding, DataBinding이 무엇인가?

developer.android.com/topic/libraries/view-binding

 

뷰 결합  |  Android 개발자  |  Android Developers

뷰 결합 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있습니다. 모듈에서 사용 설정된 뷰 결합은 모듈에 있는 각 XML 레이아웃 파일의 결합 클래스를 생성합니다. 바인딩 클래스의

developer.android.com

developer.android.com/topic/libraries/data-binding

 

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

데이터 결합 라이브러리 Android Jetpack의 구성요소. 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원

developer.android.com

Bind는 "묶다, 결합하다"의 의미를 갖고 있다. 
이것으로 미루어 보면 ViewBinding은 뷰를 묶는 것, DataBinding은 데이터를 결합하는 것으로 해석할 수 있다.

우리가 안드로이드를 개발하며 View와 상호작용하는 것을 생각해보자.

Java라면 버터나이프 혹은 findViewById 등을 사용했을 것이다.
Koltin을 사용하면서 'kotlin-android-extensions'를 적용하고 XML 상의 View의 id로 손쉽게 접근했을 것이다.

위 방법의 문제는 View를 찾는 과정이 내부적으로 복잡하고 성능이 떨어진다.
이러한 성능문제를 해결하고자 ViewHolder 패턴과 같은것도 등장하게 되었을 것이다.

ViewBinding은 이러한 문제를 해결하고 View와의 상호작용을 더 손쉽게 하는 방법이다.
더 나아가 DataBinding은 View의 텍스트나 이미지, 혹은 어떤 값을 변경할 때 Activity/Fragment/View의 Java, Kotlin 코드에서 복잡하게 처리하지 말고, XML상에 각 뷰에서 필요한 데이터를 결합하고 자동으로 반영되게 하는 방법이다.

또한 Android의 MVVM 패턴에서 DataBinding은 어쩌면 핵심이라고 볼 수도 있다.


2. ViewBinding, DataBinding 활성화

...
apply plugin: 'kotlin-kapt'

android {
    ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    buildFeatures {
        viewBinding = true
        dataBinding = true
    }
}

dependencies {
    ...

    implementation 'androidx.recyclerview:recyclerview:1.1.0'

    implementation "androidx.room:room-runtime:2.2.6"
    kapt "androidx.room:room-compiler:2.2.6"

    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
    implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"

    implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
}

App 레벨의 build.gradle로 이동하여 위 코드처럼 아래 항목들을 추가한다.

  • apply plugin: 'kotlin-kapt'
  • buildFeatures
    • viewBinding = true
    • dataBinding = true

그 외 compileOptions, kotlinOptions는 선택사항이다.
Kotlin을 사용할 경우 Java 1.8로 컴파일 하지 않으면 컴파일에 실패할 수 있다.

* dependencies도 꼭 확인하자.


3. MainActivity, activity_main.xml 수정

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

    <data>
        <variable
            name="viewModel"
            type="com.android.sample.mytodo.viewmodel.TodoViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
       ...

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

DataBinding을 적용하기 위해선 xml 코드를 <layout></layout> 태그로 감싸주어야 한다.
(* ViewBinding 만을 적용할 땐 필요하지 않다.)

그리고 데이터를 결합하기 위해 <data></data> 태그를 생성하고, 내부에 <variable> 태그를 사용하여 데이터를 정의해주고 결합할 데이터의 이름과 타입을 정의해준다.

<import> 태그도 있는데, 변수가 아닌 어떤 클래스가 필요한 경우에 활용한다.

사실 이 예제에서 TodoViewModel을 결합했지만 특별히 xml에서 데이터를 결합하지 않기 때문에 ViewBinding만 해도 무방하긴 하다.

# MainActivity

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var mDataBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initDataBinding()
        initViewModel()
        ...
        observeViewModel()
    }

    private fun initDataBinding() {
        mDataBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    private fun initViewModel() {
        mTodoViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(TodoViewModel::class.java).also { 
            mDataBinding.viewModel = it
        }
    }
    
    private fun observeViewModel() {
        mTodoViewModel.getTodoList().observe(this, Observer {
            mTodoListAdapter.setTodoItems(it)
        })
    }

    private fun initRecyclerView() {
        ...
        mDataBinding.rlTodoList.run {
            setHasFixedSize(true)
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = mTodoListAdapter
        }
    }
    ...
    private fun openAddTodoDialog() {
        val dialogView = DialogAddTodoBinding.inflate(LayoutInflater.from(this))
        val dialog = AlertDialog.Builder(this)
            .setTitle("추가하기")
            .setView(dialogView.root)
            .setPositiveButton("확인") { _, _ ->
                val title = dialogView.etTodoTitle.text.toString()
                val description = dialogView.etTodoDescription.text.toString()
                ...
            }
            ...
    }
    ...
}

위의 코드를 참고한다.

ViewBinding 혹은 DataBinding을 적용하면 컴파일 시 Binding 객체가 자동으로 생성된다.
생성된 객체의 이름은 규칙에 따라 XML의 이름을 파싱하여 만들어진다.
activity_main.xml 이라면 ActivityMainBinding 과 같이 만들어진다.
언더바(_)를 기준으로 Upper Camel Case로 이름을 생성하며 마지막에 Binding이 붙는 형태이다.
(Upper Camel Case는 우리가 흔히 클래스를 작성할 때 따르는 네이밍 규칙이다.)

DataBinding 객체를 생성할 땐 DataBindingUtil 클래스를 통해 생성해야 한다.

openAddTodoDialog함수에서 Dialog의 View를 생성할땐 ViewBinding이 사용되었다.
이전에 Dialog의 View를 생성할 땐 LayoutInflate에 dialog_add_todo.xml를 전달하였었다.
하지만 위에선 DialogAddTodoBinding 이란 Binding객체를 사용하였고, dialog_add_todo.xml은 DataBinding과는 달리 <layout></layout> 을 사용하지 않았다.


4. TodoListAdapter의 TodoViewHolder, item_todo.xml 수정

# item.todo.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.android.sample.mytodo.model.TodoModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/tv_todo_title"
            android:textSize="16dp"
            android:textColor="#000000"
            android:text="@{model.title}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/tv_todo_description"
            android:textSize="12dp"
            android:textColor="#808080"
            android:text="@{model.description}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_todo_title"/>

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/tv_todo_created_date"
            android:textSize="10dp"
            android:textColor="#cccccc"
            app:todoDate="@{model.createdDate}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_todo_description"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

item_todo.xml에서 DataBinding이 유용하게 사용되었다.

item_todo.xml은 TodoModel을 표현하기 위한 레이아웃으로, TodoModel과 데이터를 결합할 수 있다.
위 코드에서 <variable> 태그를 사용하여 TodoModel 타입의 변수를 "model" 이란 이름으로 정의한 것을 볼 수 있다.

이제 item_todo.xml에서 TodoModel 타입의 model 변수를 사용할 수 있다. 마치 Java나 Kotlin 코드처럼.
다만, 위에서 볼 수 있듯이 변수를 사용할 땐 @와 {} 로 감싸주어야 한다.

여기서 마지막 TextView에 app:todoDate=@{model.createDate}를 확인할 수 있는데, todoDate란 속성은 어디에도 없다.
이는 BindingAdapter 라는 것을 사용하여 직접 만들어준 것인데, 이 부분은 TodoAdapter 코드에서 설명하겠다.

 

# TodoListAdapter

class TodoListAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
   ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return DataBindingUtil.inflate<ItemTodoBinding>(
            LayoutInflater.from(parent.context),
            R.layout.item_todo,
            parent,
            false
        ).let {
            TodoViewHolder(it, listener)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as? TodoViewHolder)?.bind(todoItems.getOrNull(position) ?: return)
    }

    class TodoViewHolder(
        private val binding: ItemTodoBinding,
        listener: OnTodoItemClickListener?
    ): RecyclerView.ViewHolder(binding.root) {
        init {
            itemView.setOnClickListener {
                listener?.onTodoItemClick(adapterPosition)
            }

            itemView.setOnLongClickListener {
                listener?.onTodoItemLongClick(adapterPosition)
                return@setOnLongClickListener true
            }
        }

        fun bind(todoModel: TodoModel) {
            binding.model = todoModel
            binding.executePendingBindings()
        }
    }
}

@BindingAdapter("todoDate")
fun setTodoDate(tv: TextView, date: Long) {
    val simpleDateFormat = SimpleDateFormat("yyyy.MM.dd HH:mm", Locale.getDefault())
    tv.text = simpleDateFormat.format(date)
}

RecyclerView의 ViewHolder에서 DataBinding을 사용하기 위해선 여러 방법이 있지만, 나는 아래와 같은 방식을 사용하며, 코드는 위를 참고하면 된다.

  • Adapter는 onCreateViewHolder에서 ViewHolder 생성 시 binding 객체를 생성한다.
  • ViewHolder는 생성자로 binding 객체를 전달받는다.

RecyclerView.ViewHolder를 상속하는 경우, 부모의 생성자에 View를 넘겨주어야 한다. 이때, 위 코드처럼 Binding 객체의 root를 넘겨주면 된다.

그리고 ViewHolder 내부에선 binding 객체를 사용하여 View와 상호작용하면 된다.
ViewHolder 전체에 클릭 이벤트가 필요한 경우, itemView, Binding.root를 사용하는 방법 등 다양하다.

그리고 xml에 정의된 변수는 Binding 객체의 속성으로 접근할 수 있다. (위의 경우 TodoModel 타입의 model)
외부에선 결합할 데이터를 Binding 객체를 사용하여 전달해주면 된다.

결합된 데이터를 변경한 경우, Binding객체의 executePendingBindings() 메서드를 호출하는 것이 좋다.
그렇지 않으면 데이터가 변경되어도 View에 반영이 되지 않을 수 있다.

이제 BindingAdapter에 대해 이야기 하겠다.

<TextView
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:id="@+id/tv_todo_created_date"
    android:textSize="10dp"
    android:textColor="#cccccc"
    app:todoDate="@{model.createdDate}"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/tv_todo_description"/>
            
@BindingAdapter("todoDate")
fun setTodoDate(tv: TextView, date: Long) {
    val simpleDateFormat = SimpleDateFormat("yyyy.MM.dd HH:mm", Locale.getDefault())
    tv.text = simpleDateFormat.format(date)
}

TextView엔 없던 속성인 "todoDate"는 위와 같이 Adapter 코드 내에 정의되어 있다.

BindingAdapter란, DataBinding을 하기 위해서 커스텀한 속성을 만들 때 사용한다.
데이터를 별도의 가공없이 보여줄 수 있지만, 시간이라던가 가격이라던가 가공이 필요한 경우 사용한다.

위의 경우는 시간값이 밀리세컨드인 Long타입이고, 이를 yyyy.MM.dd HH:mm 형식으로 보여주기 위해 BindingApdater를 정의했다.

@BindingAdapter 어노테이션을 사용하고, XML에서 사용할 속성을 어노테이션에 정해준다.
그리고 함수를 작성한다. XML에선 어노테이션에 정해준 속성의 이름으로 접근이 가능하며, 정의된 함수를 실행시킨다.



ViewBinding, DataBinding에 대해 설명하다 보니 글이 많아졌다. 읽기 싫을듯..

현재 View의 처리를 위해 권장하는 것은 위와 같이 Binding을 활용하는 것이며, 'kotlin-android-extensions'를 활용하는 것은 Deprecated 되었고 권장하지 않는다.

XML에 코드를 작성하는 것이 익숙하지 않아 처음엔 어려우며, 데이터 결합 방식이 복잡하거나 연산이 필요하면 어려울 수 있다.
또한 디버깅이 어렵다는 단점도 가지고 있다. (XML에 로그를 심을 수 없으니.)

하지만 이 방식에 적응되고 개발 이후 더 손볼 일이 없게 되면 코드도 깔끔해지고 편리한 점이 많다.
무엇보다 코드상에서 View의 속성을 변경하는 것은 상태가 많아지고 복잡할수록 누락되고 관리가 어려울 수 있는데, DataBinding을 활용하면 이런 부분도 해소할 수 있다.

 

반응형