Android Programming – MVVM with Data Binding

0. MVVM

MVVM은 마틴 파울러가 2004년에 제안한 Presentation Model의 한 종류입니다. MVVM은 Presentation Model을 바탕으로 WPF 앱을 위해 2009년에 Josh Smith가 MSDN Magazine에 기재했습니다. 마틴 파울러의 Presentation Model과 MVVM은 뷰의 상태와 행동을 추상화한다는 점이 유사합니다. 그러나 Presentation Model은 뷰의 추상화를 통해 특정 사용자 인터페이스 플랫폼에 의존하지 않는 방식입니다. MVVM과 Presentation Model은 MVC에서 유래되었습니다. MVVM은 비지니스로직이나 백엔드(data model)를 GUI와 분리함으로써 개발을 도와줍니다.

안드로이드에서 MVVM 프레임워크를 사용하기 위해 다양한 방법이 있겠지만, 이번 포스팅에서는 RxAndroidData Binding을 통해 구현하겠습니다.

1. 환경 구성

Data Binding은 구글에서 제공하는 라이브러리로 사용을 위해서는 gradle 1.3.0 이상 버젼이 필요합니다. Data Binding 사용을 위해서는 프로젝트의 build.gradle에 Data Binding의 class path가 추가되어 있어야 합니다.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:1.3.0-beta4"
        classpath "com.android.databinding:dataBinder:1.0-rc4"
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Data Binding 라이브러리의 classpath를 build.gradle에 추가했다면, 애플리케이션의 build.gradle에 apply plugin을 추가하고 com.android.databinding을 선언해 Data Binding 프로젝트임을 명시합니다.

apply plugin: 'com.android.application'
apply plugin: 'com.android.databinding'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.goodmorningcody.mvvm"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.0'
}

2. Layout

Layout은 Data Binding을 위해 LinearLayout으로 구성하고, 1개의 TextView와 2개의 EditText를 추가합니다. 첫번 째 EditText는 이름을 입력하고, 두번 째 EditText는 나이를 입력하겠습니다. 나이와 이름이 모두 입력되면 TextView에는 이름과 나이가 함께 출력되도록 구현할 것 입니다.

스크린샷 2015-11-04 오전 11.39.27

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">
    <TextView android:text=""
        android:textAlignment="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <EditText
        android:layout_marginTop="30dp"
        android:hint="@string/hint_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <EditText
        android:hint="@string/hint_age"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

3. Model과 ViewModel

MVVM에서 Model은 데이터, ViewModel은 사용자의 입력을 처리하고, Model의 데이터를 가공해 View에 필요한 데이터를 제공합니다. Model 역활을 하는 UserModel 클래스와 ViewModel 역활을 하는 UserViewModel 클래스를 추가합니다. Model 역활을 하는 UserModel 클래스에는 String 타입의 name 프로퍼티와 int 타입의 age를 추가합니다. name과 age의 getter, setter도 추가합니다.

package com.goodmorningcody.mvvm;

public class UserModel {
    private String name;
    private int age;
    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;
    }
}

UserViewModel 클래스는 추가합니다. UserViewModel 클래스는 ViewModel 역활을 하는 클래스로 프로퍼티에 UserModel을 추가합니다. UserViewModel 클래스는 android.databinding의 BaseOBservable을 상속받도록 합니다. getMessage, getName, getAge 메소드를 선언하고, UserModel의 프로퍼티에서 name, age를 반환하거나, getMessage에서는 name과 age를 결합한 문자열을 반환합니다. Data Binding을 통해 레이아웃에서 호출할 onEditAction 메소드도 선언합니다. getMessage 메소드는 @Bindable 어노테이션을 추가해 이름과 연령이 바뀔 경우 message가 변경되도록 합니다.

package com.goodmorningcody.mvvm;

import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.TextView;
import com.goodmorningcody.mvvm.BR;

public class UserViewModel extends BaseObservable {

    private UserModel model = new UserModel();
    public UserViewModel() {
    }

    @Bindable
    public String getMessage() {
        if( model.getName()==null ) {
            return "사용자의 이름을 입력해 주세요.";
        }
        return String.format("%s님의 나이는 %d살 입니다.", model.getName(), model.getAge());
    }

    public String getName() {
        return model.getName();
    }

    public String getAge() {
        return new Integer(model.getAge()).toString();
    }

    public boolean onEditAction(TextView view, int actionId, KeyEvent event) {
        if (view.getId()==R.id.name ) {
            model.setName(view.getText().toString());
        }
        else if( view.getId()==R.id.age ) {
            model.setAge(Integer.parseInt(view.getText().toString()));
        }
        notifyPropertyChanged(BR.message);
        return false;
    }
}

4. Layout에 data 정의

Data Binding을 위해서 레이아웃 리소스는 layout이라는 노드로 root를 감싸야 합니다. layout 하위에는 data를 추가하고, 사용할 변수의 name과 type을 지정합니다. layout에 선언된 data의 name은 레이아웃의 각 영역에서 @{user.name}과 같이 접근할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="user" type="com.goodmorningcody.mvvm.UserViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">
        <TextView android:text="@{user.message}"
            android:textAlignment="center"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <EditText
            android:id="@+id/name"
            android:layout_marginTop="30dp"
            android:hint="@string/hint_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:imeOptions="actionDone"
            android:inputType="text"
            android:text="@{user.name}"
            android:onEditorAction="@{user.onEditAction}"/>

        <EditText
            android:id="@+id/age"
            android:hint="@string/hint_age"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:imeOptions="actionDone"
            android:inputType="text"
            android:text="@{user.age}"
            android:onEditorAction="@{user.onEditAction}"/>

    </LinearLayout>
</layout>

5. View와 ViewModel의 바인딩

View 레이아웃에 layout과 함께 data를 추가하는 것은 데이터를 선언하는 것 뿐입니다. 실제 데이터 바인딩은 UserActivity에 DataBindingUtil을 사용해 구현합니다. UserActivity의 onCreate에서 DataBindingUtil의 setContentView 메소드를 호출하면 인자로 전달한 레이아웃 리소스의 이름을 바탕으로 만들어진 바인딩 클래스의 인스턴스가 반환됩니다. binding 인스턴스를 setUser, 즉 View 레이아웃의 data 이름인 user에 set하는 setUser 메소드를 호출하며 바인딩될 뷰모델 인스턴스를 전달합니다.

package com.goodmorningcody.mvvm;

import android.databinding.DataBindingUtil;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.goodmorningcody.mvvm.databinding.UserActivityBinding;

public class UserActivity extends AppCompatActivity {

    private UserActivityBinding binding;
    private UserViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.user_activity);
        viewModel = new UserViewModel();
        binding.setUser(viewModel);
    }
}

6. 실행

실행 후 확인해 봅시다. name과 age의 변경에 View가 갱신되는 것을 확인할 수 있습니다. Data Binding을 사용해 만들어본 프로젝트 구조가 MVVM인지 고민해 봅시다. 맞나요?

7. MVVM 참고하기

image_thumb

https://manojjaggavarapu.wordpress.com/2012/05/02/presentation-patterns-mvc-mvp-pm-mvvm/