박상권의 삽질블로그

[안드로이드/Android]DataBinding - Goodbye 버터나이프 Hello 데이터바인딩 본문

IT/Android-TIP (한글)

[안드로이드/Android]DataBinding - Goodbye 버터나이프 Hello 데이터바인딩

박상권 2017. 2. 19. 20:05

안드로이드 개발자들이 모여있는 오픈채팅방에 참여해보세요 .
Q&A 및 팁을 공유하는 방입니다..
오픈채팅방 참여


블로그를 Medium으로 옮겨서 운영하고 있습니다.
앞으로 새로운 글은 모두 미디엄 블로그를 통해서 올릴 예정입니다.
미디엄에서 다양하고 유익한 포스팅을 살펴보세요
미디엄 블로그 보기


이번에 '클린 아키텍처'를 주제로 온라인 강의를 개설하게 되었습니다 🎉
평소 오픈채팅방이나 여러 커뮤니티에서 '클린 아키텍처'와 관련된 질문들이 많았는데요. 이를 해결해줄 수 있는 마땅한 강의가 없었던것 같습니다.
평소 '클린 아키텍처' 에 대한 궁금증이나 관심이 있으셨던 분들이 수강해보시면 도움이 될것 같아요
강의 살펴보기



4줄 요약

1. 데이터바인딩을 사용하면 findViewById() 안해도 자동으로 xml에 만든 View들을 만들어 준다.

2. RecyclerView에서 각각의 item을 set해주는 작업도 xml에서 다 써주면 알아서 값이 들어간다.

3. 값이 바뀌면 알아서 바뀐값으로 View를 변경하게 할수도 있고 기타 등등 유용하게 활용 할만한게 많다.

4. 샘플프로젝트 를 실행해보고 소스코드 보면 직접 느낄수 있다




자 여러분 눈을 감고 상상해보세요

아, 눈을 감으면 이글을 못읽으니 눈은 뜨고 상상해보세요


새로운 화면을 추가해야 합니다.

안드로이드 스튜디오를 켭니다.

그다음에 무엇을 해야할까요?

네 맞습니다.

xml을 만들고 화면 레이아웃을 구성해야합니다.(답정너)



아래와 같은 레이아웃을 만들었다고 예를 들어보겠습니다.

- TextView 5개

- Button 1개

- RecyclerView 1개

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:id="@+id/tvText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tvText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tvText3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tvText4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tvText5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button
android:id="@+id/btnSample"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="button"
/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rcContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>



xml을 만들었으니 이제 Activity에서 View를 이어주어야 겠죠?

TextView tvText1;
TextView tvText2;
TextView tvText3;
TextView tvText4;
TextView tvText5;
Button btnSample;

RecyclerView rcContent;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.basic_activity);

tvText1 = (TextView) findViewById(R.id.tvText1);
tvText2 = (TextView) findViewById(R.id.tvText2);
tvText3 = (TextView) findViewById(R.id.tvText3);
tvText4 = (TextView) findViewById(R.id.tvText4);
tvText5 = (TextView) findViewById(R.id.tvText5);
btnSample = (Button) findViewById(R.id.btnSample);
rcContent = (RecyclerView) findViewById(R.id.rcContent);

제일먼저 우리는 아무생각없이 findViewById() 노가다를 해줍니다.

이 레이아웃은 간단한 샘플이기 때문에 몇개 안되는 뷰이지만

만약 10개,100개 그리고 그 이상이라면 그만큼 노가다코드가 필요할 것입니다.



그래서 우리는 버터나이프(Butterknife)를 사용해왔습니다.

[IT/Android-TIP (한글)] - [안드로이드]유용한 라이브러리 - Butter Knife (View Inject)



버터나이프를 사용한다면 우리는 조금이나마 findViewById()노가다를 줄일 수 있습니다.

@BindView(R.id.tvText1)
TextView tvText1;
@BindView(R.id.tvText2)
TextView tvText2;
@BindView(R.id.tvText3)
TextView tvText3;
@BindView(R.id.tvText4)
TextView tvText4;
@BindView(R.id.tvText5)
TextView tvText5;
@BindView(R.id.btnSample)
Button btnSample;

