본문 바로가기
개발/Android

[Android] LiveData에 대해 알아보자

by tempus 2022. 5. 10.
반응형

앞선 글에서 ViewModel에서 알아보았습니다. 이제 우리는 ViewModel이 어떤 역할을 하는지 알고 있습니다. ViewModel을 통해 UI 관련 데이터를 관리해주고 View에 데이터를 그려줄 수 있다는 것을 압니다. 하지만 여기서 문제가 생깁니다. 한 번 그려진 View는 데이터가 변화했다고 일반적으로 다시 그려주지 않습니다. 그렇다고 Activity나 Fragment를 다시 그리기에는 불필요한 비용이 듭니다. 그렇다면 데이터의 변경에 따라 즉각적으로 View의 일부분만을 갱신해줄 수 있는 방법이 무엇이 있을까? 그래서 AAC에서 LiveData를 사용해서 해당 문제를 해결해줄 수 있습니다. (물론 현재는 StateFlow를 많이 사용하는 것 같습니다.)

 

LiveData 정의

LiveData의 정의는 공식 문서에서 다음과 같습니다.

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

이를 간단하게 요약하면 다음과 같습니다.

LiveData는 관찰 가능한 Data Holder 클래스입니다. 일반적인 Observable과는 다르게 LiveData는 LifeCycle을 알고 있습니다. 또한 Observer 객체를 함께 사용합니다.

 

이러한 속성 때문에 2가지 특징이 있습니다.

  • Activity, Fragment와 같은 컴포넌트의 생명주기를 인식하여 active한 상태일 때만 데이터를 업데이트합니다.
  • LiveData는 등록된 Observer 객체에 변화를 알려주고, Observer의 onChanged() 메서드가 실행됩니다.

 

LiveData 동작 방식

일단 LiveData는 LifeCycleOwner라는 친구를 통해 생명주기를 알고 있습니다. LifeCycleOwner는 단일 메소드 인터페이스입니다. Activity나 Fragment에서 이를 상속하고 있습니다.

public interface LifecycleOwner {
    /**
     * Returns the Lifecycle of the provider.
     *
     * @return The lifecycle of the provider.
     */
    @NonNull
    Lifecycle getLifecycle();
}

LiveData는 observe()메소드를 사용해 Observer 객체와 결합합니다. 이때 observe()는 LifeCycleOwner를 필요로 하고 보통 Activity나 Fragment를 전달합니다. 그러면 해당 화면 별 생명주기에 따라 LiveData는 동작을 하게 됩니다.

 

아래는 그 예시입니다.

//FoodViewModel의 getAll()함수, LiveData를 반환한다.
fun getAll(): LiveData<List<FoodEntity>> {
    return foodAllData
}


//Fragment에서 사용
foodViewModel.getAll().observe(viewLifecycleOwner) { foods ->
    adapter.submitList(foods)
}

일반적으로 Android의 Clean Architecture에서 Presentation Layer와 Data Layer에서 데이터를 주고받는다고 하면 다음 2가지 Data의 흐름이 있습니다. (여기서는 Domain Layer는 제외)

  1. View가 Data Srouce에게 data를 달라고 명시적으로 요청
  2. Data Source는 Data를 생산하고 View는 Data를 기다림

 

이 때 LiveData를 사용하면 Data Source의 Data 변화를 Observer를 통해 관찰하고 감지하여 View에게 전달해주는 역할을 합니다.

 

LiveData의 장점

위와 같은 특징으로 인해 LiveData는 다음과 같은 장점을 같습니다.

 

1. 메모리 누수 없는 사용을 보장

LiveData는 lifeCycle은 Observer의 lifeCycle을 따라가고 Observer는 해당하는 컴포넌트의 생명주기와 결합되기 때문에 해당 컴포넌트가 Destory 될 경우 메모리상에서 스스로 해제합니다.

 

2. 항상 최신 데이터를 유지한다.

LiveData는 Observer 패턴을 사용하여 자동으로 데이터가 업데이트 되기 때문에 추가적인 코드 없이 UI는 항상 최신화된 데이터 상태를 가질 수 있습니다.

 

3. Stop 상태의 Activity와 Crash가 발생하지 않는다.

Observer의 생명 주기가 inactive일 경우, Observer는 LiveData의 어떤 이벤트도 수신하지 않습니다. (Observer의 생명주기는 해당 View의 생명주기와 결합)

 

LiveData 단점

1. 비동기 데이터 스트림을 처리하기에 적합하지 않다.

LiveData의 관찰은 오직 Main Thread에서만 진행되기 때문에 Android 클린 아키텍처의 Data Layer에서 LiveData를 사용해서 데이터 스트림을 처리하려고 하면 한계가 생깁니다.

 

2. Domain Layer에 사용하기 적합하지 않다.

