Google for Mobile I/O RECAP 2018 (01)

현장


전리품

기본 : 출입태그, 티셔츠, 텀블러 (얼리버드 이벤트로 못받음 ㅠㅠ), #IMakeApps 스티커

그리고 도시락


01. Keynote. 환영사

  • 발표자 : 민경환 (Head of Korea Android Apps and Games Business Development, Google)
  • 세션설명 :

환영함.

데이터 바인딩 라이브러리 3

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

데이터 객체

데이터 바인딩에 임의의 POJO(Plain Old Java Object)를 사용 가능하지만, POJO를 수정하더라도 UI가 업데이트 되지는 않음. 데이터가 변경될 때 이를 알려주는 기능을 데이터 객체에 부여해야 데이터 바인딩을 제대로 활용할 수 있음. Observable 객체, 필드, 컬렉션이라는 세 가지 다른 데이터 변경 알림 매커니즘이 있음.

이러한 Observable 데이터 객체 중 하나가 UI에 바인딩되어 있고 데이터 객체 속성이 변경되면 UI가 업데이트 됨.

Observable 객체

Observable 인터페이스를 구현하는 클래스를 사용하면 바인딩이 바인딩된 객체 단일 리스너를 연결.

Observable 인터페이스에는 리스너를 추가/제거하는 매커니즘이 있지만, 알림은 개발자의 선택에 따라 결정됨.

더 쉽게 개발하도록 BaseObservable을 만들어 리스너 등록 매커니즘 구현함. 속성이 변할 때 이를 알릴 책임은 데이터 클래스 구현자에게 있으며, getter에 Bindable 주석을 할당하고 setter에서 이를 알림

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

Bindable 주석은 컴파일 중에 BR 클래스 파일에 항목을 생성함. BR 클래스 파일은 모듈 패키지에 생성됨.

데이터 클래스에 대한 기본 클래스를 변경할 수 없는 경우, 리스너를 효율적으로 저장하고 알리기 편한 PropertyChangeRegistry를 사용하여 Observable 인터페이스를 구현 가능.

Observable Field

ObservableFields는 단일필드를 가진 Observable 객체.데이터 클래스에 public final 필드를 생성함.

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

ObservableField 와 형제들

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable

값 액세스시 set 및 get 접근자 메서드 사용

user.firstName.set("Google");
int age = user.age.get();

Observable 컬렉션

데이터 객체에 키 입력 방식으로 액세스 가능. 키가 String과 같은 참조 형식일 경우 ObservableArrayMap이 유용함.

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

레이아웃에서 String키를 통해 맵에 액세스시

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap&lt;String, Object&gt;"/>
</data>
…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

키가 정수일 때는 ObservableArrayList가 유용

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);ㅔ]

레이아웃에서 인덱스를 통해 액세스시

<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList&lt;Object&gt;"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

생성되는 바인딩

생성되는 바인딩 클래스는 레이아웃 내에서 레이아웃 변수를 View와 연결함.

Binding의 이름과 패키지는 사용자 지정 가능하며, 바인딩 클래스는 모두 ViewDataBinding을 확장함.

생성

일반적으로 정적 메서드 사용하여 생성.

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

또는 레이아웃을 확장시킨 후 따로 바인딩

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

바인딩을 미리 알 수 없을 때는 DataBindingUtil 클래스를 사용하여 바인딩 생성

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

###View 적용

레이아웃에서 ID가 있는 각각의 View에 대한 공용 최종 필드 생성됨.

바인딩은 ID가 있는 View를 추출함.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
   android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
  android:id="@+id/lastName"/>
   </LinearLayout>
</layout>

위 코드는 아래 바인딩 클래스를 생성함.

public final TextView firstName;
public final TextView lastName;

ID는 데이터 바인딩이 없으면 거의 필요 없지만, 코드에서 View에 액세스해야 할 경우가 있음.

###변수

각 변수에는 접근자 메서드가 주어짐

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user"  type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note"  type="String"/>
</data>

위 코드는 바인딩에 setter와 getter를 생성함

public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);

###ViewStub

ViewStub은 보이지 않게 시작되어 보이게 되거나 명시적으로 확장 명령이 있을때 다른 레이아웃을 확장하여 레이아웃에서 스스로를 대체함.

ViewStubProxy 객체가 ViewStub을 대신하여 ViewStub 확장시 확장된 View 계층 구조에 액세스하기도 할때 개발자에게 ViewStub에 대한 액세스 권한을 부여함.

다른 레이아웃을 확장할 때는 새 레이아웃에 대해 바인딩이 설정되어 있어야 함. ViewStubProxy는 하나만 존재할 수 있으며, 개발자는 ViewStubProxy에 대한 OnInflateListener를 설정하여 바인딩 설정 후에 호출하도록 함.

###고급 바인딩

####동적 변수

