티스토리 뷰

DataBinding Library 이용하면 안드로이에서 구현이 쉽지 않고 쓸때없이 많은 클래스를 생성했던 단점을 극복하면서 MVP나 MVVM 같은 패턴을 구현할 수 있다.



View는 화면에 Model이 가진 데이터를 보여주는 역할을 한다. 이 부분은 DataBinding Library가 데이터바인딩을 통해 자동으로 처리해 준다.

Model 데이터의 값과 상태를 보관한다. View가 뭔지에 대해서는 전혀 신경쓰지 않기 때문에 뷰를 조작하는 코드는 사용하지 않는다.

ViewModel은 View와 Model 사이에서 두 레이어를 중계해 준다. 사용자의 interaction을 받아서 데이터를 변경해주는 역할을 한다. 비지니스 로직은 ViewModel 레이어에 위치한다고 볼 수 있다.



이런 Design Pattern을 쓰는 이유는 테스트 커버리지를 높이고 코드의 재사용성을 향상시켜 유지보수 비용을 줄이는데 있다. 깔끔한 아키텍트를 유지하는 코드는 요구사항이 변경되어도 어떤 부분을 고쳐야 할지 명료하게 드러나므로 기존에 다른 코드에 영향을 주지 않고 빠르게 대응할 수 있다.


DataBinding + MVVM 데모를 위해서 간단한 Todo 앱을 만들어 보겠다. 

앱은 아주 간단하다. 메인화면에 리스트뷰로 Todo 목록을 보여주고 아이템을 클릭하면 디테일 화면으로 이동한다.





옵션 메뉴의 "ADD" 를 탭하면 입력화면으로 이동한다.


  




Build Setup

Gradle  빌드파일에 필요한 세팅을 한다. 아래의 한줄이면 Grale 1.5.0-alpha1 버전부터는 Data Binding Library를 사용할 수 있다. (아니라면 업그레이드 하시길 강추)

      android {
            dataBinding {
                  enabled = true
           }
      }



Data Object

먼저 Todo model 클래스 부터 만들어 보자.  id은 Todo의 키값이 된다.

public class Todo implements Serializable { private String id; private String title; private String content; public Todo() { } public Todo(String id) { this.id = id; } public Todo(String id, String title, String content) { this.id = id; this.title = title; this.content = content; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public boolean equals(Object obj) { Todo other = (Todo)obj; if (other == null) return false; boolean result = id != null && id.length() > 0; result = result && id.equals(other.id); return result; } @Override public String toString() { return "Todo{" + "id='" + id + '\'' + ", title='" + title + '\'' + ", content='" + content + '\'' + '}'; } public boolean titleRequired() { return title == null || title.length() ==0; } public boolean contentRequired() { return content == null || content.length() ==0; } }


equals 메소드를 오버라이드 한 이유는 입력한 Todo를 List를 사용해서 저장하기 때문이다. Todo를 Add할 때는 상관이 없지만, update 할 때는 id를 통해 Todo 객체를 검색할 필요가 있다.


Layout file

Todo List를 ListView에 표시할 것이기 때문에 아래와 같이 ListView를 포함하도록 한다. <layout></layout> 태그를 사용하고 있는 것을 눈여겨보자.


R.layout.main_activity

   



    
    

    

        

    



MainActivity.class

액티비티 파일에서는 DataBinding Library를 통해서 main_activty.xml를 사용할 수 있도록 해준다.

    ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        FontUtil.initFonts(this);

        setTitle("Todo List");
        setupViewModel();
    }


activty의 setContentView 대신 DataBindingUtil.setContentView를 사용하면 DataBinding Library가 해당 레이아웃에 안에 있는 매핑을 해석하여 자바 클래스 파일로 변환해 준다. 생성되는 자바 클래스 파일은 일정한 Naming Convention이 있는데 레이아웃파일의 이름을 사용한다.

레이아웃 이름에서 underscore를 빼고 마지막에  Binding이 붙는다.

main_activity -> MainActivityBinding
activity_sample -> ActivitySampleBinding


MainActivityBinding 클래스를 살펴보면 main_activity에 선언된 todoListView가 멤버 변수의 객체로 생성되어 있는 것을 확인 할 수 있다. findViewById() 사용하지 않아도 레이아웃에 id 만 부여해 주면 DataBinding Library가 자동으로 레이아웃을 자바 객체로 변환해 준다. Awesome!



TodoAdapter

리스트뷰에 데이터를 보여주기 위해서 TodoAdapter를 생성한다.

res/layout/item_todo.xml



    
        
    

    

        

        
    

TodoAdapter.java
public class TodoAdapter extends BaseAdapter {

    List items;

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ItemTodoBinding binding = null;
        Todo todo = getItem(position);
        if (convertView != null) {
            binding = (ItemTodoBinding) convertView.getTag();
        } else {
            LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
            binding = DataBindingUtil.inflate(layoutInflater, R.layout.item_todo, parent, false);
            convertView = binding.getRoot();
            convertView.setTag(binding);
        }
   
