본문 바로가기

나의 플랫폼/안드로이드

[번역] Android Architecture Components 사용시 5가지 일반적인 실수

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

출처 : https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb

 

5 common mistakes when using Architecture Components

Subtle oversights with more or less serious consequences - even if you’re not making these mistakes it should be worth keeping them in…

proandroiddev.com

다소 심각한 결과를 초래하는 미묘한 실수 - 심지어 이러한 실수를 하지 않더라도 향후 어떤 문제가 발생 향후 발생할 가능성이 있기 때문에 기억할만한 가치가 있다. 글 목록:

 

5 common mistakes when using Architecture Components

Subtle oversights with more or less serious consequences - even if you’re not making these mistakes it should be worth keeping them in…

proandroiddev.com

1. Leaking LiveData observers in Fragments

Fragment들은 lifecycle이 까다롭고 Fragment가 분리되었다가 다시 연결되었을 때 항상  실제로 Destory되는 것은 아니다. 예를 들면,  보유하고 있는 fragment들은 구성이 변경되는 동안 Destroy되지 않는다. 이런 일이 일어났을때, 그 fragment들의 인스턴스는 남아 있고 단지 view만 Destroy된다. 그래서 onDestroy()는 호출이되지 않고 DESTROYED state에 도달하지 않는다.

 

이것이 의미하는 것은 만약 우리가 onCreaterView나 그 이후 (일반적으로 onActivityCreated()) 에서 observing을 시작하고 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)

        viewModel.liveData.observe(this, Observer { updateViews(it) })  // Risky: Passing Fragment as LifecycleOwner
    }
    
    ...
}

우리는 매번 Fragment에 재연결(다시 화면에 띄울 때) 할때마다 Observer의 새로운 동일 인스턴스를 접하게 될 것이다. 하지만 LiveData는 이전 observer들을 삭제하지 않는다. 그 이유는 LifecycleOwner (Fragment)가 DESTROYED 상태로 도달하지 않았기 때문이다. 이것은 결과적으로 같은 시간에 동일한 observers의 수가 자라날 것이고 OnChanged 함수가 여러번 실행 될 것이다.


이 이슈는 here 에 보고되어 있고, 더 확장된 설명은  here에서 찾을 수 있다.

 

추천하는 해결책은 fragment’s view lifecycle 인 getViewLifecycleOwner() 나 getViewLifecycleOwnerLiveData() 를 사용하는 것이다. Support Library 28.0.0 와 AndroidX 1.0.0에 포함되어 있다. 그러면 LiveData는 observer들을 매번 every time the fragment’s view가 destroy될 때 삭제 할 것이다:

class BooksFragment : Fragment() {

    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this).get(BooksViewModel::class.java)

        viewModel.liveData.observe(viewLifecycleOwner, Observer { updateViews(it) })    // Usually what we want: Passing Fragment's view as LifecycleOwner
    }
    
    ...
}

2. Reloading data after every rotation

우리는 초기화 작업과 setup 로직을 Activities (Fragments 에서 onCreateView() 나 그 다음 유추 되어지는) 에 위치시키곤 한다. 그래서 이 시점에서 ViewModels의 일부 데이터 로드 하는 것을 trigger하려는 유혹에 빠질 수도 있다. 너의 로직에 따르더라도 의도하지 않게 대부분의 경우 매 회전 후 (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>> {
        repository.getProductDetails()  // Loading ProductDetails from network/database
        ...                             // Getting ProductDetails from repository and updating productDetails LiveData
        return productDetails
    }

    fun loadSpecialOffers() {
        repository.getSpecialOffers()   // Loading SpecialOffers from network/database
        ...                             // 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)

        viewModel.getProductsDetails().observe(this, Observer { /*...*/ })  // (probable) Reloading product details after every rotation
        viewModel.loadSpecialOffers()                                       // (probable) Reloading special offers after every rotation
    }
}

또한 해결책은 너의 로직에 달려 있다. 만약 예를 들어, Repository가 cache data 라면 위 코드는 아마도 괜찮을 것이다.

다른 해결책은:

  • AbsentLiveData 와 유사한 어떠한 것을 사용하고 데이터를 set하지 않는 경우에만 로드를 시작해라.
  • 실제로 필요할 때 데이터 로드를 시작 eg. in OnClickListener
  • 그리고 아마도 가장 간단한: ViewModel 생성자에서 로드 호출을 넣고 순수한 getters를 노출
class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    private val productDetails = MutableLiveData<Resource<ProductDetails>>()
    private val specialOffers = MutableLiveData<Resource<SpecialOffers>>()

    init {
        loadProductsDetails()           // ViewModel is created only once during Activity/Fragment lifetime
    }

    private fun loadProductsDetails() { // private, just utility method to be invoked in constructor
        repository.getProductDetails()  // Loading ProductDetails from network/database
        ...                             // Getting ProductDetails from repository and updating productDetails LiveData
    }

    fun loadSpecialOffers() {           // public, intended to be invoked by other classes when needed
        repository.getSpecialOffers()   // Loading SpecialOffers from network/database
        ...                             // Getting SpecialOffers from repository and updating _specialOffers LiveData
    }

    fun getProductDetails(): LiveData<Resource<ProductDetails>> {   // Simple getter
        return productDetails
    }

    fun getSpecialOffers(): LiveData<Resource<SpecialOffers>> {     // Simple getter
        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)

        viewModel.getProductDetails().observe(this, Observer { /*...*/ })    // Just setting observer
        viewModel.getSpecialOffers().observe(this, Observer { /*...*/ })     // Just setting observer

        button_offers.setOnClickListener { viewModel.loadSpecialOffers() }
    }
}