@BindView(R.id.rcContent)
RecyclerView rcContent;


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.butterknife_activity);
ButterKnife.bind(this);

버터나이프를 알게되면서 저는 행복했습니다.

소스코드의 양을 반으로 줄여 주었으니까요.

사랑합니다 Jake Wharton, 물론 전 남자를 좋아하지는 않습니다.





하지만 데이터바인딩(Databinding)을 사용한다면 어떨까요?










위의 화면은 오류가 아닙니다.

따로 View를 정의하고 이어줄 필요가 없습니다.

Databinding을 사용한다면 우리는 아무 소스코드를 적지 않아도 Databinding이 알아서 다 해줍니다.











최소사항

  • Android 2.1(API레벨 7) 이상

  • Android Plugin for Gradle 1.5.0-alpha1 이상

  • Android Studio 1.3 이상

아주 옛날의 환경으로 작업하고 있지 않는한 지금쓰는 환경에서 바로 사용할 수 있습니다.






설정


build.gradle (Module:app)

android{

//...

dataBinding {
enabled = true
}

}

설정은 gradle에서 enable만 true로 설정해주시면 끝납니다.

참 간단하죠?








xml


Databinding을 사용하기 위해서는 xml의 최상위에 <layout>태그를 감싸줍니다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<!--
...
-->

</LinearLayout>

</layout>





Java


Activity에서 항상 해주던 setContentView()대신에 DataBindingUtil.setContentView()를 이용해서 layout xml을 이어줍니다.

DatabindingActivityBinding binding;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = DataBindingUtil.setContentView(this, R.layout.databinding_activity);
binding.setActivity(this);

xml의 파일이름을 기준으로 XXXBinding 클래스가 새로 생성됩니다.

여기서는 xml의 이름이 databinding_activity.xml 이기때문에 자동으로 DatabindingActivityBinding 클래스가 새로 생성되었습니다.

Binding클래스 이름의 생성은 파스칼표기법 기준으로 변경됩니다.

예를들어 ted_park_activity.xml 파일은 TedParkActivityBinding 클래스를 생성시킵니다.




View를 사용하고자 할때는 binding변수에서 아래와 같이 사용할 수 있습니다. 

View의 이름은 카멜표기법 기준으로 변경됩니다.

@+id/tv_text_name 의 뷰는 tvTextName으로 이름이 변경되어 사용할 수 있습니다.










Click Event


사용자가 어떤 버튼을 클릭하면 아래 함수를 실행하도록 하려고 합니다.

public void onButtonClick(View view){
Toast.makeText(this,"Button Click",Toast.LENGTH_SHORT).show();
}





기본


버튼에 setOnClickListener()를 달아서 클릭이벤트를 받아주도록 할것 입니다. 

//btnSample.setOnClickListener(this);
btnSample.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onButtonClick(view);
}
});

setOnClickListener(this)를 하셨다면 implements View.OnClickListener 를 하셔서 onButtonClick()함수를 호출 하셨을겁니다.









버터나이프


버터나이프에서는 @OnClick(R.id.xxx) 만 함수에 넣어주면 됩니다.

@OnClick(R.id.btnSample)
public void onButtonClick(View view) {
Toast.makeText(this, "Button Click", Toast.LENGTH_SHORT).show();
}

정말 간단합니다.
버터나이프를 왜 사용해야하는지에 대한 여러가지 이유중에 하나이기도 합니다.









데이터바인딩


데이터바인딩은 xml에서 클릭되면 실행하고자하는 함수를 지정해줍니다.

<Button
android:id="@+id/btnSample"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{activity::onButtonClick}"
android:text="button"
/>

위에서는 android:onClick에서 activity라는 곳에 있는 onButtonClick()을 실행하도록 설정 해두었습니다.

여기에서 activity라는것은 xml에서 선언해둔 변수입니다.


<layout xmlns:android="http://schemas.android.com/apk/res/android"
>
<data>
<variable
name="activity"
type="gun0912.databinding.compare.databinding.DataBindingActivity"/>
</data>

xml에서 <data>태그를 이용해서 그안에 각각 사용하고자하는 변수를 <variable>로 정의해주면 사용 할 수 있습니다.


