날마다 새롭게 또 날마다 새롭게

Android lifecycle-aware components ViewModel과 LiveData 사용해보기 본문

프로그래밍/Android

Android lifecycle-aware components ViewModel과 LiveData 사용해보기

아무유 2017. 6. 8. 15:21


ViewModel과 LiveData를 사용해보자

ViewModel

ViewModel 은 UI 관련된 data를 저장하고 관리하기 하는 클래스이다. 
보통 Activity나 Fragment가 다시 recreate 되는 상황에(screen rotation 같은) 갖고 있던 data들은 clear가 된다. 이러한 상황에서 data를 잃지 않고 view를 구성하기 위해서 onSaveInstanceState 나 create 될 때, data를 로드하는 구현이 필요하다.

ViewModel을 사용하면 이런 구현이 필요 없어진다. ViewModel은 Activity/fragment lifecycle을 따라 동작하는데 생성된 시점에서 Activity/fragment가 finish() 되기 전까지 데이터를 유지하는 기능을 가지고 있다.

ViewModel 사용 방법을 알아보자

사용방법

download

viewmodel 을 사용하기 위해서 dependencies 를 추가한다. 
project/build.gradle

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

app/build.gradle

compile "android.arch.lifecycle:runtime:1.0.0-alpha1"
compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"

create

‘ViewModelProviders’를 사용해서 ViewModel 객체를 생성한다.

// this = Fragment / Activity
MyViewModel myviewmodel = ViewModelProviders.of(this).get(ChronometerViewModel.class)

예제1

예제를 통해 자세한 사용방법을 알아보자. 전체 소스는 codelab을 참고하면 된다. 

step2/ChronoActivity2 
Activity가 onCreate될 때, ‘ChronometerViewModel’을 생성하고 startdate를 가져와 Chronometer에 셋팅한다

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // The ViewModelStore provides a new ViewModel or one previously created.
    ChronometerViewModel chronometerViewModel
            = ViewModelProviders.of(this).get(ChronometerViewModel.class);

    // Get the chronometer reference
    Chronometer chronometer = (Chronometer) findViewById(R.id.chronometer);

    if (chronometerViewModel.getStartDate() == null) {
        // If the start date is not defined, it's a new ViewModel so set it.
        long startTime = SystemClock.elapsedRealtime();
        chronometerViewModel.setStartDate(startTime);
        chronometer.setBase(startTime);
    } else {
        // Otherwise the ViewModel has been retained, set the chronometer's base to the original
        // starting time.
        chronometer.setBase(chronometerViewModel.getStartDate());
    }

    chronometer.start();
}

step2/ChronometerViewModel 
startDate getter/setter, view에서 호출한다.

public class ChronometerViewModel extends ViewModel {

    @Nullable
    private Long startDate;

    @Nullable
    public Long getStartDate() {
        return startDate;
    }

    public void setStartDate(final long startDate) {
        this.startDate = startDate;
    }
}

앱을 실행한 후, 스마트폰을 좌우로 회전시켜보면 view는 새로 create되지만 Chronometer는 초기 값을 잃어버리지 않고 표시되는 것을 확인할 수 있다. 홈버튼으로 나갔다 다시 앱으로 돌아와도 값은 유지된다. 백버튼으로 나갔다 앱을 실행하면 값이 초기화 되는 것을 볼 수 있다.

이렇게 ViewModel은 Activity/Fragment 가 finish 되서 destroy 되기 전까지는 값을 유지한다.

예제2

LiveData를 사용법을 알아보기 전에 ‘Step3’ 을 조금 수정해서 ViewModel 만 사용한 예제를 살펴보자. 
참고로 이 코드는 codelab 프로젝트에 없다.

step3/ChronoActivity3rev 
ViewModle을 생성하고 ViewModel 로부터 Update 이벤트를 받아 view를 갱신한다.

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

    setContentView(R.layout.chrono_activity_3);

    viewModel = ViewModelProviders.of(this).get(ChronometerViewModelrev.class);
    viewModel.setMyListener(new ChronometerViewModelrev.MyListener() {
        @Override
        public void onUpdate(long aLong) {
            String newText = ChronoActivity3rev.this.getResources().getString(
                    R.string.seconds, aLong);
            ((TextView) findViewById(R.id.timer_textview)).setText(newText);
        }
    });
}

