반응형

 

이번 글은 데이터바인딩의 3번째 실습예제로서 사용자가 EditText에 글씨를 입력하고 버튼을 클릭하면 입력된 글씨를 TextView에 보여주는 예제입니다. 클릭이벤트 및 뷰의 데이터를 제어하는 가장 기본적이면서 어쩌면 앱 개발과정의 가장 핵심적인 기능구현이라고 생각합니다. 이 기능구현에 데이터바인딩을 적용해 보고자 합니다. 이전 포스트에서 소개했던 데이터바인딩의 특징을 다시한번 글로 정리해 보겠습니다.

 

기존의 방법은 EditText 객체를 찾아와 참조하여 입력된 글씨데이터를 얻어와서 TextView객체 참조변수를 이용하여 setText()로 직접 뷰에 데이터를 설정해주는 방법으로 기능을 구현했습니다. 

데이터 바인딩에서는 뷰객체를 참조하여 제어하지 않습니다. 뷰객체에 연결되어 있는 변수를 제어하여 뷰가 보여주는 값을 갱신하지요.

 

또한, 기존에는 클릭이벤트나 체크상태변경 이벤트, 또는 EditText의 글씨변경 리스너등을 Activity에서 설정하고 이벤트 콜백메소드를 만들어 뷰객체들에게 직접 데이터를 설정하는 등의 코드를 작성하였습니다. 그러다 보니 뷰가 많아지고 이벤트들이 많아지면 액티비티의 코드가 길어지고 거의 모든 코드가 액티비티 자바파일에서 작성되어 나중에는 어디에 어떤 메소드가 있는지 찾는 것도 어렵고 수정할때 그 위치를 찾는 것도 어려워 답답해 지는 경우가 많습니다.

자바의 객체지향을 학습하면서 많이 들어 보셨겠지만 객체지향을 위해 설계되는 class들은 그 이름에 걸맞는 변수와 기능메소드 들로 구성되어야 좋습니다. Random 클래스안에 말도 안되게 키보드 입력같은 기능메소드가 들어가 있거나, String 클래스안에 네트워트 작업 기능 메소드가 들어가 있으면 뭔가 이상하지 않나요? 클래스의 이름과도 어울리지 않고요.

 

안드로이드의 액티비티 class 역시 앱의 화면 UI를 담당하는 목적의 클래스 입니다. 이 안에서 액티비티가 보여주는 뷰를 참조하여 제어하는 코드를 작성하는 것는 것은 특별히 어색하지 않습니다. 그래서 그동안은 그렇게 뷰를 참조하고 뷰에 리스너를 달아주고 이벤트를 처리하였습니다.

하지만 데이터바인딩에서는 뷰를 참조하지 않습니다. xml 레이아웃파일에서 이미 뷰객체에 본인들이 제어할 데이터를 가진 변수와 연결되어 있습니다. 그렇기에 버튼을 클릭하는 콜백메소드를 액티비티가 아니라 데이터를 저장하는 변수가 있는 데이터클래스(User)에서 메소드를 만들었습니다. 데이터를 가진 클래스가 User인데 그 값을 변경하는 코드가 Activity에 있는 것보다는 User클래스에 있는것이 유지보수와 재사용성 면에서 훨씬 효과적일 겁니다. 이전 포스트에서 소개한 내용이지요.

 

이번 실습에서도 마찬가지로 EditText의 글씨가 입력되어 변경되는 것을 듣는 TextWatcher리스너의  콜백메소드인 onTextChanged를 User클래스에 콜백메소드로 만들어 입력된 글씨를 알아내고 그 글씨를 TextView에 곧바로 보여주도록 하겠습니다. 버튼을 클릭했을때 글씨를 얻어오는 코드는 이 다음에 이어서 소개하겠습니다. 즉, EditText에 글씨를 입력할때 마다 그 글씨를 TextView에 보여주도록 하겠습니다. 마치 자동으로 따라 써지는 것처럼요.

 

먼저, User 클래스에 TextView가 보여줄 데이터를 저장할 관찰가능한(Observable) 타입의 String 을 저장하는 msg 라는 이름의 참조변수와 객체를 만들어 주겠습니다. EditText의 글씨를 보여줄 TextView에 연결될 변수입니다.

또한 EditText 의 글씨변경에 반응하는 리스너인 TextWatcher의 콜백메소드인 onTextChanged()에 대응하는 메소드를 추가하겠습니다. 이전 글의 실습에 이어서 작업하겠습니다. 이 클래스의 가장 아래쪽 EditText 주석이 있는 부분부터 보시면 됩니다.

# User.java
import android.view.View;
import android.widget.CompoundButton;
import android.widget.Toast;

import androidx.databinding.ObservableBoolean;
import androidx.databinding.ObservableField;
import androidx.databinding.ObservableInt;