DatabindingActivityBinding binding;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = DataBindingUtil.setContentView(this, R.layout.databinding_activity);
binding.setActivity(this);

Java파일에서는 이 변수가 어떤값을 넣어줄건지 지정해주어야 합니다. 

setActivity(this)가 activity라는 변수를 이 Class로 지정하겠다는 의미인 것입니다.

setActivity()함수 또한 자동으로 생성된 함수입니다.

우리가 xml에서 aa라는 변수를 만들었다면 자동으로 binding.setAA()함수가 생성됩니다.


MVP패턴을 사용하신다면 presenter를, MVVM패턴을 사용하신다면 ViewModel를 활용해서 클릭이벤트에 맞는 동작을 수행하도록 지정하면 됩니다.


함수 이름을 지정할때 기존에는 @{activity.함수이름()} 으로 해도 문제가 없었지만 언젠가부터 이 방법은 deprecated되었다고 나오며 @{activity::함수이름()}으로 사용하도록 안내하고 있습니다.

물론 activity.함수이름()으로 사용해도 문제가 없으나 Google의 공식문서에서도 변수이름::함수이름() 으로 나와있으니 가이드를 따라서 작성하는것이 좋을것 같습니다.










RecyclerView 리스트



RecyclerView를 이용해서 이러한 리스트목록을 만드는 작업을 해보겠습니다.

사용자의 사진,이름,나이로 구성되어있는 화면입니다.

이 글을 읽는 여러분들은 화면만 봐도 레이아웃 구성을 어떻게 가져가야 할지 잘 아실겁니다.

각각의 item View로 쓰일 layout에서 1개의 ImageView와 2개의 TextView으로 레이아웃을 구성해줍니다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/list_item_container_padding"
android:gravity="center_vertical"
>
<gun0912.databinding.common.view.CircleImageView
android:layout_width="@dimen/list_item_avatar_size"
android:layout_height="@dimen/list_item_avatar_size"
android:id="@+id/ivProfile"
/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/list_item_container_padding"
android:orientation="vertical"
>

<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_item_text_margin"
/>
</LinearLayout>
</LinearLayout>


또한 이 리스트에서 쓰일 사용자 User 클래스도 생성해줍니다.

public class User {

String name;
int age;
String imgUrl;

public User(String name,int age,String imgUrl){
this.name=name;
this.age=age;
this.imgUrl=imgUrl;

}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getImgUrl() {
return imgUrl;
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
}

}



실제로는 서버에서 이 사용자의 목록을 받아오겠지만 여기서는 샘플 데이터를 생성해서 활용하도록 할 것입니다.

List<User> users = new ArrayList();

users.add(new User("Ted",32,"https://t1.daumcdn.net/cfile/tistory/251F6B4C558E627E26"));
users.add(new User("Jane",20,"http://image.celebtide.com/celeb/new/ve/279_ve_1452259300.jpg"));
users.add(new User("Paul",40,"http://res.heraldm.com/content/image/2013/12/01/20131201000224_0.jpg"));
users.add(new User("Ailee",25,"https://t1.daumcdn.net/cfile/tistory/194599374F7049A901"));






ViewHolder

각각의 item view를 보여주기위해서 ViewHolder에서 View를 만들어주는 작업을 합니다.



기본

public class UserViewHolder extends RecyclerView.ViewHolder {

TextView tvName;
TextView tvAge;
ImageView ivProfile;

public UserViewHolder(View itemView) {
super(itemView);
tvName = (TextView) itemView.findViewById(R.id.tvName);
tvAge = (TextView) itemView.findViewById(R.id.tvAge);
ivProfile = (ImageView) itemView.findViewById(R.id.ivProfile);
}
}




버터나이프

public class UserViewHolder extends RecyclerView.ViewHolder {

@BindView(R.id.tvName)
TextView tvName;
@BindView(R.id.tvAge)
TextView tvAge;
@BindView(R.id.ivProfile)
ImageView ivProfile;

public UserViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);

}
}




데이터바인딩

public class UserViewHolder extends RecyclerView.ViewHolder {

DatabindingItemBinding binding;

public UserViewHolder(View itemView) {
super(itemView);
binding = DataBindingUtil.bind(itemView);
}
}









