본문 바로가기

프로그래밍/Android

[Android] Data Binding Library

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

 

데이터 바인딩 라이브러리  |  Android Developers

The Data Binding Library enables you to write declarative layouts.

developer.android.com

 

build.gradle

apply plugin: 'kotlin-kapt'

android {
   .
   .

   dataBinding {
      enabled = true
   }

 

레이아웃XML :

ex) test_layout.xml

<layout>
    <data>
    	<variable name="vm" type="com.example.User" />
    </data>
    .
    .
    <LinearLayout>
    	<Button
        	android:id="@+id/btnTest">
   </LinearLaout>
</layout>

 

<layout></layout> 태그로 레이아웃을 감싸면 빌드시에 test_layout.xml인 경우 TestLayoutBinding 클래스를 자동으로 생성해 준다.

해당 클래스는 추가한 variable, 각 뷰와 뷰그룹에 대한 속성을 정의하고, 사용된 변수 할당 관련 메쏘드를 구성하게 된다.

 

각 뷰들도 레이아웃내의 해당 id명으로 할당되므로, 바로 사용할 수 있다.

btnTest.setOnClickListener {

    // button clicked

}

 

<data></data> 부분은 뷰모델들을 위한 자리로,  아래와 같은 메쏘드들이 TestLayoutBinding 클래스에 자동 생성된다.

public abstract void setVm(com.example.User user);

public abstract com.example.User getVm(); 

 

test_layout.xml은 TestLayoutBinding 으로 생성되는데, 자동 생성되는 클래스의 이름이나 패키지를 변경하고 싶다면

<data> 태그의 class 속성에 이름을 지정하면 해당 이름으로 클래스가 생성된다.

ex)

<data class="com.example.CustomTestLayoutBinding">

</data>

 

 

레이아웃에 정의된 User 클래스를 만들어 보자.

데이터 객체 정의(com.example.User)

public class User {
   public final String firstName;
   public final String lastName;
   public User( String firstName, String lastName ) {
      this.firstName = firstName;
      this.lastName = lastName;
   }
}

 

레이아웃 xml 내에서 User 값의 사용.

data 에서 선언된 vm 변수를 사용해 직접 바인딩 될 값을 지정할 수 있다.

@{변수명.프로퍼티}

 

<TextView 

   android:layout_width="wrap_content"

   android:layout_height="wrap_content"

   android:text="@{vm.firstName}"/>

 

이제 액티비티에서 레이아웃을 설정해 주어야 하는데, 

기존 setContentView를 DataBindingUtil에서 제공하는 setContentView 메소드로 변경한다.

바인딩 객체를 얻은 다음에는 xml에 정의한 변수를 일반 java 변수처럼 사용하면 된다. 

아래는 vm 변수에 User 값을 넣게된다.

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

   TestLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.test_layout); 
   User user = new User("Test", "User");
   
   // xml에서 선언된 data 변수에 값을 할당한다.
   // binding.vm = user;
    binding.setVm(user); 
}

 

DataBindingUtil 에는 inflate( ) , bind( ) 등의 함수도 제공한다.
Fragment는 onCreateView( LayoutInflater inflater, ViewGroup group, Bundle saveInstance) 호출 되므로, inflate 메쏘드를 사용.

리스트뷰와 같이 parent 뷰가 전달되는 경우에는 bind 메쏘드 사용.

 

 

액티비티 : DataBindingUtil.setContentView( activity , R.layout );

프래그먼트 : DataBindingUtil.inflate( inflater, R.layout, view group, attach to parent )

홀더 : DataBindingUtil.bind( view )

 

 


 

 

이벤트

데이터와 마찬가지로 뷰의 이벤트도 특정 객체의 함수에 바인딩 할 수 있는데, 직접 메쏘드를 지정하거나 람다를 사용해 콜백 형태로 메쏘드를 지정할 수 있다.

보통은 액티비티에서 이벤트 처리를 하므로, <data></data> 에 variable을 액티비티 클래스로 선언하고, 해당 객체의 메쏘드를 호출해 주면 된다.

 

MVVM 과 같이 별도 뷰 모델에서 이벤트 처리를 하는 경우에도 다를게 없다.

 

ex) 이벤트 처리용 클래스

public class Handlers {
   public void onClick( View view ) { 
   }
}

 

xml에 이벤트 객체 변수 추가

<data>
   <variable name="vm" type="com.example.User" />
   <variable name="handlers" type="com.example.Handlers" />
</data>

 

 

