본문으로 바로가기

Android Architecture Components #3 - LiveData

category 개발이야기/Android 2017. 10. 15. 01:00
반응형

LiveData는 data holder 클래스로 data가 가진 값들을 확인할 수 있으며, 다른 observerable과 다르게 app component의 lfe cycle에 따라 observing 여부를 지정할 수 있습니다.
LiveData는 LifeCycle이 STARTED나 RESUMED인 상태를 Observer가 active된 상태로 간주합니다.
 
LiveData는 주어진 lifecycle에 의해서 관찰될수 있습니다.
이는 Observer와 lifecycle이 pair로 등록되며, 등록된 Observer는 LifeCycleOwner의 상태가 active(STARTED or RESUMED)일때 wrapping된 data가 변경되면 noti를 받을 수 있습니다.
observe(LifecycleOwner owner, Observer<T> observer)가 아닌 observeForever(Observer<T> observer)로 등록되면 lifeCycle과 관계없이 데이터의 변경이 생길때 무조건 noti를 받습니다. 이렇게 등록된 observer는 removeObserver로 해체 시켜야 합니다.

Location 정보를 받는 예제

public class LocationLiveData extends LiveData<Location> {
    private LocationManager locationManager;
 
    private SimpleLocationListener listener = new SimpleLocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            setValue(location);
        }
    };
 
    public LocationLiveData(Context context) {
        locationManager = (LocationManager) context.getSystemService(
                Context.LOCATION_SERVICE);
    }
 
    @Override
    protected void onActive() {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
    }
 
    @Override
    protected void onInactive() {
        locationManager.removeUpdates(listener);
    }
}
  • onActive() 함수는 LiveData가 active observer를 하나라도 가질때 호출. 따라서 예제처럼 location update를 시작해야 한다.
  • onInactive() 함수는 LiveData가 active observer를 하나도 가지지 않았을 때 호출. 따라서 변경사항이 없기 대문에 LocationManager를 떼내도록 한다.
  • setValue() 함수가 호출되면 LiveData의 값을 업데이트 함. 또한 active observer에가 notify 한다.

아래와 같이 Fragment에서 등록할 수 있습니다.
public class MyFragment extends LifecycleFragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LiveData<Location> myLocationListener = ...;
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.observe(this, location -> {
                    // update UI
                });
            }
        });
    }
}

observe()는 LifecycleOwner와 같이 등록되어, life cycle에 따라 observer의 동작이 결정됩니다.

  • 등록된 LifeCycle함수가 active state일때 (STARTED or RESUMED) 데이터가 변경되면 noti를 발생함. (그외 상태에서는 noti가 호출되지 않음)
  • LifeCycle이 DESTROYED 되면 자동으로 observer가 remove

LiveData는 여러 activity나 fragement에서 공유될수 있습니다.
아래 예제는 LiveData를 Singleton으로 만들어 사용합니다. (그래야 여러 UI Component에서 사용하기에 효율적입니다.)
따라서 이때는 Context를 Application Context를 사용해야 합니다. (사실 아니라도 상관은 없을수 있겠지만, 여러 UI component가 공용사용하는 형태로 사용하기 때문)

public class LocationLiveData extends LiveData<Location> {
    private static LocationLiveData sInstance;
    private LocationManager locationManager;
 
    @MainThread
    public static LocationLiveData get(Context context) {
        if (sInstance == null) {
            sInstance = new LocationLiveData(context.getApplicationContext());
        }
        return sInstance;
    }
 
    private SimpleLocationListener listener = new SimpleLocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            setValue(location);
        }
    };
 
    private LocationLiveData(Context context) {
        locationManager = (LocationManager) context.getSystemService(
                Context.LOCATION_SERVICE);
    }
 
    @Override
    protected void onActive() {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
    }
 
    @Override
    protected void onInactive() {
        locationManager.removeUpdates(listener);
    }
}


Singleton으로 만들었으니, fragment에서는 아래와 같이 호출할 수 있습니다.

public class MyFragment extends LifecycleFragment {
    public void onActivityCreated (Bundle savedInstanceState) {
        Util.checkUserStatus(result -> {
            if (result) {
                LocationLiveData.get(getActivity()).observe(this, location -> {
                   // update UI
                });
            }
        });
  }
}

특징

  • Memory leak 방지: Observer는 Lifecycle과 연결되기 때문에 Lifecycle과 함께 cleanup 된다.
  • activity stop에 따른 crash 방지: inActive 상태에서는 이벤트를 받지 않기 때문에 stop 상태에서 수행하면서 발생하는 crash를 방지한다
  • Always up to date data: LifeCycle이 재 시작되면 가장 최신 데이터를 받는다
  • Proper configuration change: activity나 fragment가 재생성된느 경우 최신 데이터를 즉각 받을 수 있다
  • Sharing Resources: singleton으로 만들어서 system service(resource)에 한번만 연결하고, 관련 observer들을 전부 커버할 수 있다
  • No more manual lifecycle handling: lifecycle에 대한 처리를 직접할 필요가 없다

Transformation

변경된 LiveData를 observer에게 전달하기 전에 데이터를 가공하고 싶거나, 다른 LiveData 를 만들고 싶다면 Transformations 을 이용합니다.
Transformations.map() 은 아래와 같이 정의되어 있습니다.

Api

Definition

Description

 Transformations.map()

 LiveData<Y> map (LiveData<X> source, Function<X, Y> func)

LiveData를 return하며, source에 이벤트가 생길때마다 main thread에서 function이 수행된다.

 Transformations.switchMap()

 LiveData<Y> switchMap (LiveData<X> trigger, 

                Function<X, LiveData<Y>> func)

trigger LiveData가 변경에 따라 이벤트를 발생 시키면 function을 적용하여 결과를 새로만든 새로운 LiveData에 set한다. 또한 이때 새로운 LiveData에 등록된 observer들에게 재전송 된다.


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


Transformation은 lifeCycle에 따라 LiveData가 발생될때 수행되므로 laze calculation을 구현할 수 있습니다.

또한 이는 ViewModel 내부에서 LifeCycle을 필요로 하는경우 유용하게 쓰일 수 있겠죠~

우편번호를 얻는 예제를 보면 아래와 같습니다.

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    public MyViewModel(PostalCodeRepository repository) {
       this.repository = repository;
    }
 
    private LiveData<String> getPostalCode(String address) {
       // DON'T DO THIS
       return repository.getPostCode(address);
    }
}

위 코드는 우편번호를 얻는 코드(getPostalCode(String address))를 수행하면 새로운 LiveData를 반환합니다.

따라서 UI는 기존 LiveData를 unregiter 하고 새로운 LiveData object에 register 해야합니다.

또한 UI가 재생성 되는 경우에도 repository.getPostCode(address)를 호출하여야 합니다.

이런경우 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는 final로 지정되어 변경되지 않습니다. addressInput이 변경될때 등록되어있는 active한 observer가 있다면 function이 수행되고, 그렇지 않으면 수행되지 않습니다.

반응형