Item세팅

onBindView()에서 각각의 position에 맞는 User클래스의 내용에 따라 뷰의 내용을 넣어주는 작업을 해줍니다.



기본 / 버터나이프

@Override
public void onBindView(UserViewHolder holder, int position) {

User user = getItem(position);

String name = user.getName();
holder.tvName.setText(name);

int age = user.getAge();
holder.tvAge.setText(Integer.toString(age));

String imgUrl=user.getImgUrl();
ImageUtil.loadImage(holder.ivProfile,imgUrl, ContextCompat.getDrawable(getContext(),R.drawable.no_pic));

}


이미지를 로딩할때는 아래와 같이 로딩할 것입니다.

public class ImageUtil {

public static void loadImage(ImageView imageView, String url, Drawable errorDrawable) {
Glide.with(imageView.getContext()).load(url).error(errorDrawable).into(imageView);
}

}



데이터바인딩

@Override
public void onBindView(UserViewHolder holder, int position) {

User user = getItem(position);
holder.binding.setUser(user);

}


원래의 기본이나 버터나이프에서는 각각 holder의 View에 사용자 정보를 넣어주는 작업을 해주었지만 데이터바인딩에서는 그럴 필요가 없습니다.

setUser()라는 함수를 보고 유추해 본다면, xml의 어딘가에서 user라는 변수를 만들어주었고 그 변수를 setUser()를 통해서 할당해준다는것을 아실겁니다.


<?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="user"
type="gun0912.databinding.common.model.User"/>
</data>


onBindView()에서 값을 넣어주는대신에 데이터바인딩에서는 xml에서 값을 넣어줍니다.

<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
/>

<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_item_text_margin"
android:text="@{Integer.toString(user.age)}"
/>

android:text에 @{user.필드이름} 을 넣어주는것만으로 user의 정보를 가져와서 다시 TextView에 넣어주는 작업을 해줄 필요가 없는 것입니다.






@BindingAdapter


그런데 여기서 의문이 생깁니다.

User정보중에 imgUrl이라는 필드는 'http://xxx/xxx.jpg' 처럼 이미지가 저장되어있는 주소값으로 되어있습니다.

이걸 그냥 TextView에서 넣는다면 이미지 url만 출력되는 원하지 않는 결과를 볼것입니다.

이럴때 @BindingAdapter를 이용해주면 좋습니다.


먼저 사용된 방식부터 살펴보겠습니다.

<gun0912.databinding.common.view.CircleImageView
android:id="@+id/ivProfile"
android:layout_width="@dimen/list_item_avatar_size"
android:layout_height="@dimen/list_item_avatar_size"
app:error="@{@drawable/no_pic}"
app:imageUrl="@{user.imgUrl}"
/>


사용자의 이미지url을 app:imageUrl에 넣어주고, 오류가 발생했을때의 이미지 drawable을 app:error에 넣어줍니다.

이렇게 사용할 수 있는것은 아래와 같이 @BindingAdapter를 미리 만들어 두었기 때문입니다.

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView imageView, String url, Drawable errorDrawable) {
ImageUtil.loadImage(imageView, url, errorDrawable);
}

여기서 bind:imageUrl, bind:error로 만들어주었기때문에 xml에서 이걸 이용해서 값을 넣어 줄 수 있습니다.




@BindingAdapter응용


@BindingAdapter를 잘 응용하면 유용한 기능들을 소스코드를 줄이면서 사용할 수 있습니다.

커스텀폰트를 보여주고자 할때 사용하는 라이브러리중 제일 유명한 Calligraphy라는 라이브러리가 있습니다.

@BindingAdapter를 이용해서 라이브러리사용없이 똑같은 기능을 구현할 수도 있습니다.


@BindingAdapter({"bind:font"})
public static void setFont(TextView textView, String fontName) {
textView.setTypeface(Typeface.createFromAsset(textView.getContext().getAssets(), "fonts/" + fontName));
}
<TextView
app:font="@{`Source-Sans-Pro-Regular.ttf`}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>


아마 기존에 Calligraphy 라이브러리를 사용해보신분들은 이 방법이 얼마나 유용한지 이해 하실 수 있을겁니다.


