웹사이트 검색

Android MVVM LiveData 데이터 바인딩


우리는 이미 별도의 자습서에서 데이터 바인딩을 구현했습니다. 오늘은 MVVM Android 애플리케이션에서 데이터 바인딩과 함께 LiveData를 사용할 것입니다. LiveData가 ViewModel에서 UI를 쉽게 업데이트하는 방법을 살펴보겠습니다.

MVVM LiveData 데이터 바인딩

지금까지 데이터 바인딩을 사용하여 ViewModel에서 View를 업데이트했습니다. LiveData는 전달할 데이터에 대한 컨테이너 역할을 하는 편리한 데이터 홀더입니다. LiveData의 가장 좋은 점은 수명 주기를 인식한다는 것입니다. 따라서 백그라운드에 있으면 UI가 업데이트를 시도하지 않습니다. 이렇게 하면 런타임에 많은 충돌이 발생하지 않습니다. MutableLiveData 클래스는 공용 메서드 setValue()getValue()를 제공하므로 이 클래스를 사용합니다. 위의 개념을 사용하여 간단한 로그인 응용 프로그램을 만들어 봅시다. 먼저 LiveData와 양방향 데이터 바인딩을 사용한 다음 데이터 바인딩 Observable을 LiveData로 완전히 리팩토링합니다.

시작하기

앱의 build.gradle에 다음 종속성을 추가합니다.


android {
    ...

    dataBinding {
        enabled = true
    }
    ...
}

dependencies {
    ...
    implementation 'android.arch.lifecycle:extensions:1.1.1'
    implementation 'com.android.support:design:28.0.0-beta01'
    ...
}

프로젝트 구조

모델

User.java 클래스에서 모델을 정의했습니다.

package com.journaldev.androidmvvmdatabindinglivedata;


import android.util.Patterns;

public class User {

    private String mEmail;
    private String mPassword;


    public User(String email, String password) {
        mEmail = email;
        mPassword = password;
    }

    public String getEmail() {
        if (mEmail == null) {
            return "";
        }
        return mEmail;
    }


    public String getPassword() {

        if (mPassword == null) {
            return "";
        }
        return mPassword;
    }

    public boolean isEmailValid() {
        return Patterns.EMAIL_ADDRESS.matcher(getEmail()).matches();
    }


    public boolean isPasswordLengthGreaterThan5() {
        return getPassword().length() > 5;
    }

}


공들여 나열한 것

activity_main.xml의 코드는 다음과 같습니다.

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

    <data>

        <variable
            name="loginViewModel"
            type="com.journaldev.androidmvvmdatabindinglivedata.LoginViewModel" />
    </data>


    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="8dp"
            android:orientation="vertical">

            <android.support.design.widget.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:error="@{loginViewModel.errorEmail}"
                app:errorEnabled="true">

                <EditText
                    android:id="@+id/inEmail"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="Email"
                    android:inputType="textEmailAddress"
                    android:padding="8dp"
                    android:text="@={loginViewModel.email}" />

            </android.support.design.widget.TextInputLayout>

            <android.support.design.widget.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:error="@{loginViewModel.errorPassword}"
                app:errorEnabled="true">

                <EditText
                    android:id="@+id/inPassword"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="Password"
                    android:inputType="textPassword"
                    android:padding="8dp"
                    android:text="@={loginViewModel.password}" />

            </android.support.design.widget.TextInputLayout>


            <Button
                android:id="@+id/button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:onClick="@{()-> loginViewModel.onLoginClicked()}"
                android:text="LOGIN" />


            <ProgressBar
                android:id="@+id/progressBar"
                style="?android:attr/progressBarStyleLarge"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="8dp"
                android:visibility="@{loginViewModel.busy}" />


        </LinearLayout>

    </ScrollView>

</layout>

로그인 기능을 시뮬레이트하기 위해 ProgressBar가 표시됩니다.

모델 보기

LoginViewModel.java의 코드는 다음과 같습니다.

package com.journaldev.androidmvvmdatabindinglivedata;

import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.ObservableField;
import android.os.Handler;
import android.support.annotation.NonNull;


public class LoginViewModel extends BaseObservable {
    private String email;
    private String password;
    private int busy = 8;
    public final ObservableField<String> errorPassword = new ObservableField<>();
    public final ObservableField<String> errorEmail = new ObservableField<>();

    public LoginViewModel() {
    }

    private MutableLiveData<User> userMutableLiveData;

    LiveData<User> getUser() {
        if (userMutableLiveData == null) {
            userMutableLiveData = new MutableLiveData<>();
        }

        return userMutableLiveData;
    }

    @Bindable
    @NonNull
    public String getEmail() {
        return this.email;
    }

    public void setEmail(@NonNull String email) {
        this.email = email;
        notifyPropertyChanged(BR.email);
    }

    @Bindable
    @NonNull
    public String getPassword() {
        return this.password;
    }

    public void setPassword(@NonNull String password) {
        this.password = password;
        notifyPropertyChanged(BR.password);
    }

    @Bindable
    public int getBusy() {
        return this.busy;
    }

