개요

LiveData 는 Data의 변경을 관측할 수 있는 Data Holder 클래스 입니다.
일반적인 Observable과는 다르게 LiveData는 생명주기를 알고 있습니다.(Lifecycle-Aware)
액티비티나, 프레그먼트, 서비스 등과 같은 컴포넌트의 생명주기(Lifecycle)를 존중하고 그에따라 LiveData는 활성상태(active)일때만 데이터를 업데이트 합니다.
활성상태란 STARTED 또는 RESUMED를 의미합니다.

Lifecycle State

CREATED Created state for a LifecycleOwner.
DESTROYED Destroyed state for a LifecycleOwner.
INITIALIZED Initialized state for a LifecycleOwner.
RESUMED Resumed state for a LifecycleOwner.
STARTED Started state for a LifecycleOwner.

LiveData 객체에 LifecycleOwner와 함께 옵저버를 등록할 수 있습니다.
LifecycleOwner가 추가된 LiveData는 DESTROYED 상태가 되면 자동으로 제거 됩니다.
이점은 메모리 누수에 관해 걱정할 필요가 없으며, 특히 UI컴포넌트(액티비티, 프레그먼트) 등을 사용할 때 유용합니다.
또한 UI컨트롤러 객체가 파괴 되어도 LiveData는 즉시 구독이 취소됩니다.(Unsubscribed)
추가적으로 LiveData는 onActive()와 onInactive() 메소드를 가지고 있는데 이 메서드를 활용하여 활성화된 Observer수가 변경될 때 알림을 받습니다.
이로 인해 LiveData는 이 객체를 관찰중인(참조중인) Observer가 없을때 무거운 리소스를 해제 할 수 있습니다.

onActive() : 활성화된 Observer수가 0에서 1로 바뀔때 호출 된다.
onInactive() : 활성화된 Observer수가 1에서 0으로 바뀔때 호출 된다.

LiveData의 장점

  • Data와 UI간 동기화
    앱의 생명주기의 변경에 따라 UI를 매번 갱신할 필요가 없고, 옵저버객체내에서 UI를 갱신하는것으로 통합시킬 수 있다.
  • 메모리 누수 방지
    옵저버는 Lifecycle 객체와 바인딩 되어있으며 연관된 생명주기가 소멸시에 옵저버도 같이 메모리상에서 정리가 된다.
  • 액티비티가 종료됨으로 인한 크래쉬들을 방지 할 수 있다.
    옵저버의 생명주기가 비활성화 된다면, LiveData의 데이터 변경 이벤트를 받지 않는다.
  • 수동적인 생명주기 관리가 필요없다
    UI컴포넌트는 그저 관련된 데이터를 관측(Observe)만 하면 됩니다. onStop 이나 onResume에서 해제, 등록 하는 절차를 할필요가 없습니다.
    LiveData가 자동으로 UI컴포넌트의 생명주기를 알고 관리합니다.
  • 항상 최신의 데이터를 유지할 수 있다.
    생명주기가 만약 비활성화(Inactive)상태가 되어도 최신의 데이터를 활성(active)상태일때 다시 받습니다. 
  • UI컨트롤러의 상태변경이 쉽다.
    UI컴포넌트가 환경설정(configuration)의 문제로,예를 들면 가로-세로 화면전환, 재생성(recreated)되거나 하여도 즉시 최신의 데이터를 받아 옵니다.
  • 자원공유
    LiveData를 상속하여 자신만의 LiveData클래스를 구현할 수 있고, 싱글톤 패턴을 이용하여 시스템 서비스를 둘러싸면(wrap) 앱 어디에서나 공유 할 수 있습니다.

LiveData 사용하기

  1. LiveData는 보통 ViewModel내에서 함께 쓰입니다.
  2. 옵저버 객체를 만들고 onChange() 메소드를 통해 LiveData의 data변경을 감지 할 수 있습니다. 보통 Observer는 프레그먼트나 액티비티 같은 UI컨트롤러에서 만듭니다.
  3. observe()메소드를 통해 Observer를 붙일 수 있으며, observe()는 LifecycleOwner 객체를 포함해야 합니다. (Activity 또는 Fragment가 LifeCycleOwner를 implement 하고 있다.)