물론 저는 커스텀폰트를 사용하고자 할때는 Typekit을 이용합니다.

[IT/Android-TIP (한글)] - [안드로이드]커스텀 폰트 쉽게 적용하는 방법 - Typekit







@BindingConversion


만약 사용자 정보중에 Date타입의 timeDate라는 필드가 있다고 가정을 해보겠습니다.

이 정보를 TextView에서 1900.01.01 과 같은 방식으로 보여주고자 한다면 어떻게하면 좋을까요?

Java소스코드에서 Date필드를 가져온뒤, SimpleDateFormat을 이용해서 Date->Text로 날짜형식 변환을 해주고, TextView에 해당 값을 넣어줄겁니다.

하지만 @BindingConversion을 이용한다면 간단합니다.


@BindingConversion
public static String convertDateToDisplayedText(Date date) {
return new SimpleDateFormat("yyyy.MM.dd").format(date);
}


<TextView
android:id="@+id/tvUserTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.timeDate}"
/>


@BindingConversion에서 Date타입이 들어오면 이를 String으로 변경해서 리턴해주도록 만들어 두었기때문에 xml에서 text에 Date타입의 필드를 넣어주면 알아서 변경되서 값이 변경됩니다.





@BindingConversion 응용


값에따라서 View를 보여주거나 숨기고 싶은경우 아래와 같이 해줄 수도 있습니다.

@BindingConversion
public static int convertBooleanToVisibility(boolean visible) {
return visible ? View.VISIBLE : View.GONE;
}
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.age > 20}"
/>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{user.loaded}"
>


android:visibility 는 원래 int값이 들어가야하지만 true/false에 따라서 이를 맞게 표시하도록 설정해주면 xml에서 편하게 사용할 수 있습니다.






식언어


xml에서 사용할 수 있는 식언어는 아래와 같습니다.

  • 수학 + - / * %
  • 문자열 연결 +
  • 논리 && ||
  • 이항 & | ^
  • 단항 + - ! ~
  • 시프트 >> >>> <<
  • 비교 == > < >= <=
  • instanceof
  • 그룹화 ()
  • 리터럴 - 문자, 문자열, 숫자, null
  • 형변환
  • 메서드 호출
  • 필드 액세스
  • 배열 액세스 []
  • 삼항 연산자 ?:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(index+1)}"
android:visibility="@{user.age>20 ? View.VISIBLE : View.GONE}"
android:transitionName='@{"image_"+id}'
/>






Include


아래와 같은 화면구성에서 3개의 뷰는 하나의 layout을 재사용해서 사용할 수 있습니다.




layout의 구조는 같지만 안의 내용이 달라지기때문에 이럴때 우리는 보통 CustomView를 만들어서 attribute에 해당 값을 넣어서 보여주는 방식을 이용하곤 했습니다.

[IT/Android-TIP (한글)] - [안드로이드]CustomView를 만들어서 재사용하기


하지만 데이터바인딩과 include를 잘 활용한다면 우리는 커스텀뷰를 만들지않고도 이를 구성할 수 있습니다.



재사용될 xml layout

<layout
xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable name="count" type="int"/>
<variable name="title" type="String"/>
</data>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">

<TextView
style="@style/CustomText_Subhead"
android:text="@{Integer.toString(count)}"
android:textStyle="bold"
/>

<TextView
style="@style/CustomText_Body"
android:text="@{title}"
android:textColor="@color/txt_midiumgray"/>

</LinearLayout>

</layout>

이 layout에서는 count와 title을 변수로 받아올것이고 이를 TextView에 넣어주는 작업을 해줍니다.



<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"

>

<include
layout="@layout/sns_counter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:count="@{user.postCount}"
app:title="@{@string/post}"/>

<include
layout="@layout/sns_counter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:count="@{user.followerCount}"
app:title="@{@string/follower}"/>

<include
layout="@layout/sns_counter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:count="@{user.followingCount}"
app:title="@{@string/following}"/>

</LinearLayout>

위의 layout을 <include> 태그로 재사용해주면서 위에서 선언해주었던 count,title 변수값을 넣어주면 됩니다.

이러한 방식을 이용해서 CustomView로 사용하던것도 얼마든지 간단하게 include로 재사용 할 수 있습니다.









