[Android] Jetpack - LiveData
LiveData๋ฅผ ํ๋ก์ ํธ์ importํ๋ ๋ฐฉ๋ฒ์ ํ๋ก์ ํธ์ ์ปดํฌ๋ํธ ์ถ๊ฐํ๊ธฐ ์ฐธ๊ณ
LiveData
- Observable Data Holder Class
- Lifecycle-aware
- Activity, Fragment, Service ๊ฐ์ ์ฑ ์ปดํฌ๋ํธ์ Lifecycle๊ณผ ์ฐ๊ด
- ํ์ฑํ(active) ์ํ(
STARTED
๋๋RESUMED
)์ธ Observer์๊ฒ๋ง ์ ๋ฐ์ดํธ๋ฅผ ์๋ฆผ
- LifecycleOwner์ Lifecycle์ด
DESTROYED
์ํ๊ฐ ๋๋ฉด Observer๋ฅผ ์ ๊ฑฐ- leak์ ๋ํ ๊ฑฑ์ ์์ด ์์ ํ๊ฒ LiveData๋ฅผ observe ๊ฐ๋ฅ
- Activity์ Fragment์์ ํนํ ์ ์ฉ
LiveData ์ฌ์ฉ๋ฒ์ ๋ํ ๋ ๋ง์ ์ ๋ณด๋ Work with LiveData objects
LiveData ์ฌ์ฉ์ ์ฅ์
1. UI์ ๋ฐ์ดํฐ ์ํ ์ผ์น ๋ณด์ฅ
2. ๋ฉ๋ชจ๋ฆฌ ๋ฆญ X
์ฐ๊ฒฐ๋ Lifecycle์ด destroyed ์ํ๊ฐ ๋๋ฉด ์์์ ์ ๊ฑฐ
3. ์ค์ง๋ ์กํฐ๋นํฐ๋ก ์ธํ ํฌ๋์ X
๋นํ์ฑํ ์ํ(์, ๋ฐฑ์คํ์ ์๋ Activity)์ด๋ฉด ์ด๋ค LiveData ์ด๋ฒคํธ๋ ๋ฐ์ง ์์
4. ์ง์ Lifecycle์ ๋ค๋ฃฐ ํ์ X
5. ํญ์ ์ต์ ์ ๋ฐ์ดํฐ
Lifecycle์ด ๋นํ์ฑํ -> ํ์ฑํ ์ํ๋ก ๋์์ค๋ฉด ๊ฐ์ฅ ์ต์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์) Activity๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋์ ์๋ค๊ฐ foreground๋ก ๋์์จ ์งํ ์ต์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์
6. ์ ์ ํ configuration changes ์ฒ๋ฆฌ ๊ฐ๋ฅ
ํ๋ฉด ํ์ ๊ณผ ๊ฐ์ configuration change๋ก ์ธํด Activity๋ Fragment๊ฐ ์ฌ์์ฑ๋ ๊ฒฝ์ฐ, ๊ฐ์ฅ ์ต์ ์ ์ ํจํ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ๋ฐ์
7. ์์ ๊ณต์
LiveData๋ฅผ ํ์ฅํ์ฌ ์์คํ ์๋น์ค๋ฅผ ๊ฐ์ธ๋ ์ฑ๊ธํค์ผ๋ก ๋ง๋ค์ด์ ์ฑ์์ ๊ณต์ ๊ฐ๋ฅ ์์คํ ์๋น์ค์ LiveData๋ฅผ ํ ๋ฒ ์ฐ๊ฒฐํด๋์ผ๋ฉด, ์์คํ ์๋น์ค๊ฐ ํ์ํ Observer๋ LiveData๋ง ๋ฐ๋ผ๋ณด๋ฉด ๋จ ์์ธํ ๋ด์ฉ์ ์๋ LiveData ํ์ฅ ํํธ ์ฐธ๊ณ
LiveData ์ฌ์ฉํ๊ธฐ
- ํน์ ํ์
์ ๋ฐ์ดํฐ๋ฅผ ๋ด๋
LiveData
์ธ์คํด์ค ์์ฑ (๋ณดํตViewModel
์์์) - Observer ์์ฑ ๋ฐ
onChanged()
๋ฉ์๋ ์ ์ (๋ณดํต Activity/Fragment ๊ฐ์ UI ์ปจํธ๋กค๋ฌ ์์์) observe()
๋ฉ์๋๋ฅผ ํตํด Observer์ LiveData ์ฐ๊ฒฐ (๋ณดํต Activity/Fragment ๊ฐ์ UI ์ปจํธ๋กค๋ฌ ์์์)
observeForever(Observer)๋ฅผ ์ด์ฉํ์ฌ LifecycleOwner ์์ด Observer๋ฅผ ๋ฑ๋กํ ์ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ Observer๊ฐ ํญ์ ํ์ฑํ ์ํ๋ก ์ธ์ง๋๊ธฐ ๋๋ฌธ์ ๋ณํ์ ๋ํ ์๋ฆผ์ ํญ์ ๋ฐ๊ฒ ๋ฉ๋๋ค. removeObserver(Observer) ๋ฉ์๋๋ฅผ ํตํด Observer๋ฅผ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
LiveData ์์ฑ
LiveData๋ Collections ๊ตฌํ์ฒด๋ฅผ ํฌํจํ ๋ชจ๋ ํ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ด์ ์ ์์ต๋๋ค. ๋ณดํต LiveData๋ ์๋ ์์ ์ฒ๋ผ ViewModel ์์ ์ ์ฅ๋๋ฉฐ, getter๋ฅผ ํตํด ์ ๊ทผ๋ฉ๋๋ค. ์ด๊ธฐ์ LiveData ๊ฐ์ ์ค์ ๋์ง ์์ต๋๋ค.
public class NameViewModel extends ViewModel {
// String ๋ฐ์ดํฐ๋ฅผ ๊ฐ๋ LiveData
private MutableLiveData<String> currentName;
public MutableLiveData<String> getCurrentName() {
if (currentName == null) {
currentName = new MutableLiveData<String>();
}
return currentName;
}
// Rest of the ViewModel...
}
LiveData๋ฅผ Activity/Fragment๊ฐ ์๋ ViewModel์ ์ ์ฅํ์ธ์.
- Activity/Fragment ํฌ๊ธฐ๊ฐ ๋๋ฌด ์ปค์ง๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด,
- ์ด์ UI ์ปจํธ๋กค๋ฌ์ ์ญํ ์ ๋ฐ์ดํฐ๋ฅผ ํ์ํ๋ ๊ฒ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ฒ์ด ์๋๋๋ค.
- Configuration change๊ฐ ๋ฐ์ํด๋ LiveData๋ ์ ์ง๋๋๋ก ํน์ Activity/Fragment์ LiveData ์ธ์คํด์ค๋ฅผ ๋ถ๋ฆฌํด์ผ ํฉ๋๋ค.
LiveData ๊ตฌ๋ ํ๊ธฐ
๋๋ถ๋ถ์ ๊ฒฝ์ฐ, ์ฑ ์ปดํฌ๋ํธ์ onCreate()
์์ ์ด ๊ตฌ๋
์ ์์ํ๊ธฐ์ ์ ์ ํฉ๋๋ค.
- Activity/Fragment ์ onReume()์ ์ ์ํ์ ๋์ ๋ถํ์ํ ํธ์ถ์ ๋ฐฉ์งํ๊ธฐ ์ํด
- Activity/Fragment๊ฐ ํ์ฑํ๋ ํ ์ต๋ํ ๋นจ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ํ์ํ๊ธฐ ์ํด
๊ตฌ๋ ์์์ ๋ํ ์์ ์ฝ๋:
public class NameActivity extends AppCompatActivity {
private NameViewModel model;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Other code to setup the activity...
// Get the ViewModel.
model = ViewModelProviders.of(this).get(NameViewModel.class);
// Create the observer which updates the UI.
final Observer<String> nameObserver = new Observer<String>() {
@Override
public void onChanged(@Nullable final String newName) {
// Update the UI, in this case, a TextView.
nameTextView.setText(newName);
}
};
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
model.getCurrentName().observe(this, nameObserver);
}
}
nameObserver.observe()
๊ฐ ํธ์ถ๋๋ฉด mCurrentName
์ ๊ฐ์ฅ ์ต์ ๊ฐ์ ์ฆ์ onChanged()
๋ก ์ ๋ฌ๋ฐ๊ฒ ๋ฉ๋๋ค.
LiveData์ ๊ฐ์ด ์์ง ์ค์ ๋์ง ์์๋ค๋ฉด onChanged()
๋ ํธ์ถ๋์ง ์์ต๋๋ค.
LiveData ์ ๋ฐ์ดํธ
LiveData๋ ์ ์ฅ๋ ๊ฐ์ ๊ฐฑ์ ํ๋ public ํจ์๋ฅผ ์ ๊ณตํ์ง ์์ต๋๋ค.
LiveData์ ์ ์ฅ๋ ๊ฐ์ ๋ณ๊ฒฝํ ํ์๊ฐ ์๋ ๊ฒฝ์ฐ setValue(T)
/postValue(T)
๋ฅผ ์ ๊ณตํ๋ MutableLiveData
๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
๋ณดํต MutableLiveData๋ ViewModel ์์์๋ง ์ฌ์ฉํ๊ณ , ViewModel์ Observer์ ๋ณ๊ฒฝ ๋ถ๊ฐ๋ฅํ LiveData๋ง ๊ณต๊ฐํฉ๋๋ค.
LiveData ์ ๋ฐ์ดํธ ์์ :
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String anotherName = "John Doe";
model.getCurrentName().setValue(anotherName);
}
});
๋ฉ์ธ ์ค๋ ๋์์ LiveData๋ฅผ ์
๋ฐ์ดํธ ํ๋ ๊ฒฝ์ฐ์๋ ๋ฐ๋์ setValue(T) ์ฌ์ฉ
๋ง์ฝ ์์ปค ์ค๋ ๋์์ ์ฝ๋๊ฐ ์คํ๋๋ค๋ฉด postValue(T) ์ฌ์ฉ
Room๊ณผ ํจ๊ป ์ฌ์ฉํ๊ธฐ
Room ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ LiveData๋ฅผ ๋ฐํํ๋ Observable ์ฟผ๋ฆฌ๋ฅผ ์ง์ํฉ๋๋ค. Room์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ๋ณ๊ฒฝ๋์ ๋ LiveData๋ฅผ ์ ๋ฐ์ดํธํ๋ ๋ฐ ํ์ํ ๋ชจ๋ ์ฝ๋๋ฅผ ์์ฑํด์ค๋๋ค. ์์ฑ๋ ์ฝ๋๋ ํ์ํ ๊ฒฝ์ฐ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์คํํฉ๋๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ์ UI๋ฅผ ํญ์ ๋๊ธฐ์ ์ผ๋ก ์ ์งํ๊ธฐ์ ์ ์ฉํฉ๋๋ค. Room๊ณผ DAO์ ๋ํด์๋ Room ๊ฐ์ด๋ ์์ ๋ ์์ธํ ๋ณผ ์ ์์ต๋๋ค.
Coroutine๊ณผ ํจ๊ป ์ฌ์ฉํ๊ธฐ
LiveData๋ Kotlin coroutines๋ ์ง์ํฉ๋๋ค. ์์ธํ ๋ด์ฉ์ Android App Component์ Kotlin coroutine ์ฌ์ฉํ๊ธฐ ์ฐธ๊ณ
LiveData ํ์ฅ
public class StockLiveData extends LiveData<BigDecimal> {
private StockManager stockManager;
private SimplePriceListener listener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
public StockLiveData(String symbol) {
stockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
stockManager.requestPriceUpdates(listener);
}
@Override
protected void onInactive() {
stockManager.removeUpdates(listener);
}
}
onActive()
: ํ์ฑํ Observer๊ฐ ์์ ๋ ํธ์ถ. ์ฃผ์ ๊ฐ๊ฒฉ ๋ณํ์ ๋ํ ๊ตฌ๋ ์ ์ด ๋ฉ์๋์์ ์์ํด์ผ ํฉ๋๋ค.onInactive()
: ํ์ฑํ Observer๊ฐ ํ๋๋ ์์ ๋ ํธ์ถ. Observer๊ฐ ํ๋๋ ์์ผ๋ฏ๋ก StockManager์ ์ฐ๊ฒฐ์ ์ ์งํ ํ์๊ฐ ์์ต๋๋ค.setValue(T)
LiveData๊ฐ Lifecycle์ ์๊ณ ์๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ Activity/Fragment/Service ๊ฐ ๊ณต์ ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
LiveData๋ฅผ ์ฑ๊ธํค์ผ๋ก ๋ง๋ ์์ :
public class StockLiveData extends LiveData<BigDecimal> {
private static StockLiveData sInstance;
private StockManager stockManager;
private SimplePriceListener listener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
@MainThread
public static StockLiveData get(String symbol) {
if (sInstance == null) {
sInstance = new StockLiveData(symbol);
}
return sInstance;
}
private StockLiveData(String symbol) {
stockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
stockManager.requestPriceUpdates(listener);
}
@Override
protected void onInactive() {
stockManager.removeUpdates(listener);
}
}
์๋ ์์ ์ฒ๋ผ Fragment์์ ์ฌ์ฉ:
public class MyFragment extends Fragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
StockLiveData.get(symbol).observe(this, price -> {
// Update the UI.
});
}
}
LiveData ๋ณํ
- Observer์ ์ ๋ฌํ๊ธฐ ์ ์ ๊ฐ์ ๋ณ๊ฒฝํ๊ณ ์ถ์ ๊ฒฝ์ฐ
- ๋ค๋ฅธ ๊ฐ์ ๊ธฐ๋ฐํ์ฌ ๋ค๋ฅธ LiveData๋ฅผ ๋ฐํํ๊ณ ์ถ์ ๊ฒฝ์ฐ
Lifecycle ํจํค์ง๋ ์ด๋ฐ ๊ฒฝ์ฐ๋ฅผ ์ํ Transformations
ํด๋์ค๋ฅผ ์ ๊ณตํจ
Transformations.map()
LiveData์ ๊ฐ์ ํจ์๋ฅผ ์ ์ฉ์ํค๊ณ , ๊ทธ ๊ฒฐ๊ณผ ๊ฐ์ ์ ๋ฌ
LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
user.name + " " + user.lastName
});
Transformations.switchMap()
map()๊ณผ ์ ์ฌํ๊ฒ LiveData์ ๊ฐ์ ํจ์๋ฅผ ์ ์ฉ์ํค๊ณ , ๊ทธ ๊ฒฐ๊ณผ ๊ฐ์ ์ ๋ฌํ์ง๋ง, switchMap()์ ๋ฆฌํด ํ์ ์ LiveData
private LiveData<User> getUser(String id) {
...;
}
LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );
Transformations๋ ๋ฐํ๋ LiveData๋ฅผ ๊ตฌ๋ ํ๋ Observer๊ฐ ์์ผ๋ฉด ๋ณํ์ ์ ์ฉํ์ง ์์ต๋๋ค. Transformations๋ lazy ์ฐ์ฐ์ด๊ธฐ ๋๋ฌธ์, Lifecycle๊ณผ ์ฐ๊ด๋ ๋์์ ๋ช ์์ ์ธ ํธ์ถ์ด๋ ์์กด์ฑ์ ํ์์์ด ๋์ํฉ๋๋ค.
ViewModel ๊ฐ์ฒด ์์์ LiveData๊ฐ ํ์ํ ๊ฒฝ์ฐ, Transformations๋ ๋ ์ข์ ๋ฐฉ์์ด ๋ ์ ์์ต๋๋ค. UI ์ปดํฌ๋ํธ๊ฐ ์ฃผ์์ ๊ทธ ์ฃผ์์ ํด๋นํ๋ ์ฐํธ ๋ฒํธ๋ฅผ ๋ฐ๋๋ค๊ณ ๊ฐ์ ํ ๋, ์ ๋ชจ๋ฅด๊ณ ์์ฑํ ViewModel ์ฝ๋๋ ์๋์ ๊ฐ์ ์ ์์ต๋๋ค:
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);
}
}
์ด๋ ๊ฒ ํ๋ฉด UI ์ปดํฌ๋ํธ์์ getPostalCode()๋ฅผ ํธ์ถํ ๋๋ง๋ค ์ด์ LiveData์ ๊ตฌ๋ ์ ํด์งํ๊ณ , ์๋ก์ด LiveData๋ฅผ ๊ตฌ๋ ํด์ผ ํฉ๋๋ค. ๊ฒ๋ค๊ฐ UI ์ปดํฌ๋ํธ๊ฐ ์ฌ์์ฑ๋์ ๋, ์ด์ ํธ์ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ repository.getPostCode()๋ฅผ ๋ ๋ค์ ํธ์ถํ๊ฒ ๋ฉ๋๋ค.
์ฃผ์์ ๋ฐ๋ผ ์ฐํธ ๋ฒํธ๋ฅผ ์ฐพ๋ ๋ก์ง์ Transformations๋ฅผ ์ด์ฉํ์ฌ ์๋์ฒ๋ผ ๊ตฌํํ ์ ์์ต๋๋ค:
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๋ addressInput์ ๋ณํ์ผ๋ก ์ ์๋ฉ๋๋ค. postalCode์ ์ฐ๊ฒฐ๋ ํ์ฑํ ์ํ์ Observer๊ฐ ์๊ณ , addressInput์ด ๋ณ๊ฒฝ๋๋ฉด postalCode์ ๊ฐ์ ๋ค์ ๊ณ์ฐํฉ๋๋ค.
์๋ก์ด ๋ณํ ์์ฑํ๊ธฐ
์์ฃผ ๋ค์ํ ๋ณํ ๋ฐฉ๋ฒ์ด ์์ง๋ง, ๋ํดํธ๋ก ์ ๊ณต๋์ง๋ ์์ต๋๋ค.
์ํ๋ ๋ณํ์ ๊ตฌํํ๊ณ ์ถ์ ๊ฒฝ์ฐ MediatorLiveData
๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
์์ธํ ๋ด์ฉ์ https://developer.android.com/reference/androidx/lifecycle/Transformations.html?hl=ko
์ฌ๋ฌ LiveData ํฉ์น๊ธฐ
MediatorLiveData๋ LiveData์ ์๋ธํด๋์ค๋ก, ์ฌ๋ฌ LiveData๋ฅผ ํฉ์น ์ ์๊ฒ ํด์ค๋๋ค. MediatorLiveData๋ LiveData๋ค ์ค ํ๋๋ผ๋ ๋ณ๊ฒฝ๋๋ฉด Observer์ ๋ณ๊ฒฝ์ ์๋ฆฝ๋๋ค.
์, ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ ๋คํธ์ํฌ๊ฐ ๋ณ๊ฒฝ๋ ๋ UI๊ฐ ๋ณ๊ฒฝ๋๋ค๋ฉด, ์๋์ฒ๋ผ MediatorLiveData์ LiveData๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค.
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ๋ฐ์ดํฐ์ ๋ํ LiveData
- ๋คํธ์ํฌ๋ก ์ ๊ทผํ๋ ๋ฐ์ดํฐ์ ๋ํ LiveData
๋ ์์ธํ ์์ ๋ App Architecture ๊ฐ์ด๋์ Addendum: exposing network status ํํธ ์ฐธ๊ณ
์ถ๊ฐ ์๋ฃ
Samples
- Sunflower, a demo app demonstrating best practices with Architecture Components
- Android Architecture Components Basic Sample
Codelabs
Blogs
- ViewModels and LiveData: Patterns + AntiPatterns
- LiveData beyond the ViewModelโโโReactive patterns using Transformations and MediatorLiveData
- LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)
Videos
Comments