티스토리 뷰
주의 : 이 글은 원문이 존재하는 글로, 본인은 그저 번역 / 의역한 것일뿐임을 알림.
원문 : https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb
Leaking LiveData observers in Fragments
Reloading data after every rotation
Leaking ViewModels
Exposing LiveData as mutable to Views
Creating ViewModel's dependencies after every configuration change
1. Leaking LiveData observers in Fragments
fragment 는 까다로운 lifecycle 을 가지고 있고 fragment 가 detach 또는 re-attach 될 때 항상 실제로 파괴되지 않는다. 예로, configuration change 가 발생하는 동안 Fragment 는 파괴되지 않는다.configuration change 동안 fragment 의 instance 는 살아남고 오직 view 만 파괴된다. 따라서 onDestroy()
는 호출되지 않고 DESTROYED
상태에 도달하지 않는다.
이 문제는 우리가 LiveData 를 아래와 같이 onCreateView()
에서 옵저빙을 시작하거나, ( 때떄로 onActivityCreated()
에서도 ) LifecycleOwner 로 fragment 를 전달할 때 발생한다.
class BooksFragment: Fragment() {
private lateinit var viewModel: BooksViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_books, container)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(BooksViewModel::class.java)
// Risky: Passing Fragment as LifecycleOwner
viewModel.liveData.observe(this, Observer { updateViews(it) })
}
...
}
fragment 의 LifecycleOwner 가 DESTROYED
상태에 도달하지 않았기 때문에 기존의 observer instance 를 제거하기도 전에 fragment 가 re-attach 될 때마다 observer 의 새로운 instance 를 전달할 것이다.
결국 onChange()
에서 같은 시점, 같은 코드에서 활성화되어 여러번 실행되는 옵저버의 수가 늘어나는 결과를 초래한다.
해당 이슈는 본래 여기에 보고되었으며, 더 자세한 설명은 여기에서 찾을 수 있다.
권장 솔루션은 support library 28.0.0, AndroidX 1.0.0 에 추가된 getViewLifecycleOwner()
또는 getViewLifecycleOwnerLiveData()
를 통해 fragment 의 view lifecycle 을 사용하는 것이다.
그렇게 함으로 LiveData 는 fragment 의 view 가 파괴될 때마다 observer 를 제거할 것이다.
2. Reloading data after every rotation
우리는 Activity 의 onCreate()
혹은 Fragment 의 onCreateView()
메소드에 초기화 로직을 배치한다.
그렇기에 그 시점에 ViewModel 의 데이터를 불러와 연결하는것이 매력적으로 느껴질 수 있다.
하지만 당신의 코드에 따라, ViewModel 이 사용되고 있음에도 화면이 회전될 때마다 데이터가 다시 로드 될 가능성이 있다.
그것은 대부분이 무의미하고 의도하지 않은 작업일 것이다.
예제 :
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val productDetails = MutableLiveData<Resource<ProductDetails>>()
private val specialOffers = MutableLiveData<Resource<SpecialOffers>>()
fun getProductsDetails(): LiveData<Resource<ProductDetails>> {
// Loading ProductDetails from network/database
repository.getProductDetails()
// Getting ProductDetails from repository and updating productDetails LiveData
...
return productDetails
}
fun loadSpecialOffers() {
// Loading SpecialOffers from network/database
repository.getSpecialOffers()
// Getting SpecialOffers from repository and updating specialOffers LiveData
...
}
}
class ProductActivity : AppCompatActivity() {
lateinit var productViewModelFactory: ProductViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProviders.of(this, productViewModelFactory).get(ProductViewModel::class.java)
// (probable) Reloading product details after every rotation
viewModel.getProductsDetails().observe(this, Observer { /*...*/ })
// (probable) Reloading special offers after every rotation
viewModel.loadSpecialOffers()
}
}
이 해결방안 역시 당신의 코드에 따라 다르다. 예제로 보면, repository 가 데이터를 캐싱하는 경우에 위 코드는 정상일 수 있다.
다른 해결방안은 아래와 같다.
LiveData 와 비슷한 AbsentLiveData 를 사용, 데이터가 세팅되지 않았을 경우에만 로딩을 시작
OnClickListener 에서 실제로 데이터가 필요한 경우에만 로딩을 시작
제일 간단한 방법으로, ViewModel 의 초기화 시점에 로딩을 하고 getter 로 LiveData 를 제공
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val productDetails = MutableLiveData<Resource<ProductDetails>>()
private val specialOffers = MutableLiveData<Resource<SpecialOffers>>()
init {
// ViewModel is created only once during Activity/Fragment lifetime
loadProductsDetails()
}
// private, just utility method to be invoked in constructor
private fun loadProductsDetails() {
// Loading ProductDetails from network/database
repository.getProductDetails()
// Getting ProductDetails from repository and updating productDetails LiveData
...
}
// public, intended to be invoked by other classes when needed
fun loadSpecialOffers() {
// Loading SpecialOffers from network/database
repository.getSpecialOffers()
// Getting SpecialOffers from repository and updating _specialOffers LiveData
...
}
// Simple getter
fun getProductDetails(): LiveData<Resource<ProductDetails>> {
return productDetails
}
// Simple getter
fun getSpecialOffers(): LiveData<Resource<SpecialOffers>> {
return specialOffers
}
}
class ProductActivity : AppCompatActivity() {
lateinit var productViewModelFactory: ProductViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProviders.of(this, productViewModelFactory).get(ProductViewModel::class.java)
// Just setting observer
viewModel.getProductDetails().observe(this, Observer { /*...*/ })
// Just setting observer
viewModel.getSpecialOffers().observe(this, Observer { /*...*/ })
button_offers.setOnClickListener { viewModel.loadSpecialOffers() }
}
}
3. Leaking ViewModels
ViewModel 에 view 참조를 전달하지 말라는것은 이미 강조되어 왔다.
주의 : ViewModel 은 activity context 를 가질 수 있는 view, lifecycle 또는 class 를 절대 참조해서는 안 된다.
하지만 우리는 다른 class 들에 ViewModel 의 참조를 전달하는 것 또한 조심해야한다.
activity 혹은 fragment 가 종료될 때, activity 보다 오래 살아남을 수 있는 어떤 객체에서도 ViewModel 이 참조되어서는 안되며, ViewModel 은 올바르게 가비지 콜렉팅 되어야 한다.
아래 예제는 싱글톤 스코프 내에서 repository 에 ViewModel listener 를 전달한고 삭제하지 않는다.
@Singleton
class LocationRepository() {
private var listener: ((Location) -> Unit)? = null
fun setOnLocationChangedListener(listener: (Location) -> Unit) {
this.listener = listener
}
private fun onLocationUpdated(location: Location) {
listener?.invoke(location)
}
}
class MapViewModel: AutoClearViewModel() {
private val liveData = MutableLiveData<LocationRepository.Location>()
private val repository = LocationRepository()
init {
// Risky: Passing listener (which holds reference to the MapViewModel)
repository.setOnLocationChangedListener {
// to singleton scoped LocationRepository
liveData.value = it
}
}
}
해결방안으로 onCleared()
메소드를 이용하여 listener 를 제거하고 이를 repository 에 WeakReference
로 저장하며 LiveData 를 사용함으로 repository 와 ViewModel 간의 통신을 하는 것이다.
이를 통해 정학환 가비지 콜렉팅을 보장할 수 있다.
@Singleton
class LocationRepository() {
private var listener: ((Location) -> Unit)? = null
fun setOnLocationChangedListener(listener: (Location) -> Unit) {
this.listener = listener
}
fun removeOnLocationChangedListener() {
this.listener = null
}
private fun onLocationUpdated(location: Location) {
listener?.invoke(location)
}
}
class MapViewModel: AutoClearViewModel() {
private val liveData = MutableLiveData<LocationRepository.Location>()
private val repository = LocationRepository()
init {
// Risky: Passing listener (which holds reference to the MapViewModel)
repository.setOnLocationChangedListener {
// to singleton scoped LocationRepository
liveData.value = it
}
}
// GOOD: Listener instance from above and MapViewModel
override onCleared() {
// can now be garbage collected
repository.removeOnLocationChangedListener()
}
}
4. Exposing LiveData as mutable to views
이건 버그는 아니지만 관심사 분리에 어긋난다.
fragment, activity 의 view 는 LiveData 와 그에 따른 자신의 상태를 업데이트 할 수 없어야 한다.
그것은 오직 ViewModel 의 책임이기 때문이다. view 들은 LiveData 를 단지 observe 할 수 있어야 한다.
그러므로 우리는 MutableLiveData 를 getter 혹은 베이킹 필드로 캡슐화하여 접근하여야 한다.
class CatalogueViewModel : ViewModel() {
// BAD: Exposing mutable LiveData
val products = MutableLiveData<Products>()
// GOOD: Encapsulate access to mutable LiveData through getter
private val promotions = MutableLiveData<Promotions>()
fun getPromotions(): LiveData<Promotions> = promotions
// GOOD: Encapsulate access to mutable LiveData using backing property
private val _offers = MutableLiveData<Offers>()
val offers: LiveData<Offers> = _offers
fun loadData(){
// Other classes can also set products value
products.value = loadProducts()
// Only CatalogueViewModel can set promotions value
promotions.value = loadPromotions()
// Only CatalogueViewModel can set offers value
_offers.value = loadOffers()
}
}
5. Creating ViewModel's dependencies after every configuration change
ViewModel 은 화면 회전과 같은 configuration change 가 발생할 때 살아남기 때문에
변경이 발생할 때마다 종속성을 생성하는 것은 중복되며, 특히 종속성 생성자에 로직이 있는 경우 의도하지 않은 결과를 야기한다.
매우 명확히 들릴 수 있지만, ViewModelFactory 를 사용할 때 간과하기 쉬운 부분이다.
ViewModel 은 대개 create()
과 동일한 종속성을 가진다.
ViewModelProvider 는 ViewModelFactory 의 instance 가 아닌 ViewModel 의 instance 를 보존한다.
아래의 코드를 보자.
class MoviesViewModel(
private val repository: MoviesRepository,
private val stringProvider: StringProvider,
private val authorisationService: AuthorisationService
) : ViewModel() {
...
}
// We need to create instances of below dependencies to create instance of MoviesViewModelFactory
class MoviesViewModelFactory(
private val repository: MoviesRepository,
private val stringProvider: StringProvider,
private val authorisationService: AuthorisationService
) : ViewModelProvider.Factory {
// but this method is called by ViewModelProvider only if ViewModel wasn't already created
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MoviesViewModel(repository, stringProvider, authorisationService) as T
}
}
class MoviesActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: MoviesViewModelFactory
private lateinit var viewModel: MoviesViewModel
// Called each time Activity is recreated
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movies)
// Creating new instance of MoviesViewModelFactory
injectDependencies()
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MoviesViewModel::class.java)
}
...
}
매 번 configuration change 가 발생 시 우리는 새로운 ViewModelFactory instance 를 만들고 불필요한 새로운 종속성을 만들 것이다.
class MoviesViewModel(
private val repository: MoviesRepository,
private val stringProvider: StringProvider,
private val authorisationService: AuthorisationService
) : ViewModel() {
...
}
class MoviesViewModelFactory(
// Passing Providers here
private val repository: Provider<MoviesRepository>,
// instead of passing directly dependencies
private val stringProvider: Provider<StringProvider>,
private val authorisationService: Provider<AuthorisationService>
) : ViewModelProvider.Factory {
// This method is called by ViewModelProvider only if ViewModel wasn't already created
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MoviesViewModel(repository.get(),
// Deferred creating dependencies only if new insance of ViewModel is needed
stringProvider.get(),
authorisationService.get()
) as T
}
}
class MoviesActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: MoviesViewModelFactory
private lateinit var viewModel: MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movies)
// Creating new instance of MoviesViewModelFactory
injectDependencies()
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MoviesViewModel::class.java)
}
...
}
'프로그래밍 > Android' 카테고리의 다른 글
DataBinding, Binding 객체 생성 (1) | 2021.07.11 |
---|---|
[Android] getLocationOnScreen vs getLocationInWindow() (1) | 2020.01.19 |
[Android] Message.obtain() vs Handler.obtainMessage() (0) | 2019.10.03 |
[Android] Handler ( sendMessage, post ) / runOnUiThread (1) | 2019.10.03 |
[Android] 인터페이스 상수 (0) | 2019.08.25 |