이벤트에 참조 지정

<TextView 
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{vm.userProperty}"
   android:onClick="@{handlers::onClick}"
/>

위처럼 객체명::메쏘드 형태로 지정하는 경우 별도 매개변수, 리턴값에 대한 내용이 없이 메쏘드명만 적어주면된다.

물론, 해당 메쏘드의 리턴타입과 매개변수는 해당 이벤트가 필요로 하는 타입과 일치하게 선언되어 있어야 한다.

위의 경우 View.OnClickListener 에 맞게 리턴타입과 입력 매개변수를 정의한 메쏘드를 호출해야한다.

 

 

android:onClick="@{(view)->handlers.onClick(view)}"

람다식을 사용한 리스너 바인딩을 사용해 임의의 메쏘드를 호출하도록 할 수 있다. 메쏘드 바인딩은 이벤트 리스너와 매개변수가 일치해야 하지만 리스너  바인딩은 리턴값만 맞춰주면 된다.

람다식은 안드로이드에서 지원되는 뷰만 지원되며, 만약 커스텀뷰에서 사용하는 경우에는 @BindingAdapter 를 사용해 원하는 동작을 위한 메쏘드를 명시적으로 지정해 주어야 한다.

 

@BindingAdapter( {"namespace:attribute_name" } )

public static void myFunction( View view, int value ) {

 

}

위와 같이 선언되면 xml 에서 해당 attribute_name 으로 설정되는 값은 위 static 메쏘드를 통해 바인딩 된다.

<ImageView

    app:attribute_name="@{value}" />

 

{"attribute1","attribute2"} 와 같이 여러개의 값을 바인딩 가능하며,

메쏘드의 첫 인자는 View 하위 객체로 시작하고, 입력받을 값 타입에 맞게 인자를 선언한다.

 

 

코틀린의 경우 

object BindingAdapters {
    @BindingAdapter(value=["app:attribute"])
    @JvmStatic fun myBindingAdapter( view: View, value:Int ) {
    }

}

 

@BindingMethods(
    BindingMethod( 
    	type = View::class, 
    	attribute = "app:attribute", 
        method = "function" )
 )
  

 

 


Observable

위에 정의한 User 객체는 직접 setter를 호출해서 값을 적용했다.

 

데이터 변경 일어날때 자동으로 뷰에 값이 전달되도록 하기 위해서는 뷰모델 데이터를 android.databinding.Observable 혹은 android.databinding.BaseObservable 을 상속받아 구현하거나 이미 구현되어 있는 Observable 객체들을 사용해야 한다.

 

데이터 클래스를 BaseObservable을 상속받도록 수정하고, 바인딩 처리가 가능하도록 추가 코드작성

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); 
   } 
}

가장 큰 차이점은 getter에 @Bindable 이라는 어노테이션이 추가 되었고,

setter 내에서 notifyPropertyChanged() 를 호출하게 된다.

 

getter에 @Bindable 주석을 붙여주면 BR 이라는 클래스를 생성하고, 각 속성에 인덱스를 부여하게 된다.

데이터 변경 후 notifyPropertyChanged(BR.property_name)  메쏘드를 호출해 변경 이벤트를 전달한다.

변경 이벤트가 전달되면 바인딩된 곳에서는 해당 인덱스에 맞는 실제 getter가 실행되어 데이터를 가져가게된다. 

즉, 유저가 setter를 통해 값을 변경하면, xml에서는 @Bindable어노테이션이 붙은 프로퍼티 중에서 해당 하는 프로퍼티의 값을 가져가 자동으로 적용한다.

 

 

이미 구현되어 있는 Observable 객체들을 사용하면 BaseObservable을 상속받지 않고, setter, getter와 같은 불필요한 처리할 필요가 없어 조금 더 간결한 코드를 만들수 있다.

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

 

지원되는 Observable 객체들

ObservableField<T>

Observable+타입 : Int, Boolean, Byte, Char, Short, Long, Float, Double, Parcelable

Observable+컬렉션 : ArrayMap, ArrayList

 

 

이 Observable 데이터를 "@{vm.firstName}" 처럼 레이아웃에서 사용하면 데이터 변경시 해당 값이 자동으로 뷰에 적용된다.

 

 


리스트뷰나 리사이클러뷰

간단한 구조의 경우 별도 객체를 생성하지 않고, ObservableArrayList<Data> 를 그대로 사용해 구성한다.