public class User {

    //값 변경이 관찰되는 멤버변수 ObservableXXX : primitive type XXX && List Or Map  && Reference type is ObservableField<>
    public ObservableField<String> name= new ObservableField<>();
    public ObservableInt age= new ObservableInt();
    public ObservableBoolean fav= new ObservableBoolean();


    //constructor
    public User(String name, int age, boolean fav) {
        this.name.set(name);
        this.age.set(age);
        this.fav.set(fav);
    }

    //change name click callback method : onclick속성에 반응하는 액티비티의 콜백메소드 작업을 데이터가 있는 이 클래스에서 코
    public void changeName(View view){
        name.set("ROBIN"); //Observable객체들의 값 변경은 반드시 set()을 해야 자동 값 갱신이 이루어짐 [ ObservableXXX 객체들은 어차피 = 대입연산자 사용 못함]
    }

    //increase age by click .. callback method
    public void increaseAge(View view){
        age.set( age.get() + 1 ); // ++연산이 불가하가이 get()으로 가져온 값에 1을 더한 값을 set()
    }

    //toggle fav by button click... callback method
    public void toggleFav(View v){
        fav.set(!fav.get());
    }

    //onCheckedChanged 일때 User객체의 fav값도 같이 변경하기 - 체크상태 변경 리스너의 콜백메소드를 그대로 만들
    public void changeFav(CompoundButton view, boolean isChecked){
        //fav의 값에 의해 체크박스의 체크상태는 자동반영되지만.. 반대로
        //체크박스의 체크상태를 변경했을때 fav멤버값이 자동으로 변경되지 않기에...직접 set
        fav.set(isChecked);
        //값이 제대로 변경되었는지 확인
        Toast.makeText(view.getContext(), "fav : " + fav.get(), Toast.LENGTH_SHORT).show();
    }


    //callback메소드가 아닌 일반 메소능 - 람다식을 이용하여 onClick에 반
    public void changeName(){
        this.name.set("HONG");
    }



    //EditText의 글씨 변경값을 저장하는 관찰되고 있는 멤버값
    public ObservableField<String> msg= new ObservableField<>();

    //EditText의 TextChanged event callback method... [ TextWatcher 클래스의 콜백메소드 - 파라미터 참고 ]
    public void onTextChanged(CharSequence s, int start, int before, int count){
        //변경될때 마다의 글씨를 TextView와 연결되어 있는 msg 변수에 설정
        msg.set(s.toString());
    }
    
}

 

이제, 레이아웃 xml 파일에서 EditText와 TextView를 추가하겠습니다. 이전 실습의 코드에 이어서 작업하겠습니다.

# activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Data Binding의 root는 <layout> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- 1. 레이아웃뷰와 바인딩할 데이터들 명칭과 클래스지정   -->
    <data>
        <!-- 데이터바인딩으로 연결할 User클래스 객체를 이 레이아웃에서는 user 라는 이름으로 참조하여 사용하겠다는 설정  -->
        <variable
            name="user"
            type="com.kitesoft.databindingjava.User" />

    </data>


    <!-- 2. 레이아웃 뷰 : 기존에 root로 만들었던 뷰   -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <!-- 텍스트뷰의 글씨를 <data>태에서 지정한 <variable>그 요소의 User객체의 멤버값 사용 : Observarble 멤버이기에 값변경이 관찰되어 자동 반영됨 -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{user.name}"/>

        <!-- int형 값인 user.age는 TextView에 text로 직접 설정이 안되기에 String.valueOf()메소드를 이용하여 문자열로 변환하여 설정 [문자열 결합 +""는 안됨]   -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{String.valueOf(user.age)}"/>

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="좋아요"
            android:checked="@{user.fav}"
            android:onCheckedChanged="@{user::changeFav}"/>
            <!-- onCheckedChanged="" : onClick 속성과 비슷하게 onChcekedChagedListener의 콜백메소드를 자동호출하는 속성 :자동완성 안됨 -->

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="change name"
            android:onClick="@{user::changeName}"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="increase age"
            android:textAllCaps="false"
            android:backgroundTint="@color/black"
            android:onClick="@{user::increaseAge}"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="change favorite"
            android:backgroundTint="@color/teal_700"
            android:onClick="@{user::toggleFav}"/>

        <!-- onclick속성에 발동하는 메소드 사용하지 않고 xml에서 바로 람다식으로 콜백메소드를 만들어 일반 메소드 호출가 -->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:backgroundTint="#FFFF3300"
            android:text="change name by lamda functioin"
            android:onClick="@{ (v)-> user.changeName() }"/>


        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="enter text"
            android:inputType="text"
            android:onTextChanged="@{user::onTextChanged}"/>
        <!-- onTextChanged=""속성- 자동완성은 안되지만 직접 작성하면 TetWatcher의 콜백메소드 호출 가능함  -->

        <!-- EditText의 입력글씨를 그대로 보여주는 TextView       -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{user.msg}"/>
            
    </LinearLayout>