임의의 레이아웃에 대해 작동하는 RecyclerView.Adapter가 특정 바인딩 클래스를 알지 못하는 경우 onBindViewHolder(VH, int) 중에 바인딩 값을 할당해야 함.

BindingHolder에는 ViewDataBinding 베이스를 반환하는 getBinding 메서드가 있음.

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

####즉각적인 바인딩

변수나 Observable이 변경되면 바인딩이 다음 프레임 전에 변경되도록 예약됨.

바인딩을 즉시 실행해야 할 경우 ViewDataBinding#executePendingBindings() 메서드를 사용.

####백그라운드 스레드

데이터 모델이 컬렉션이 아닌 한, 백그라운드 스레드에서 데이터 모델을 변경할 수 있음.

데이터 바인딩은 동시 실행 문제 방지를 위해 각각의 변수/필드를 지역 범위로 설정함.

특성 Setter

바인딩된 값이 변경시마다 바인딩 클래스는 바인딩 식과 함께 View에 대한 setter 메서드를 호출해야 함.

데이터 바인딩 프레임워크에는 값을 설정하기 위해 호출할 메서드를 사용자 지정하는 방법이 있음.

###자동 Setter

데이터 바인딩은 메서드 setAttribute 찾기를 시도. 특성 이름 자체만 찾으면 됨.

TextView의 android:text와 연관된 식은 setText(String)을 찾음.

식에서 int가 반환되는 경우 데이터 바인딩은 setText(int) 메서드를 검색함.

참고로 주어진 이름을 가진 특성이 존재하지 않더라도 데이터 바인딩은 작동함.

예를 들어 DrawerLayout에 특성이 없지만 setter는 많이 있다고 가정해보면 자동 setter를 사용하면 이런 setter 중 하나를 사용할 수 있음.

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

이름이 바뀐 Setter

setter의 이름이 일치하지 않는 특성은 BindingMethods 주석을 통해 특성을 setter와 연결할 수 있음.

메서드는 클래스와 연결되어야 하며 이름이 바뀐 메서드마다 BindingMethod 주석이 하나씩 포함됨.

예를 들어 android:tint 특성은 실제로 setTint가 아니라 setImageTintList(ColorStateList)와 연결됨.

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Android 프레임워크 특성이 이미 구현되었으므로, 개발자가 setter의 이름을 바꿔야 할 일은 거의 없음.

사용자 지정 Setter

일부 특성은 사용자 지정 바인딩 로직이 필요함.

예를 들어 android:paddingLeft 특성에 대해 연결되 setter가 없고 setPadding(left, top, right, bottom)이 존재함.

개발자는 BindingAdapter 주석이 있는 정적 바인딩 어댑터 메서드를 사용하여 특성의 setter를 호출도록 사용자 지정이 가능함.

Android 특성에 이미 BindingAdapter가 생성되어 있음.

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

바인딩 어댑터는 다른 형식의 사용자 지정에 유용함. 예를 들어 이미지 로드를 위해 오프스레드 상태로 맞춤 로더 호출 가능함.

개발자가 만든 바인딩 어댑터는 충돌이 있을 경우 데이터 바인딩 기본 어댑터를 무시함.

여러 매개변수를 받는 어댑터를 사용할 수도 있음.

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

imageUrl과 error가 둘 다 ImageView에 사용되고 imageUrl이 문자열이고 error가 드로어블인 경우에 이 어댑터가 호출됨.

  • 맞춤 네임스페이스는 일치 확인 중에 무시됨.
  • Android 네임스페이스용 어댑터를 작성할 수도 있음.

바인딩 어댑터 메서드는 선택적으로 핸들러의 기존 값을 취할 수도 있음.

기존 값과 새 값을 취하는 메서드는 특성의 모든 기존 값을 먼저 가진 후 새 값을 가져야 함.

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
   if (oldPadding != newPadding) {
       view.setPadding(newPadding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
   }
}

이벤트 핸드러는 한 개의 추상 메서드를 가진 추상 클래스나 인터페이스와 함께 사용할 수 있을 뿐임.

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue);
        }
    }
}

리스너에 여러 개의 메서드가 있을 때는 여러 개의 리스너로 분할해야 함.

View.OnAttachStateChangeListener에는 onViewAttachedToWindow()와 onViewDetachedFromWindow()의 두 메서드가 있음.

이들 메서드의 특성과 핸들러를 구분하기 위해 두 개의 인터페이스를 만들어야 함.

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}

한 리스너를 변경하면 다른 리스너에도 영향을 미치기 때문에 세 가지의 각기 다른 바인딩 어댑터가 있어야 함.

즉 각각의 특성을 위한 바인딩 어댑터 하나와 두 리스너 모두에 하나씩 있어야 함.

@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

View 가 View.OnAttachStateChangeListener에 대한 set 메서드를 사용하는 대신 리스너에 대한 add와 remove를 사용하기 때문에 위 예시는 좀 더 복잡함.