리스트뷰의 경우 리스트 배열이 변경되는 것이므로, 해당 배열을 변수로 선언한다. ObservableArrayList<User> 의 경우 xml이라 아래와 같은 형식으로 선언해 준다. 

<data>
    <import type="android.databinding.ObservableArrayList" />
    <import type="com.example.User" />
    
    <variable name="arrayList" type="ObservableArrayList&lt;User&gt;" />
</data>


<RecyclerView
    android:id="@+id/recyclerView"
    .
    .
    app:list="@{arrayList}" />
    

 

액티비티에서 각 데이터 초기화

변수에 값을 할당해 주어야 한다. xml상에 arrayList 라는 변수를 선언했으니 binding 이후에 setArrayList로 리스트 객체를 할당한다. 

MyTestActivityBiding binding = DataBindingUtil.setContentView( this, R.layout.my_test_activity);
UserListAdapter adapter = new UserListAdapter();
ObservableArrayList<User> array = new ObservableArrayList<User>();
binding.recyclerView.setAdapter( adapter );
binding.setArrayList( array );

 

 

해당 리스트 데이터를 리스트뷰에 할당하기 위해 xml에서는 커스텀 속성인 app:list 를 사용했다.

이 속성을 처리할 BindingAdapter 메쏘드를 정의한다. binding adapter를 정의해 두면, 해당 값이 설정되면 정의된 메쏘드가 호출된다.

@BindingAdapter({"list"})
public static void bindList( RecyclerView view, ObservableArrayList<User> list ) {
    UserListAdapter adapter = (UserListAdapter) view.getAdapter();
    adapter.setList( list );
}

 

이제 adapter에 setList 메쏘드를 구현해 주면, 데이터가 변경되면 자동으로 리스트뷰에도 데이터가 적용된다.

// listview
class UserListAdapter extends BaseAdapter {
    private Context mContext;
    private List<User> userList;
    
    public void setList( List<User> list ) {
    	this.userList = list;
        notifyDataSetCahanged();
    }
    
    UserListAdapter( Context context ) {
        mContext = context;
    }
    
    @Override
    public View getView( int i, View view, ViewGroup viewGroup ) {
        UserItemLayoutBinding binding;
        if( view == null ) {
            view = LayoutInflater.from( mContext ).inflate(R.layout.user_item_layout, null);
            binding = DataBindingUtil.bind( view );
            view.setTag(binding);
        } else {
            binding = vidw.getTag();
        }
        
        // binding으로 각 뷰 설정
        return binding.getRoot();
    }
}


// 아니면 inflater를 사용해도 되고...
// recyclerview
class UserListAdapter extends RecyclerView.Adapter<UserViewHolder> {
    private List<User> userList;
    private LayoutInflater layoutInflater;
    
    public void setList( List<User> list ) {
    	this.userList = list;
        notifyDataSetCahanged();
    }
    
    @Override
    public ViewHolder onCreateViewHolder( ViewGroup parent, int viewType ) {
        if( layoutInflater == null ) {
            layoutInflater = LayoutInflater.from( parent.getContext() );
        }
        
        UserItemLayoutBinding binding = DataBindingUtil.inflate( layoutInflater, R.layout.user_item_layout, false);
        return new UserViewHolder( binding );
    }
    
    @Override
    public void onBindViewHolder( ViewHolder holder, int position) {
    
    }
}

class UserViewHolder extends RecyclerView.ViewHolder {
    UserItemLayoutBinding binding;
    
    public UserViewHolder( UserItemLayoutBinding binding) {
    	super( binding.getRoot() );
    	self.binding = binding;
    }
}

 


 

AAC

데이터 바인딩은 MVVM 아키텍처를 위해 가장 필수적인 요소라고 할 수 있는데, 일반적으로 MVVM을 적용하기에는 예외적인 처리들이 필요하게 된다.

안드로이드에서는 AAC(Android Architecture Components)에서 ViewModel과 LiveData 등 바인딩과 MVVM을 위한 클래스들을 제공하고 있다.

 

ViewModel

ViewModel은 생명주기 관리를 위해 제공되는 클래스이다.

싱글톤 객체와 같이 사용되기에 여러 프래그먼트에서 동일한 뷰모델이 사용되는 환경에서도 쉽게 사용이 가능하며, 액티비티의 생명주기에 맞춰 동작하도록 되어 있다.

 

Application Context를 사용해야 하는 경우 AndroidViewModel 을 상속 받아야 하며, 액티비티, 뷰에 대한 context는 가질수 없다.

 