    public void setBusy(int busy) {
        this.busy = busy;
        notifyPropertyChanged(BR.busy);
    }


    public void onLoginClicked() {

        setBusy(0); //View.VISIBLE
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {


                User user = new User(getEmail(), getPassword());

                if (!user.isEmailValid()) {
                    errorEmail.set("Enter a valid email address");
                } else {
                    errorEmail.set(null);
                }

                if (!user.isPasswordLengthGreaterThan5())
                    errorPassword.set("Password Length should be greater than 5");
                else {
                    errorPassword.set(null);
                }

                userMutableLiveData.setValue(user);
                setBusy(8); //8 == View.GONE

            }
        }, 5000);
    }
}



ObservableField는 관찰 가능하도록 만드는 객체 래퍼입니다. 위의 코드에서 LiveData 내부에 사용자를 캡슐화했습니다. 사용자 객체가 변경될 때마다 MainActivity에서 관찰되고 적절한 조치가 취해집니다. 버튼을 클릭하면 ProgressBar를 Visible로 설정합니다. 보기.VISIBLE=0. View.GONE == 8 5초 지연 후 이메일과 암호가 확인되고 TextInputLayout 바인딩 가능 항목이 업데이트됩니다.

ObservableField는 수명 주기를 인식하지 않습니다.

MainActivity.java 클래스는 다음과 같습니다.

package com.journaldev.androidmvvmdatabindinglivedata;

import android.arch.lifecycle.Observer;
import android.databinding.DataBindingUtil;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;

import com.journaldev.androidmvvmdatabindinglivedata.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

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


        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        LoginViewModel loginViewModel = new LoginViewModel();
        binding.setLoginViewModel(loginViewModel);

        loginViewModel.getUser().observe(this, new Observer() {
            @Override
            public void onChanged(@Nullable User user) {
                if (user.getEmail().length() > 0 || user.getPassword().length() > 0)
                    Toast.makeText(getApplicationContext(), "email : " + user.getEmail() + " password " + user.getPassword(), Toast.LENGTH_SHORT).show();
            }
        });

    }
}

위의 코드에서 observe 메서드는 MutableLiveData에 포함된 User 개체의 변경 사항을 찾습니다. 사용자 이름과 비밀번호가 포함된 토스트를 표시합니다. 이제 ObservableField를 LiveData로 완전히 교체해 보겠습니다.

ObservableField를 LiveData로 리팩터링

새 LoginViewModel.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.androidmvvmdatabindinglivedata;

import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import android.os.Handler;


public class LoginViewModel extends ViewModel {


    public MutableLiveData<String> errorPassword = new MutableLiveData<>();
    public MutableLiveData<String> errorEmail = new MutableLiveData<>();

    public MutableLiveData<String> email = new MutableLiveData<>();
    public MutableLiveData<String> password = new MutableLiveData<>();
    public MutableLiveData<Integer> busy;

    public MutableLiveData<Integer> getBusy() {

        if (busy == null) {
            busy = new MutableLiveData<>();
            busy.setValue(8);
        }

        return busy;
    }


    public LoginViewModel() {

    }

    private MutableLiveData<User> userMutableLiveData;

    LiveData<User> getUser() {
        if (userMutableLiveData == null) {
            userMutableLiveData = new MutableLiveData<>();
        }

        return userMutableLiveData;
    }


    public void onLoginClicked() {

        getBusy().setValue(0); //View.VISIBLE
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {


                User user = new User(email.getValue(), password.getValue());

                if (!user.isEmailValid()) {
                    errorEmail.setValue("Enter a valid email address");
                } else {
                    errorEmail.setValue(null);
                }

                if (!user.isPasswordLengthGreaterThan5())
                    errorPassword.setValue("Password Length should be greater than 5");
                else {
                    errorPassword.setValue(null);
                }

                userMutableLiveData.setValue(user);
                busy.setValue(8); //8 == View.GONE

            }
        }, 3000);
    }
}

이제 위의 클래스는 BaseObservable이 더 이상 필요하지 않으므로 ViewModel을 확장합니다. 이제 ObservableFields를 MutableLiveData로 변경했습니다. MutableLiveData의 변경 사항은 데이터 바인딩 덕분에 레이아웃에서 자동으로 업데이트됩니다. MainActivity.java 클래스는 이제 다음과 같이 업데이트되었습니다.

package com.journaldev.androidmvvmdatabindinglivedata;

import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.databinding.DataBindingUtil;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;

import com.journaldev.androidmvvmdatabindinglivedata.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

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


        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        LoginViewModel loginViewModel = ViewModelProviders.of(this).get(LoginViewModel.class);
        binding.setLoginViewModel(loginViewModel);
        binding.setLifecycleOwner(this);
        

        loginViewModel.getUser().observe(this, new Observer() {
            @Override
            public void onChanged(@Nullable User user) {
                if (user.getEmail().length() > 0 || user.getPassword().length() > 0)
                    Toast.makeText(getApplicationContext(), "email : " + user.getEmail() + " password " + user.getPassword(), Toast.LENGTH_SHORT).show();
            }
        });

    }
}

AndroidMVVMDataBindingLiveData

Github 프로젝트 링크