일반적으로 Android 클린 아키텍처의 Domain Layer는 의존성을 가지지 않는 순수한 Kotlin/Java로 되어있는 레이어입니다. 하지만 LiveData는 AAC 라이브러리이기 때문에 해당하는 모듈을 추가해주어야 하는 문제가 있습니다.

 

3. 오직 한번만 변경된 데이터를 관찰해야 할 경우 문제가 생길 수 있습니다.

기본적으로 LiveData는 변경된 데이터가 뷰의 재생성에 따라 여러 번 사용할 수 있기 때문에 한 번만 떠야 하는 경우 문제가 발생할 수 있습니다.(Dialog, Toast, SnackBar) 이를 해결하기 위해 SingleLiveData를 사용합니다.

 

LiveData 사용법

앞서 LiveData가 무엇인지? 장단점은 무엇인지? 알아보았으니 본격적으로 어떻게 사용하는 것인지 알아보려고 합니다. 일반적으로 ViewModel과 같이 사용합니다.

class SimpleViewModel : ViewModel() {

    private val _data = MutableLiveData<String>()
    val data: LiveData<String> get() = _data
    
    private var mCountDown : CountDownTimer
    private val arr = arrayOf("one", "two", "three")
    private var i =0

    init {
        mCountDown = object : CountDownTimer(299000, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                if(i == arr.size) i = 0
                _data.value = arr[i++]

            }
            override fun onFinish() {
            }
        }
        mCountDown.start()
    }

}

위는 1초마다 text의 String을 변경해주기 위한 ViewModel입니다.

 

MutableLiveData는 값의 get/set 모두를 할 수 있고 LiveData는 값의 get()만을 할 수 있습니다. _data와 data를 나눈 이유는 ViewModel과 View의 역할을 분리하기 위함입니다. ViewModel은 _data를 통해 값을 변경할 수 있고 View는 데이터의 읽기만을 허용합니다.

 

LiveData가 값을 전달하는 방법은 postValuesetValue 두가지가 있는데 조금 차이가 있습니다.

  • setValue : MainThread(UI)가 보장될 경우에는 set을 활용
  • postValue : MainThread가 아닌 IO 스케쥴러를 활용하는 경우 postValue를 활용

 

setValue 내부 코드

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

Main Thread에서 Set동작을 즉각적으로 수행합니다. 그래서 값 변화와 동시에 전달이 됩니다. 만약 백그라운드에서 setValue()를 호출하면 오류가 납니다.

 

postValue 내부 코드

protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
    postTask = mPendingData == NOT_SET;
    mPendingData = value;
    }
    if (!postTask) {
    return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

postValue는 백그라운드에서 값을 변경합니다. 백그라운드 쓰레드에서 동작하다가 메인 쓰레드에 값을 Post 하는 방식으로 사용됩니다. 그리고 여러 번 수행해도 맨 마지막 동작만 실질적으로 반영이 됩니다.

 

이제 해당하는 Activity나 Fragment에서 다음과 같이 사용할 수 있습니다.

class MainActivity : AppCompatActivity() {

    private val simpleViewModel : SimpleViewModel by viewModels()
    private lateinit var binding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        simpleViewModel.data.observe(this){
                binding.textField.text = it
        }

    }
}

 

Room과 같이 사용할 때는 다음과 같이 사용할 수 있습니다. (축약 버전) - DB에 있는 모든 음식 정보들 가져오기

@Dao
interface FoodDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(item : FoodEntity)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun initDB(items : List<FoodEntity>)

    @Delete
    suspend fun delete(item : FoodEntity)

    @Query("SELECT * FROM FOOD")
    fun getAllFoods() : LiveData<List<FoodEntity>>

}

Repository

class FoodRepository @Inject constructor(
    private val foodDao: FoodDao,
    private val imageService: ImageService
){
     fun getAll(): LiveData<List<FoodEntity>> {
        return foodDao.getAllFoods()
    }
}

ViewModel

@HiltViewModel
class FoodViewModel @Inject constructor(
    private val repository: FoodRepository
) : ViewModel()
{
    private val foodAllData : LiveData<List<FoodEntity>> = repository.getAll()

    fun getAll(): LiveData<List<FoodEntity>> {
        return foodAllData
    }
}

Fragment

class MenuManagementFragment : BaseFragment<FragmentMenuManagementBinding>(FragmentMenuManagementBinding::inflate) ,OnItemClick {

    private val foodViewModel by activityViewModels<FoodViewModel>()
    private val adapter = FoodAdapter(this)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        super.onViewCreated(view, savedInstanceState)
        initView()

        foodViewModel.getAll().observe(viewLifecycleOwner) { foods ->
            adapter.submitList(foods)
        }
    }
}

 

추가적으로 공부하면 좋을 내용들

  • DataBinding
  • LifeCycle
  • Flow
  • StateFlow
반응형

댓글


loading