티스토리 뷰

728x90
반응형

Architecture Components 사용 시의 5가지 일반적인 실수

주의 : 이 글은 원문이 존재하는 글로, 본인은 그저 번역 / 의역한 것일뿐임을 알림.

원문 : 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)
  }
   
  ...
}


반응형
공지사항
최근에 올라온 글