        binding.setTodo(todo);
        return convertView;
    }
}


ViewHolder 패턴을 사용하는 것 보다 코드가 심플해졌음을 알 수 있다.



이제 남은 작업은 MVVM을 더해주는 것이다. MainViewModel 클래스를 생성해 보자.



MainViewModel.java

public class MainViewModel extends BaseObservable {

    List todoList;
    TodoAdapter todoAdapter;
    MainViewModelEvent event;

    public MainViewModel(List todoList, MainViewModelEvent event) {
        this.event = event;
        setTodoList(todoList);
    }

    public void setEvent(MainViewModelEvent event) {
        this.event = event;
    }

    public List getTodoList() {
        return todoList;
    }

    public void setTodoList(List todoList) {
        this.todoList = todoList;
        if (todoAdapter == null)
            todoAdapter = new TodoAdapter(todoList);
        else
            todoAdapter.setItems(todoList);
    }

    public TodoAdapter getTodoAdapter() {
        return todoAdapter;
    }

    public void setTodoAdapter(TodoAdapter todoAdapter) {
        this.todoAdapter = todoAdapter;
    }

    public void todoListItemClick(int position) {
        if (event != null) {
            Todo todo = todoAdapter.getItem(position);
            event.onTodoSelected(todo);
        }
    }

    public static interface MainViewModelEvent {
        public void onTodoSelected(Todo todo);
    }
}


이제 main_activity 에 아래와 같이 ListView의 어탭터를 설정하고 onItemListClick 이벤트를 설정한다. 


        
    

    

        

    


android:adapter="@{viewModel.todoAdapter}"

데이터바인딩은 xml에 없는 속성이 있을 경우 바인딩 오브젝트에서 setter 를 검색해서 바인딩 해준다. 우리는 android:adapter를 설정했고 리스트뷰에는 setAdapter()메소드가 존재하므로 todoListView.setAdapter(viewModel.todoAdapter)가 호출되게 된다. 그리고 @{viewModel.todoAdapter}라는 표현식은 viewModel 객체 안에 있는 property 또는 getter로 매핑되어진다. 즉, public TodoAdapter todoAdapter - public TodoAdapter getTodoAdapter() - public TodoAdapter todoAdapter() 순으로 해당 검색을 해서 존재하는 것을 바인딩 해준다.

android:onItemClickListener="@{(parent, view, position, id) -> viewModel.todoListItemClick(position)}"

데이터바인딩은 Lambda Expression 을 이용해서 위와 같이 이벤트리스터를 바인딩 할 수 있다. ListView의 아이템이 클릭되면 onItemClickListener에 할당된 viewModel.todoListItemClick() 메소드가 호출되게 된다. 즉, 위의 코드는 다음과 같이 해석될 수 있다.

    todoListView.setOnItemListClick(new AdapterView.OnItemClickListener() {
           @Override
           public void onItemClick(AdapterView parent, View view, int position, long id) {
                 modelView.todoListItemClick(position);
           }
    });


이제 나머지 부분은 MainActivity에서 MainViewModel을 생성해주고 초기화 해주는 것이다.

    MainViewModel modelView;

    public void setupViewModel() {
        modelView = new MainViewModel(Storage.getInstance().dummyList(), viewModelEvent);
        binding.setViewModel(modelView);
    }

    private void selectTodo(Todo todo) {
        //To do something    
    }

   private MainViewModel.MainViewModelEvent viewModelEvent = new MainViewModel.MainViewModelEvent() {
        @Override
        public void onTodoSelected(Todo todo) {
            selectTodo(todo);
        }
    };


이로써 View(Activity), Model(Todo) 간에 서로 독립적인 코드를 작성하게 되었다. ViewModel이 뷰에 종속적이지 않기 때문에 별도의 유닛테스트가 가능하다.

public class MainViewModelTest {

    MainViewModel.MainViewModelEvent event = new MainViewModel.MainViewModelEvent() {
        @Override
        public void onTodoSelected(Todo todo) {

        }
    };

    @Test
    public void shouldCallTodoSelectedOnce() throws Exception {
        List todoList = Mockito.mock(List.class);
        MainViewModel mock = Mockito.mock(MainViewModel.class);
        mock.setTodoList(todoList);
        mock.setEvent(event);
        mock.todoListItemClick(0);

        Mockito.verify(mock, times(1)).todoListItemClick(0);
    }

}



데이터바인딩의 강력함을 확인했기를 바라며, 여기서 글을 마칠까 한다. 데이터바인딩에 너무 복잡한 표현식을 넣는 것과 부분을 자제하고 앱의 아키텍쳐를 도와줄 도구로 사용한다면 유용하게 사용할 수 있으리라 생각한다. 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함