Note: observeForever(Observer)를 통해 LifeCycleOwner없이 Observer를 등록할 순 있지만 , 이경우에는 옵저버는 항상 active상태이므로 데이터변화를 항상 받는다. removeObserver(Observer) 메소드를 통해 Observer를 제거 할 수 있다.

LiveData의 data가 변경될때 LifecycleOwner가 활성화 되어있는한 등록된 모든 옵저버들에게 이벤트를 보내게 된다.
LiveData는 UI컨트롤러 옵저버들이 데이터 변화를 감지하고 알맞게 UI를 자동으로 변경할 수 있도록 해준다.

LiveData 객체 만들기

LiveData는 Collections, List와 같은 어떤 data건 감쌀 수 있다. 보통 ViewModel 내에 저장되어서 사용된다.

public class NameViewModel extends ViewModel {
	//String을 감싼 LiveData만들기
	private MutableLiveData<String> mCurrentName;
    public MutableLiveData<String> getCurrentName() {
        if (mCurrentName == null) {
            mCurrentName = new MutableLiveData<String>();
        }
        return mCurrentName;
    }
}
//초기값은 아직 설정되지 않았다.
Note:LiveData와 ViewModel은 Activity나 Fragment가 거대해지는것을 피한다. UI Controller는 그저 data를 어떻게 보여주고 표현하느냐에만 신경을 쓰면된다. 예전처럼 data를 직접 관리할 필요가 없다. 직접 관리하면 LifeCycle과 관련하여 처리해야할 것이 많아진다.(예를 들면 Activity의 Screen Rotation이라든가…) 그래서 UIController와 LiveData를 디커플링하기 위해 ViewModel이 LiveData를 관리하고 UI컨트롤러는 그저 옵저버를 통해 UI갱신만 하면된다. 그러면 configuration의 변화에서도 LiveData 객체는 살아남을수 있다.

LiveData 데이터 변경 관측하기

대부분의 경우 onCreate()에서 observe()를 하면 적당하다. 이유는 다음과 같다

  • 중복 호출을 방지할 수 있다. 예를 들면 onResume()이라던가 하는곳에 observe()를 배치하면 중복호출을 배제하기 힘들다.
  • 컴포넌트가 STARTED 상태가 되었을때 바로 Activity나 fragment에서 Data를 표현해 줄 수 있기 때문이다.

일반적으로 LiveData는 data가 변경되었을때 옵저버를 활성화 시키기 위에 data의 변경 사항을 전달한다. 이런 사항에 대한 하나의 예외사항으로 옵저버들은 inactive에서 active상태로 변경될때도 변경 알림을 받는다.
더나아가 만약 옵저버가 inactive에서 active로 두번 변경 되면 마지막에 변경된 하나의 변경 알림만 받는다.

public class NameActivity extends AppCompatActivity {
    private NameViewModel mModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		//ViewModel 얻기
        mModel = ViewModelProviders.of(this).get(NameViewModel.class);
        // UI를 변경할 수 있는 옵저버 만들기
        final Observer<String> nameObserver = new Observer<String>() {
            @Override
            public void onChanged(@Nullable final String newName) {
                // 이 경우에는 이곳에서 TextView의 값을 변경한다.
                mNameTextView.setText(newName);
            }
        };
        // observe()메소드를 통해 LiveData를 감지하고 LifecycleOwner와 Observer를 넘겨준다.
        mModel.getCurrentName().observe(this, nameObserver);
    }
}

LiveData객체의 data를 변경해보자

LiveData는 데이터를 변경할 수 있는 메소드가 public하게 있지 않다. 그럼 어떻게 해야할까?
정답은 LiveData를 상속한 MutableLiveData를 쓰도록하자.
MutableLiveData는 setValue(T) 와 postValue(T) 메소드를 가지고 있어서 이를 통해 데이터를 변경할 수 있다.

mButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        String anotherName = "Charles";
        mModel.getCurrentName().setValue(anotherName);
    }
});
Note:setValue(T)는 반드시 mainThread에서 호출해야한다. 만약 subThread에서 호출한다면 postValue(T)를 통해 LiveData를 업데이트 하도록 하자.

LiveData와 Room 같이 써보기

Room에서 return 값을 LiveData로 갖는 Observable queries가 가능하다.
데이터베이스가 변경될때 LiveData도 같이 업데이트 된다. 필요하다면 쿼리로 인해 생성된 코드는 비동기적으로 백그라운드 쓰레드에서 생성된다.

LiveData 상속하여 확장하기

public class StockLiveData extends LiveData<BigDecimal> {
    private StockManager mStockManager;
    private SimplePriceListener mListener = new SimplePriceListener() {
        @Override
        public void onPriceChanged(BigDecimal price) {
            setValue(price);
        }
    };
    public StockLiveData(String symbol) {
        mStockManager = new StockManager(symbol);
    }
    @Override
    protected void onActive() {
        mStockManager.requestPriceUpdates(mListener);
    }
    @Override
    protected void onInactive() {
        mStockManager.removeUpdates(mListener);
    }
}
  • onActive() 메소드는 LiveData가 활성화된 옵저버를 갖고 있을때 호출된다.
  • onInactive() 메소드는 LiveData가 활성화된 옵저버를 하나도 갖고 있지 않을때 호출된다.
  • setValue()는 LiveData의 값을 변경하고 옵저버에 대해 변경사항을 알려준다

위의 StockLiveData를 다음과 같이 써보자

public class MyFragment extends Fragment {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        LiveData<BigDecimal> myPriceListener = ...;
        myPriceListener.observe(this, price -> {
            // Update the UI.
        });
    }
}

observe()메소드를 통해 fragment와 옵저버를 넘겨 주고 있다. fragment는 LifecycleOwner로 쓰인다. fragment를 넘겨주는 이유는 다음과 같다.

  • Lifecycle객체가 active상태가 아니라면, 옵저버는 data변경에도 호출되지 않는다.
  • Lifecycle객체가 파괴된다면, 옵저버도 자동으로 제거 된다.

LiveData가 라이프사이클을 알고있는 (Lifecycle-aware) 이유가 바로 fragment, activity 나 service같은 객체를 공유 하기 때문이다.
두개이상의 프래그먼트나 액티비티에서도 동일한 LiveData객체를 동시에 observe() 할 수 있다.

LiveData의 변형

아마도 개발자는 LiveData의 data를 변경하거나 그 data를 기반으로하는 다른 부가적인 것들을 Observer를 통해 얻고 싶을때도 있을것이다.
LiveData는 그런 시나리오도 이미 예측하고 준비해놨지 흠흠. 

  • Transformation.map()
    LiveData<User> userLiveData = ...;
    LiveData<String> userName = Transformations.map(userLiveData, user -> {
        user.name + " " + user.lastName
    });
  • Transformations.switchMap()
    private LiveData<User> getUser(String id) {
      ...;
    }
    LiveData<String> userId = ...;
    LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );

    map()과 switchMap()은 비슷해보이지만 다르다. map()에서는 두번째 인자의 함수의 리턴값이 object면 되지만 switchMap()에서는 리턴값이 반드시 LiveData이여야 한다.