3. Leaking ViewModels

ViewModel에 View references를 전달해서는 안된다는 것은 이미 이미 강조 가 되었다.

하지만 또한 우리는 다른 클래스에 ViewModels에 대한 참조를 전달하는 것을 주의해야 한다. Activity ( 또는 by analogy Fragment) 가 끝난 후, ViewModel은 Activity 보다 오래 살아 남는 어떠한 object도 참조해서는 안된다. 그러면 그 ViewModel은 garbage collect 될 수 있다.

 

한 leak 관련 예로 Singleton 범위로 할당된 Repository의 listener를 ViewModel에 전달 되어 질 수 있다. 그리고 나중에 listener를 Clear 하지 않은 경우:

@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 {
        repository.setOnLocationChangedListener {   // Risky: Passing listener (which holds reference to the MapViewModel)
            liveData.value = it                     // to singleton scoped LocationRepository
        }
    }
}

해결책은 Repository에서 WeakReference로 저장한 listener를 onCleared() 함수에서 삭제할 수 있다. Repository와 ViewModel은 LiveData를 통해 사용 한다. - 또는 기본적으로 적합하고 올바른 garbage collection을 보장하는 모든 것.

@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 {
        repository.setOnLocationChangedListener {   // Risky: Passing listener (which holds reference to the MapViewModel)
            liveData.value = it                     // to singleton scoped LocationRepository
        }
    }
  
    override onCleared() {                            // GOOD: Listener instance from above and MapViewModel
        repository.removeOnLocationChangedListener()  //       can now be garbage collected
    }  
}

4. Exposing LiveData as mutable to views

이건 버그는 아니다. 하지만 우려의 한 부분이다.
Views - Fragments 와 Activities - ViewModels의 책임으로 인해 LiveData와 그 자신의 상태를 수정해서는 안된다.  Views는 LiveData를 observe만 해야 한다.

따라서 MutableLiveData에 대한 접근을 캡슐화 해야 한다. 예를 들어 getters 또는 backing properties :

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(){
        products.value = loadProducts()     // Other classes can also set products value
        promotions.value = loadPromotions() // Only CatalogueViewModel can set promotions value
        _offers.value = loadOffers()        // Only CatalogueViewModel can set offers value
    }
}

5. Creating ViewModel’s dependencies after every configuration change

ViewModel은 회전과 같은 변경사항이 유지 된다. 따라서 변경이 발생할 때마다 dependencies 작성하는 것이 단순히 중복되며 때로는 의도하지 않는 동작으로 이어질 수 있다. 특히 dependencies 생성자에 로직이 있는 경우.

이것이 명백하게 들릴 수도 있지만, ViewModelFactory를 사용할 때 간과하기 쉽다. ViewModelFactory는 일반적으로 생성되는 ViewModel과 동일한 종속성을 갖는다.
ViewModelProvider는 ViewModel 인스턴스를 유지하지만 ViewModelFactory 인스턴스는 유지하지 않는다. 다음과 같이 코드가 있는 경우 :

class MoviesViewModel(
    private val repository: MoviesRepository,
    private val stringProvider: StringProvider,
    private val authorisationService: AuthorisationService
) : ViewModel() {
    
    ...
}


class MoviesViewModelFactory(   // We need to create instances of below dependencies to create instance of MoviesViewModelFactory
    private val repository: MoviesRepository,
    private val stringProvider: StringProvider,
    private val authorisationService: AuthorisationService
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {  // but this method is called by ViewModelProvider only if ViewModel wasn't already created
        return MoviesViewModel(repository, stringProvider, authorisationService) as T
    }
}


class MoviesActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModelFactory: MoviesViewModelFactory

    private lateinit var viewModel: MoviesViewModel

    override fun onCreate(savedInstanceState: Bundle?) {    // Called each time Activity is recreated
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movies)

        injectDependencies() // Creating new instance of MoviesViewModelFactory

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(MoviesViewModel::class.java)
    }
    
    ...
}

구성 변경이 발생할 때마다 ViewModelFactory의 새 인스턴스를 생성하므로 모든 dependencies 의 새 인스턴스를 불필요하게 생성한다. (어떻게 범위가 지정되지 않은 경우).

해결책은 ActivityFragment 수명 동안 한 번만 호출되므로 create () 함수가 실제로 호출 될 때까지 dependencies 작성을 연기하는 것이다. 예를 들어 lazy 초기화를 사용하여 이를 달성 할 수 있습니다. eg. Providers:

 

class MoviesViewModel(
    private val repository: MoviesRepository,
    private val stringProvider: StringProvider,
    private val authorisationService: AuthorisationService
) : ViewModel() {
    
    ...
}


class MoviesViewModelFactory(
    private val repository: Provider<MoviesRepository>,             // Passing Providers here 
    private val stringProvider: Provider<StringProvider>,           // instead of passing directly dependencies
    private val authorisationService: Provider<AuthorisationService>
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {  // This method is called by ViewModelProvider only if ViewModel wasn't already created
        return MoviesViewModel(repository.get(),                    
                               stringProvider.get(),                // Deferred creating dependencies only if new insance of ViewModel is needed
                               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)
      
        injectDependencies() // Creating new instance of MoviesViewModelFactory

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(MoviesViewModel::class.java)
    }
    
    ...
}

Additional resources