step3/ChronometerViewModelrev 
‘Timer’를 사용해서 1초마다 이벤트를 Activity로 전달한다.

public ChronometerViewModelrev() {
    mInitialTime = SystemClock.elapsedRealtime();
    // Update the elapsed time every second.
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000;
            // setValue() cannot be called from a background thread so post to main thread.
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    if(myListener != null) myListener.onUpdate(newValue);

                }
            });
        }
    }, ONE_SECOND, ONE_SECOND);
}

앱을 실행해보면 예제1과 마찬가지로 스마트폰을 좌우로 회전하거나 홈화면으로 나갔다 돌아와도 값이 유지되는 것을 확인할 수 있다. 그런데 Activity의 onUpdate 리스너에 로그를 남겨보면 홈화면에 나가있는 상태에서도 이벤트를 받아 view를 갱신하는 것을 확인할 수 있다.

view가 보이지 않는 상태에서는 데이터는 유지되더라도 view를 갱신할 필요는 없다. 이런 아쉬움은 LiveData를 사용하면 없어진다.

LiveData

LiveData 는 acitivity/fragment lifecyle에 따라 동작하며 STARTED/RESUMED state일 때만 데이터 변화 이벤트를 observer 에게 전달한다.

사용방법

download

ViewModel과 설정이 동일하다

observe

LiveData는 observe 호출을 통해서 데이터 갱신 이벤트를 받을 수 있다.

LiveData.get(getActivity()).observe(this, location -> {
                   // update UI
                });

예제3

LiveData를 사용한 예제를 살펴보자. 
step3/ChronoActivity3 
‘LiveDataTimerViewModel’에서 LiveData를 호출해 observe 하는 것을 볼 수 있다. elapsedTime 에 값이 변화가 생기면 onChanged가 호출된다.

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

    setContentView(R.layout.chrono_activity_3);
    mLiveDataTimerViewModel = ViewModelProviders.of(this).get(LiveDataTimerViewModel.class);
    subscribe();
}

private void subscribe() {
    final Observer<Long> elapsedTimeObserver = new Observer<Long>() {
        @Override
        public void onChanged(@Nullable final Long aLong) {
            String newText = ChronoActivity3.this.getResources().getString(
                    R.string.seconds, aLong);
            ((TextView) findViewById(R.id.timer_textview)).setText(newText);
            Log.d("ChronoActivity3", "Updating timer");
        }
    };
    mLiveDataTimerViewModel.getElapsedTime().observe(this, elapsedTimeObserver);
}

step3/LiveDataTimerViewModel 
Timer를 사용해 1초마다 LiveData인 mElapsedTime 에 값을 변경한다.

public LiveDataTimerViewModel() {
    mInitialTime = SystemClock.elapsedRealtime();
    // Update the elapsed time every second.
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000;
            // setValue() cannot be called from a background thread so post to main thread.
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    mElapsedTime.setValue(newValue);
                }
            });
        }
    }, ONE_SECOND, ONE_SECOND);
}

앱을 실행해보면 예제2와 마찬가지로 스마트폰을 좌우로 회전하거나 홈화면으로 나갔다 돌아와도 값이 유지되는 것을 확인할 수 있다. 그런데 예제2와는 다르게 홈화면에 나가있는 상태에서는 Activity의 onChanged 리스너의 로그가 발생하지 않는 것을 확인할 수 있다.

LiveData는 lifecycle이 STARTED/RESUMED state일 때만 dispatch하기 때문에 onChanged는 호출되지 않는다.

결론

Lifecycle-aware components인 LiveData, ViewModel을 사용하면 그 동안 Data-view 연결하는 작업을 할 때, exception이 발생하지 않도록 lifecycle을 신경써서 구현해야 했던 작업들을 줄일 수 있고 앱의 성능을 높이는데 도움이 될 것 같다.


Comments