Transformation메소드를 옵저버의 Lifecycle을 기반으로 하여 정보를 가져오는데 쓸수 있다는것을 알았다. Transformation은 옵저버가 LiveData를 반환하는것을 감시하고 있지 않는한 별도의 작업을 하지 않는다. 왜냐면 Transformation은 lazy하게 작동되기 떄문이다. 생명주기와 관련된 행동은 추가적인 명백한 호출또는 의존성을 요구하는것 없이 암묵적으로 물려주게 된다.
만약 Lifecycle객체가 ViewModel 내에서 필요하다면, Transformation은 적합한 방법일것이다. 예를들면 Activity나 Fragment가 주소값을 가지고 있고 주소를 통해 우편번호를 얻는 로직을 짠다 치면, LiveData와 ViewModel에서 간략히 배운이상 아마 이제부터는 ViewModel을 사용하려 할거고, 초심자인 당신은 이렇게 짤게 분명하다.

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    public MyViewModel(PostalCodeRepository repository) {
       this.repository = repository;
    }
    private LiveData<String> getPostalCode(String address) {
       // 이.렇.게.짜.면.아.니.되.오.
       return repository.getPostCode(address);
    }
}

이렇게 짜면 UI컨트롤러에서는 이전 LiveData을 해제하는 작업이 필요하고 다시 새로운 객체에 대해서 getPostalCode()가 호출될때마다 등록하는 과정이 필요하다. 게다가 UI컨트롤러가 재생성 되기라도 한다면 또다른 getPostCode()를 호출하게 되고 이전의 데이터는 쓸모가 없어진다.(중복작업)
그럼 어떻게 해야할까?
우편번호검색을 주소를 일력하는것에 대한 변형(transformation)으로 구현하면 된다. 다음 코드를 확인해보자

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    private final MutableLiveData<String> addressInput = new MutableLiveData();
    public final LiveData<String> postalCode =
            Transformations.switchMap(addressInput, (address) -> {
                return repository.getPostCode(address);
             });
  public MyViewModel(PostalCodeRepository repository) {
      this.repository = repository
  }
  private void setInput(String address) {
      addressInput.setValue(address);
  }
}

이렇게 짠다면 postalCode 필드는 public이며 final 이다 왜냐면 변경될 일이 없으니까!
postalCode필드는 addressInput의 변형으로 정의되었으며, 이것이 의미하는바는 repository.getPostCode()메소드가 addressInput data의 변경시점마다 호출된다는 점이다.
Observer가 추가되기 전까지는 어떠한 작업도 일어나지 않는다.
이런 메커니즘은 로우레벨에서 필요할때 LiveData를 생성하고 뒤늦게(lazily) 계산되어지는것을 허용하게 된다. ViewModel은 쉽게 LiveData의 객체를 참조하고 변형규칙을 최상위에서 정의할 수 있다.

새로운 변형(Transformation) 규칙 생성하기

아마 앱개발을 하다보면 쓸만한 다양한 변형하는 방법이 존재할 것이다. 하지만 그런것들이 기본적으로 제공되는것들이 아닐수도 있다.
자신만의  변형을 구현하려면 MediatorLiveData를 이용하면된다. MediatorLiveData는 다른 LiveData변화를 감지하며 그 이벤트들을 올바르게 전달합니다.

여러개의 LiveData 소스를 합치기

MediatorLiveData는 LiveData의 subclass로서 복수개의 LiveData를 합칠수 있게한다.
MediatorLiveData의 옵저버들은 언제든지 source가 변경될때마다 이벤트가 호출된다.
예를 들면 만약 UI에 있는 LiveData객체가 네트워크나 또는 데이터베이스로부터 업데이트가 되면, 아마 MediatorLiveData를 사용해볼수도 있다.
액티비티나 프레그먼트에서 필요한건 오로지 MediatorLiveData의 감지 뿐이다. 이를통해 여러개의 소스로부터 변경사항을 감지 할 수 있다.
자세한 사항은 https://developer.android.com/jetpack/docs/guide#addendum 이 섹션을 확인해보도록 하자.

Snackbar와 함께 LiveData사용하기

https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

Buy me a coffeeBuy me a coffee
카테고리: Android

1개의 댓글

chopinFrog · 2019년 12월 7일 8:52 오전

LiveData 관련된 정보를 찾다가, 댓글 남깁니다. 너무 정리를 잘해 주셨네요. 감사합니다.

답글 남기기

이메일은 공개되지 않습니다.