</layout>

 

이제, 실행해보면 EditText에 글씨를 입력하여 변경될때 마다의 글씨가 바로 밑에 배치된 TextView에 그대로 보여질 겁니다. 

 

 

이번에는, 입력글씨가 변경될때마다 TextView의 글씨를 바꾸지 말고 버튼을 클릭하였을때 TextView에 글씨를 보여주도록 하겠습니다. 실제로는 더 많이 쓰여지는 상황입니다. EditText 뷰를 참조하지 않기 때문에 버튼을 클릭했을때 입력글씨를 얻어오는 식의 코드는 작성할 수 없습니다. 그럼 어떻게 해야 할까요? 특별한 문법이 있는 것은 아니고 조금 다른 방법으로 그렇게 동작하도록 구현해 낼겁니다.

 

EditText의 글씨가 변경될때 마다 TextView와 연결된 ObservableField<String> 객체인 msg 의 값을 set()하다보니 매번 화면이 갱신되면서 적용이 되니까, 굳이 msg의 매번 값을 설정하지 말고 버튼 눌렀을때 한번만 set()하면 되겠죠. 근데 그 때의 EditText 값을 읽어올 수 없으니 매번 글씨가 변경될 때마다 관찰되지 않는 일반 String 변수에 그 값을 저정할 겁니다. 글씨가 변경될 때 마다 발동하는 콜백메소드에서 EditText에 입력된 글씨를 일반 String 변수에 저장하고 버튼을 눌렀을때 마지막으로 저장된 String 변수값을 TextView가 보여주는 Observable 필드에 설정하면 그 순간 한번만 화면을 갱신하게 됩니다.

 

코드로 바로 소개하겠습니다. 위 실습의 User클래스에 이어서 코드를 작성하겠습니다.

# User.java
mport android.view.View;
import android.widget.CompoundButton;
import android.widget.Toast;

import androidx.databinding.ObservableBoolean;
import androidx.databinding.ObservableField;
import androidx.databinding.ObservableInt;

public class User {

    //값 변경이 관찰되는 멤버변수 ObservableXXX : primitive type XXX && List Or Map  && Reference type is ObservableField<>
    public ObservableField<String> name= new ObservableField<>();
    public ObservableInt age= new ObservableInt();
    public ObservableBoolean fav= new ObservableBoolean();


    //constructor
    public User(String name, int age, boolean fav) {
        this.name.set(name);
        this.age.set(age);
        this.fav.set(fav);
    }

    //change name click callback method : onclick속성에 반응하는 액티비티의 콜백메소드 작업을 데이터가 있는 이 클래스에서 코
    public void changeName(View view){
        name.set("ROBIN"); //Observable객체들의 값 변경은 반드시 set()을 해야 자동 값 갱신이 이루어짐 [ ObservableXXX 객체들은 어차피 = 대입연산자 사용 못함]
    }

    //increase age by click .. callback method
    public void increaseAge(View view){
        age.set( age.get() + 1 ); // ++연산이 불가하가이 get()으로 가져온 값에 1을 더한 값을 set()
    }

    //toggle fav by button click... callback method
    public void toggleFav(View v){
        fav.set(!fav.get());
    }

    //onCheckedChanged 일때 User객체의 fav값도 같이 변경하기 - 체크상태 변경 리스너의 콜백메소드를 그대로 만들
    public void changeFav(CompoundButton view, boolean isChecked){
        //fav의 값에 의해 체크박스의 체크상태는 자동반영되지만.. 반대로
        //체크박스의 체크상태를 변경했을때 fav멤버값이 자동으로 변경되지 않기에...직접 set
        fav.set(isChecked);
        //값이 제대로 변경되었는지 확인
        Toast.makeText(view.getContext(), "fav : " + fav.get(), Toast.LENGTH_SHORT).show();
    }


    //callback메소드가 아닌 일반 메소능 - 람다식을 이용하여 onClick에 반
    public void changeName(){
        this.name.set("HONG");
    }



    //EditText의 글씨 변경값을 저장하는 관찰되고 있는 멤버값
    public ObservableField<String> msg= new ObservableField<>();

    //EditText의 TextChanged event callback method... [ TextWatcher 클래스의 콜백메소드 - 파라미터 참고 ]
    public void onTextChanged(CharSequence s, int start, int before, int count){
        //변경될때 마다의 글씨
        msg.set(s.toString());
    }
    
    

    //EditText의 값 변경시마다의 글씨를 일반 멤버변수에 저장
    String titleStr="";