ViewModel 클래스 자체는 아무기능을 하지 않으므로, 별도의 생성방식을 사용해야 한다.

ViewModelProvider 를 통해서 viewmodel 클래스를 지정해 주는 방식으로 생성한다.

별도의 생성자를 가지는 커스텀 뷰모델의 경우 ViewModelProvider.Factory 를 상속받은 factory 클래스를 제공하도록 되어있다.

 

* 별도의 생성자가 없는 뷰모델의 경우

   class MyViewModel : VieModel() {

      .

      .

   }

 

   뷰모델 생성( 액티비티 onCreate )

   var myModel = ViewModelProviders.of(this).get( MyViewModel::class.java )

 

 

* 커스텀 생성자가 있는 뷰모델의 경우

   class MyViewModel( val initValue : Long) : ViewModel() {

   

      override fun onCleared() {

      }

   }

 

   뷰모델 팩토리 

   class MyViewModelFactory( val initValue: Long ) : ViewModelProvider.Factory 

   {

      override fun <T:ViewModel?> create(modelClass: Class<T>):T {

          return MyViewModel( initValue ) as T

          // return modelClass.getConstructor( Long::class.java).newInstance( initValue )

      }

   }

 

   생성

   var factory = MyViewModelFactory( 1 )

   var myModel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)

 

 

 

 

LiveData

데이터바인딩 라이브러리에는 ObservableField와 같은 홀더가 존재했는데, LiveData도 같은 기능을 하며,  안드로이드의 생명주기에 따른 예외처리가 추가되어 있다.

 

class MyViewModel( val initValue : Long) : ViewModel() {

   .

   .

   // 일반적으로 외부에서는 값을 변경하지 못하게 선언하려면 MutableLiveData를 사용한다.

   privae val _data = MutableLiveData<String>()

   val data: LiveData<String> get() = _data

   .

   .

   // 별도 set 함수를 제공해서 해당 함수에서 LiveData의 값을 수정하도록 한다.

   // 값의 수정은 LiveData의 setValue, postValue 메쏘드를 통해서 이루어지게 된다.

   fun setDataValue( data: String ) {

      // 메인 쓰레드인경우 setValue( val )

      // 다른 쓰레드인경우 postValue( val )

      _data.postValue( data )

   }

}

 

 

뷰모델과 라이브데이터의 기본적인 처리 방법은 아래와 같다.

 

- 뷰모델 생성 및 옵저버

val myFactory = MyViewModelFactory( 0 )

val myModel = ViewModelProviders.of( this, myFactory ).get( MyViewModel::class.java )

 

- 변경을 감지할 옵저버를 생성

val dataObserver : Observer<String> = Observer {

   newName ->

   // 변경시 처리할 내용

}

 

- 라이브데이터의 observe 호출해 옵저버 등록

myModel.getData().observe( this, dataObserver );

 

- 값변경

myModel.setDataValue( "test" )

 

옵저버를 통해 데이터가 변경시 필요한 작업이 가능하게 된다.

단순히 뷰에 데이터값을 적용하는 경우에는 기존과 마찬가지로 레이아웃에서 android:text="@{vm.data}" 과 같이 직접 지정해주면 된다. 

 

최근 많이 사용되는 MVVM의 대략적인 처리 플로우는 아래와 같다

1. 뷰에서 사용자 이벤트 발생

2. 레이아웃에 연결된 뷰모델의 이벤트 콜백 호출

3. 뷰모델에서 RxJava2를 통해 Repository로 데이터를 요청

4. Repository에서 SQLite, Retrofit을 통해 데이터 가져오기

5. 데이터가 수신되면 RxJava2는 구독자에게 push

6. 뷰모델에서 데이터 수신

7. 뷰모델의 라이브데이터의 값 변경 및 알림

8. 뷰에 변경된 값 적용

 

'프로그래밍 > Android' 카테고리의 다른 글

[Android] gradle android 빌드 구성  (0) 2019.09.17
[Android] ConstraintSet  (0) 2019.08.22
Gradle Kotlin, AndroidX 설정  (0) 2019.08.17
[Android] androidX Camera  (0) 2019.07.15
[Android] Retrofit  (0) 2019.04.11
String 리소스에 html 태그 넣기  (0) 2014.04.29
파일 변경 이벤트  (0) 2014.04.01
[안드로이드TV] 개발 전 확인사항  (0) 2014.02.17
GoogleTV 개발환경 설정  (0) 2014.02.06
GCM 메시지 전송  (0) 2013.09.13