ListenerUtil 클래스는 Binding Adapter에서 이전의 리스너를 제거할 수 있도록 이들을 추적하는 데 도움이 됨.

인터페이스 OnViewDetachedFromWindow와 OnViewAttachedToWindow에 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 주석을 추가하면 데이터 바인딩 코드 생성기가 addOnAttachStateChangeListener(View.OnAttachStateChangeListener) 에 의해 지원되는 같은 버전인 HoneyComb MR1과 새 기기에서 실행 시 리스너만 생성되어야 한다는 점을 인식하게 됨.

변환기

객체 변환

바인딩 식에서 Object가 반환되면 자동 setter, 이름이 바뀐 setter, 맞춤 setter 중에서 setter가 선택됨.

Object는 선택된 setter의 매개변수 형식으로 형변환됨.

이는 ObservableMaps를 사용하여 데이터를 유지하는 경우에 편리함.

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

userMap은 Object를 반환하고 그 Object는 setter setText(CharSequence)에서 찾은 매개변수 형식으로 자동 형변환됨.

매개변수 형식에 대한 혼돈이 있을 수 있을 때는 개발자가 식을 통해 형변환해야 함.

사용자 지정변환

특정 형식 간 자동 변환이 이루어져야 할 경우. 예를 들어 배경 설정시.

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

배경은 Drawable을 취하지만 색은 정수 형식임.

Drawable이 실행되고 정수가 반환될 때마다 int가 ColorDrawable로 변환되어야 함.

이 변환은 BindingConversion 주석과 함께 정적 메서드를 사용하여 수행됨.

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

변환은 setter 레벨에서만 이루어지므로 다음과 같이 형식을 혼합할 수 없음

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Android Studio의 데이터 바인딩 지원

데이터 바인딩 식에 대해 다음과 같은 기능 지원함

  • 구문 강조표시
  • 식 언어 구문 오류의 플래그 지정
  • XML 코드 완성
  • (선언 탐색 등의) 탐색과 빠른 문서화를 포함한 참조

배열과 제너릭 형식은 오류가 없을 때 오류를 표시할 수도 있음.

Preview 창에 데이터 바인딩 식의 기본값이 표시됨(제공되는 경우).

Preview 창은 TextView에 PLACEHOLDER 기본 텍스트 값을 표시하는 레이아웃 XML 파일의 한 예시.

<TextView android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{user.firstName, default=PLACEHOLDER}"/>


데이터 바인딩 라이브러리 2

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

식 언어

공통

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

연산 누락

Java에서 사용 가능한 식 구문에서 몇 가지 연산 누락됨

  • this
  • super
  • new
  • 명시적 일반 호출

null 병합 연산자

null 병합 연산자는 왼쪽 피연산자가 null 이 아니면 왼쪽, null이면 오른쪽 피연산자를 선택함.

android:text="@{user.displayName ?? user.lastName}"

아래와 동일함

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

속성 참조

식이 클래스의 한 속성을 참조할 떄, 필드, getter, ObservableField 에 똑같은 형식이 사용됨.

android:text="@{user.lastName}"

NullPointerException 방지

생성되는 데이터 바인딩 코드는 자동으로 null 검사하여 null 포인터 예외를 피함.

예를 들면

  • user가 null 인 경우
  • user.name은 기본값(null)
  • user.age는 기본값(0) (여기서 age는 int)

컬렉션

편의상 [] 연산자를 사용하여 배열, 목록, 희소 목록, 맵 등의 공통 컬렉션에 액세스할 수 있음.

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String&gt;"/>
    <variable name="sparse" type="SparseArray&lt;String&gt;"/>
    <variable name="map" type="Map&lt;String, String&gt;"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

문자열 리터럴

작은 따옴표로 묶어서 사용하면 식에서 큰 따움표를 사용하기 쉬움.

특성 값을 큰 따움표로 묶을 경우 문자열 리터럴에는 ' 또는 ` 중 하나를 사용해야 함.

android:text='@{map["firstName"]}'
or
android:text="@{map[`firstName`]}"
android:text="@{map['firstName']}"

리소스

일반적인 구문을 사용하는 식의 일부로 리소스에 액세스할 수 있음.

<!-- 일부로 리소스에 액세스할 수 있음 -->
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

<!-- 매개변수를 제공하여 형식 문자열과 복수형을 평가할 수 있음 -->
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

<!-- 복수형이 여러 매개변수를 취할 때는 모든 매개변수가 전달되어야 함 -->
  Have an orange
  Have %d oranges

android:text="@{@plurals/orange(orangeCount, orangeCount)}"

plurals 는 단수/복수형에 대한 처리시 사용.

https://developer.android.com/guide/topics/resources/string-resource?hl=ko#Plurals

형식일반적인 참조식 참조
String[]@array@stringArray
int[]@array@intArray
TypedArray@array@typedArray
Animator@animator@animator
StateListAnimator@animator@stateListAnimator
color int@color@color
ColorStateList@color@colorStateList


+ Recent posts