    public void changedTitle(CharSequence s){
        titleStr= s.toString();
    }

    //버튼 클릭시에 저장되어 있는 title변수의 값을 ObservableField<>의 변수값으로 설정
    public ObservableField<String> title= new ObservableField<>(""); //""값으로 초기화

    //버튼 클릭 콜백메소드
    public void clickBtn(View v){
        title.set(titleStr);
    }

    // 이렇게 데이터를 가지고 있고 데이터의 값을 변경하는 코드가 있는 클래스가 MVVM에서 MiewModel 의 역할을 하게됨.

}

 

마지막으로, 레이아웃 xml 파일에 EditText, Button, TextView 를 1개씩 추가하겠습니다.

# activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Data Binding의 root는 <layout> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- 1. 레이아웃뷰와 바인딩할 데이터들 명칭과 클래스지정   -->
    <data>
        <!-- 데이터바인딩으로 연결할 User클래스 객체를 이 레이아웃에서는 user 라는 이름으로 참조하여 사용하겠다는 설정  -->
        <variable
            name="user"
            type="com.kitesoft.databindingjava.User" />

    </data>


    <!-- 2. 레이아웃 뷰 : 기존에 root로 만들었던 뷰   -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <!-- 텍스트뷰의 글씨를 <data>태에서 지정한 <variable>그 요소의 User객체의 멤버값 사용 : Observarble 멤버이기에 값변경이 관찰되어 자동 반영됨 -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{user.name}"/>

        <!-- int형 값인 user.age는 TextView에 text로 직접 설정이 안되기에 String.valueOf()메소드를 이용하여 문자열로 변환하여 설정 [문자열 결합 +""는 안됨]   -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{String.valueOf(user.age)}"/>

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="좋아요"
            android:checked="@{user.fav}"
            android:onCheckedChanged="@{user::changeFav}"/>
            <!-- onCheckedChanged="" : onClick 속성과 비슷하게 onChcekedChagedListener의 콜백메소드를 자동호출하는 속성 :자동완성 안됨 -->

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="change name"
            android:onClick="@{user::changeName}"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="increase age"
            android:textAllCaps="false"
            android:backgroundTint="@color/black"
            android:onClick="@{user::increaseAge}"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="change favorite"
            android:backgroundTint="@color/teal_700"
            android:onClick="@{user::toggleFav}"/>

        <!-- onclick속성에 발동하는 메소드 사용하지 않고 xml에서 바로 람다식으로 콜백메소드를 만들어 일반 메소드 호출가 -->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:backgroundTint="#FFFF3300"
            android:text="change name by lamda functioin"
            android:onClick="@{ (v)-> user.changeName() }"/>


        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="enter text"
            android:inputType="text"
            android:onTextChanged="@{user::onTextChanged}"/>
        <!-- onTextChanged=""속성- 자동완성은 안되지만 직접 작성하면 TetWatcher의 콜백메소드 호출 가능함  -->

        <!-- EditText의 입력글씨를 그대로 보여주는 TextView       -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{user.msg}"/>


        <!-- EditText에 글씨 입력하고 버튼 클릭하면 그 글씨 텍스트뷰에 보이기  -->
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="input title"
            android:inputType="text"
            android:onTextChanged="@{(s,start,before,count) -> user.changedTitle(s)}"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="OK"
            android:onClick="@{user::clickBtn}"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{user.title}"/>

    </LinearLayout>

</layout>

EditText의 onTextChanged속성값으로 콜백함수를 바로 지정하지 않고 익명메소드를 람다식으로 지정하여 이벤트가 발생하면 람다식으로 표현된 함수가 호출되고 그 실행내역으로 user객체의 changeTitle()이라는 일반 메소드를 호출하면서 변경된 글씨를 가진 s를 파라미터로 전달하도록 해봤습니다. 바로 위 실습과 같이 콜백에 반응하지만 조금 다른 방식으로 소개해 봤습니다. 

 

보시다 시피 MainActivity.java는 전혀 건드리지 않았습니다. 실무에서는 이렇게 액티비티에 코드가 별로 없는 경우가 많습니다. 역할에 충실하게 코드들의 위치를 구분하여 작성하는 것이지요. 좋긴 하지만 초보분들에게는 오히려 한눈에 코드가 보이지 않아 더 복잡하게 느껴질 수도 있습니다.

 

처음에는 어색하지만 조금 익숙해지면 여러분들의 예상보다 훨씬 매력적인 기법이라는 것을 느끼실 수 있으실 겁니다.

미리 포기하지 말고 도전해 보시길 권해드립니다.

 

다음 데이터바인딩 관련 글로는 Fragment에서 데이터 바인딩을 적용하는 것을 소개하겠습니다.

반응형

+ Recent posts