Hanbit the Developer

[Kotlin] Android DataBinding 예제(+ 양방향 데이터 바인딩, 클릭 이벤트, 삼항 연산자) 본문

Mobile/Android

[Kotlin] Android DataBinding 예제(+ 양방향 데이터 바인딩, 클릭 이벤트, 삼항 연산자)

hanbikan 2022. 2. 10. 15:45

 서론

안드로이드 어플을 구현하다보면, 뷰와 데이터 간의 일관성을 위해 의존성을 주입하거나 로직을 복잡하게 만들어야 하는 경우가 상당히 많았다. 하지만, 데이터바인딩을 사용하면 뷰에 들어가는 값이 항상 뷰모델의 값과 일치하게 하도록 할 수 있다. 값을 임의로 증가시켜도 뷰를 따로 업데이트해줄 필요가 없다.

게다가 뷰모델은 액티비티나 프래그먼트의 수명주기 전체를 scope로 두기 때문에, 화면을 회전하는 등의 행위가 있어도 따로 신경쓰지 않아도 된다.

그리고 데이터바인딩 라이브러리를 이용하면, 뷰의 클릭 이벤트(setOnClickListener)를 액티비티나 프래그먼트 클래스 파일 내에 지정하지 않아도 된다. xml 파일 내에 클릭 이벤트를 지정해줄 수 있기 때문이다.

 

 액티비티 구성

맨 위는 +, - 버튼을 눌러서 count 값을 조절할 수 있는 영역이고, 가운데 박스는 뷰모델에 실시간으로 값이 양방향으로 전달되는 단순한 EditText이며, 밑에 있는 10!은 count 값이 10 이상일 때만 보여지는 TextView이다. 어떤 목적을 위해 설계된 어플리케이션이라기보단, 이 글에서 다룰 핵심 요소들을 설명하기 위한 수단에 불과하다.

 

 build.gradle

android {
	...

	buildFeatures {
		viewBinding true
		dataBinding true
	}
    
	...
}

 

 MainActivity.kt

package com.example.mvvmapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.databinding.DataBindingUtil
import com.example.mvvmapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.lifecycleOwner = this
        binding.viewModel = viewModel
    }
}

코드는 이걸로 끝이다. 데이터 바인딩 라이브러리의 너무나도 강력한 장점 덕분이다.

여기서 binding와 DataBindingUtil.setContentView()의 타입이 일치하지 않는다는 오류가 뜨게 된다. 사실 데이터바인딩을 위해 레이아웃을 <layout> 요소로 감싸주어야하는데 이것을 하지 않았기 때문이며, 구체적인 사항은 activity_main.xml 항목에 설명되어있다.

특히 binding.viewModel이라는 것은 xml 파일에서 쓸 <data> 항목과 연관이 있다는 것에 유의한다. <data> 요소는 간단히 말해서 xml 파일에서 참조할 수 있는 변수를 말한다.

 

 MainViewModel.kt

package com.example.mvvmapplication

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel: ViewModel() {
    var count = MutableLiveData<Int>()
    var editText = "Remember Me!"

    init {
        count.value = 0
    }

    fun increaseCount() {
        count.value = count.value?.plus(1)
    }

    fun decreaseCount() {
        count.value = count.value?.minus(1)
    }
}

 

count를 MutableLiveData로 선언함으로써 이 값이 변경될 때마다 observe 될 수 있는 것이고 결과적으로 데이터 바인딩이 이루어질 수 있다.

 

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>
        <import type="android.view.View"/>
        <variable
            name="viewModel"
            type="com.example.mvvmapplication.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="36sp"
            android:text="@{Integer.toString(viewModel.count)}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"/>


        <Button
            android:id="@+id/button_plus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{()->viewModel.increaseCount()}"
            android:text="+"
            app:layout_constraintTop_toBottomOf="@id/text"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@id/button_minus"
            />
        <Button
            android:id="@+id/button_minus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{()->viewModel.decreaseCount()}"
            android:text="-"
            app:layout_constraintTop_toBottomOf="@id/text"
            app:layout_constraintLeft_toRightOf="@id/button_plus"
            app:layout_constraintRight_toRightOf="parent"
            />

        <EditText
            android:id="@+id/edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={viewModel.editText}"
            app:layout_constraintTop_toBottomOf="@id/button_plus"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="72sp"
            android:text="10!"
            android:visibility="@{viewModel.count >= 10 ? View.VISIBLE : View.GONE}"
            app:layout_constraintTop_toBottomOf="@id/edit_text"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