Observable





위의 화면에서 데이터값이 변경되면 View도 갱신하려면 어떻게 해주어야 할까요

아래 방법이 일반적인 방법입니다.

  1. 데이터 가져오기
  2. TextView에 setText()
  3. 데이터 변경됨
  4. 변경된 데이터 가져오기
  5. TextView에 setText()


하지만 Observable을 이용한다면, 한번 TextView에 setText()해준 이후에 값이 변경되면 알아서 변경된 내용으로 View를 업데이트 시켜줄 수 있습니다.



Class를 정의할때 BaseObservable을 상속받습니다.

public class User extends BaseObservable {


getXXX()함수에는 @Bindable을, setXXX()함수에는 notifyPropertyChanged()함수를 추가해줍니다.

String name;

@Bindable
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}


이렇게 설정해주고 xml에서 android:text="@{user.name}" 으로 넣어두면 사용자의 이름이 변경되면 알아서 해당 TextView의 내용도 변경되도록 할 수 있습니다.

<TextView
style="@style/CustomText_Body"
android:layout_marginTop="@dimen/default_padding"
android:text="@{user.name}"
android:textStyle="bold"
/>






ObservableField


BaseObservable을 상속받아서 setXXX(), getXXX()함수를 만드는게 귀찮다면 ObservableField를 사용할 수 있습니다.

public class User2 {

public final ObservableField<String> name = new ObservableField<>();
public final ObservableInt postCount = new ObservableInt();
public final ObservableBoolean follow = new ObservableBoolean();

}


ObservableField로 사용할수 있는 타입은 아래와 같습니다.

  • ObservableField
  • ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, ObservableParcelable
변수를 set, get 할때는 아래와 같이 사용해주면 됩니다.
String name = user.name.get();
user.name.set("aa");






응용


지금까지 설명드렸던 내용들을 잘 활용한다면 아래와 같은 동작을 간단하게 구성해줄 수 있습니다.



  1. 팔로우 버튼 클릭시, 팔로우 버튼 숨기기

  2. Progressbar 보이기

  3. 팔로우 완료시, 팔로워 숫자 +1 하기

  4. 변경된 팔로워 숫자를 TextView에 다시 setText()해주기

  5. Progressbar 숨기기

  6. 팔로우 완료되어있는 팔로잉 버튼 보이기

이 과정을 소스코드로 만들었을대 어떻게 할지 잘 생각해보시고 데이터바인딩에서 xml로 어떻게 간단하게 해결했는지 살펴보시기 바랍니다.


지금까지 설명드렸던 예제들과 위의 응용 동작은 Github에서 샘플 프로젝트로 확인 해보실 수 있습니다.

https://github.com/ParkSangGwon/TedDataBindingSample


(내용이 유용하셨다면 Github 오른쪽위의 [Star]버튼을 눌러주시면 감사하겠습니다

저에게는 별풍선 주시는 효과가 있어요!)





불편사항


데이터바인딩을 사용해보면서 몇가지 불편한 사항들이 있었습니다.

물론 이러한 것들도 점점 개선될 것이라고 믿고 있습니다.

  • 버터나이프에서 사용하는 Resource binding은 사용할 수 없다(@BindString, @BindDimen, … )

  • Android Studio 최신버전인데도 제대로 반영이 안된다

  • 여러 군데에서 같은 변수이름의 @BindingAdapter 을 사용해도 오류가 없다고 한다

  • 인코딩 문제때문인지 xml에서 ‘&&’ 대신 ‘&amp;&amp;’로 써야한다

  • Java소스코드는 줄어들지만 xml소스코드는 늘어난다 


그럼에도 불구하고 데이터바인딩을 아주 좋습니다.

이제는 버터나이프대신 데이터바인딩을 사용해서 프로젝트를 만들어 보세요

Goodbye 버터나이프 Hello 데이터바인딩





감사합니다.




안드로이드 개발자끼리 소통하기위한 오픈채팅방을 만들었습니다.

안드로이드 관련 Q&A및 팁을 공유하는 곳입니다.

관심있으신분들은 참여해보세요.

https://open.kakao.com/o/g8rSGB

Comments