위 코드에서 핵심을 짚어보자.

 

우선 아래와 같이 최상위 레이아웃에 <layout>이 들어가야하며, 내부에는 data 요소와 view 요소가 순차적으로 들어있다.

<import> 요소는 후설할 예정이다.

특히 data 내부의 variable 요소는, 이후에 쓰일 @{} 구문을 위해 필요하다. xml에서 사용할 특수한 변수를 선언한 것이라고 보면 된다. 이 때 name 속성에 해당하는 viewModel은 이후에 다음과 같이 쓰이게 된다.

 

 - 데이터 바인딩

android:text="@{Integer.toString(viewModel.count)}"

위 코드는 viewModel의 count 변수와 데이터바인딩 하겠다는 의미이다.

 

 - 클릭 이벤트

android:onClick="@{()->viewModel.increaseCount()}"

 

 

클릭 시 다음과 같은 순서로 데이터 바인딩이 진행된다.

 

 

 

 - 양방향 데이터 바인딩

사용자가 뷰를 이용하여 직접 값을 변경할 수 있는 TextEdit이나 CheckBox의 경우, 사용자에 의해 뷰의 내용이 변경될 시 그 내용이 ViewModel에 전달되지 않는다. 위에서 TextView에 적용되어있는 것을 그대로 EditText에도 적용했다고 해보자. 이 때 A라는 초기값을 B로 변경한 뒤에 화면을 회전시키면 수정한 값은 날아가고 A가 다시 들어오게 된다. 수정된 값이 ViewModel에 전달되지 않았기에 데이터 바인딩으로 인해 그렇게 된 것이다. 이를 위해 있는 것이 @={} 구문이다.

android:text="@={viewModel.editText}"

 

이렇게 하면 값을 유저가 수정하면 그것이 곧바로 뷰모델에 업데이트 된다.

 

 - 삼항 연산자

삼항 연산자를 이용하여 count가 10 이상일 때만 VISIBLE이 되는 TextView를 설계해보자. 아래 코드를 넣어주면 된다.

android:visibility="@{viewModel.count >= 10 ? View.VISIBLE : View.INVISIBLE}"

사실 이렇게만 하면 View를 참조할 수 없어서 에러가 뜨게 된다. <data /> 요소 내부에 View를 import 해주어야 정상적으로 사용이 가능하다.

<data>
        <import type="android.view.View"/>
        <variable
            name="viewModel"
            type="com.example.mvvmapplication.MainViewModel" />
    </data>

 

설계한 대로 count가 10 이상일 때만 보여지는 TextView가 완성된다.

 

참고로 조건문 내부에 &&을 쓸 상황이 생길 수도 있는데, 이 때는 다음과 같이 이스케이핑 문자열을 사용하여야 한다.

android:visibility="@{viewModel.a == 0 &amp;&amp; viewModel.b == 0? View.VISIBLE : View.INVISIBLE}"

 

 

 

참고 자료

 

https://stackoverflow.com/questions/37152824/android-databinding-using-logical-operator

 

android databinding using "&&" logical operator

I am trying to use the and "&&" operator in xml using Android databinding, android:visibility="@{(bean.currentSpaceId == bean.selectedSpaceId **&&** bean.currentSpaceId > 0)? V...

stackoverflow.com

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

 

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

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

developer.android.com