【譯】使用 Android Architecture Components 的五個常見誤區

前言

本文翻譯自【5 common mistakes when using Architecture Components】,介紹了在使用 Android Architecture Components 的五個常見誤區。水平有限,歡迎指正討論。 輕微的疏忽或多或少會帶來嚴重的後果 —— 即便你沒有犯這些錯誤,也應該瞭解並記住它們,以免未來遇到一些問題。本文將介紹如下五個誤區:html

正文

泄露 Fragment 中的 LiveData 觀察者

原文:Leaking LiveData observers in Fragmentsjava

Fragment 具備複雜的生命週期,當一個 Fragment 與其宿主 Activity 取消關聯(執行 Fragment#onDetach()),而後從新關聯(執行 Fragment#onAttach())時,實際上這個 Fragment 並不老是會被銷燬(執行 Fragment#onDestroy())。例如在配置變化時,被保留(Fragment#setRetainInstance(true))的 Fragment 不會被銷燬。這時,只有 Fragment 的視圖會被銷燬(Fragment#onDestroyView()),而 Fragment 實例沒有被銷燬,所以不會調用 Fragment#onDestroy() 方法,也就是說 Fragment 做爲 LifecycleOwner 沒有到達已銷燬狀態 (Lifecycle.State#DESTROYED)。 這意味着,若是咱們在 Fragment#onCreateView() 及之後的方法(一般是 Fragment#onActivityCreated())中觀察 LiveData,並將 Fragment 做爲 LifecycleOwner 傳入就會出現問題。 例如:android

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
    }

    //...
}
複製代碼

每次當 Activity 從新關聯 Fragment 時,咱們都會傳遞一個新的相同的觀察者實例,可是 LiveData 不會刪除之前的觀察者,由於 LifecycleOwner(即 Fragment)沒有達到已銷燬狀態。這最終會致使愈來愈多的相同觀察者同時處於活動狀態,從而致使 Observer#onChanged() 方法也會被重複執行屢次。git

這個問題最初是在這裏提出的,在這裏能夠找到更多細節。github

推薦的解決方案是:經過 Fragment#getViewLifecycleOwner()Fragment#getViewLifecycleOwnerLiveData() 方法獲取 Fragment 的視圖(View)生命週期,而不是 Fragment 實例的生命週期,這兩個方法是在 Support-28.0.0AndroidX-1.0.0 中添加的,這樣,LiveData 就會在每次 Fragment 的視圖銷燬時移除觀察者。數據庫

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
    }

    //...
}
複製代碼

每次屏幕旋轉後都從新加載數據

原文:Reloading data after every rotation後端

一般,咱們在 Activity#onCreate(),或 Fragment#onCreateView() 及之後的生命週期方法中初始化代碼邏輯,用來觸發 ViewModel 獲取數據。若是代碼不規範,在每次屏幕旋轉後,即便 ViewModel 實例不會從新建立,也可能致使從新加載數據,而在大多數狀況下,數據並無變化,因此這種從新加載沒有意義。 例如:api

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 緩存了數據,上面的例子就沒有問題,由於 Repository 的緩存有效就不會請求網絡或讀寫數據庫。但也有一些其餘解決辦法:緩存

  • 使用相似於 AbsentLiveData 的類,只有在沒有執行過 LiveData#setValue()LiveData#postValue()的狀況下,纔會加載數據。
  • 在實際須要的地方纔開始加載數據,例如點擊事件(OnClickListener)
  • 可能最簡單的方案是,將加載數據的邏輯,寫在 ViewModel 的構造方法中,並暴露 getter 方法,例如:
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() }
    }
}
複製代碼

泄露 ViewModel

原文:Leaking ViewModels網絡

Google 官方已經明確提示ViewModel 不該持有 View、Lifecycle、或其餘可能持有 Activity 的 Context 的類的引用

Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

可是,咱們也應注意 其餘類不該持有 ViewModel 的引用。在 Activity 或 Fragment 銷燬後,其它任何比 Activity 或 Fragment 生命週期長的類都不該再持有 ViewModel 的引用,不然會影響 ViewModel 被 GC 回收,從而泄露 ViewModel。 以下面的例子,Repository 是一個單例,它持有了 ViewModel 的監聽器引用,但並無釋放:

@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
        }
    }
}
複製代碼

有以下幾種方案能夠避免泄露 ViewModel:

  • ViewModel#onCleared() 方法中移除監聽器。
  • Repository 持有 Listener 的 弱引用(WeakReference)。
  • Repository 和 ViewModel 使用 LiveData 來通訊。
  • 其餘保證 ViewModel 能被 GC 正確回收的方案。
@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
    }  
}
複製代碼

將易變的 LiveData 暴露給 View

原文:Exposing LiveData as mutable to Views

這個誤區不是 Bug,可是它違背了關注點分離原則。如下引自 維基百科

關注點分離(Separation of concerns,SOC)是對只與「特定概念、目標」(關注點)相關聯的軟件組成部分進行「標識、封裝和操縱」的能力,即標識、封裝和操縱關注點的能力。是處理複雜性的一個原則。因爲關注點混雜在一塊兒會致使複雜性大大增長,因此可以把不一樣的關注點分離開來,分別處理就是處理複雜性的一個原則,一種方法。關注點分離在計算機科學中,是將計算機程序分隔爲不一樣部分的設計原則,是面向對象的程序設計的核心概念。每一部分會有各自的關注焦點。關注焦點是影響計算機程式代碼的一組資訊。關注焦點能夠像是將代碼優化過的硬件細節通常,或者像實例化類別的名稱同樣具體。展示關注點分離設計的程序被稱爲模組化程序。模組化程度,也就是區分關注焦點,經過將資訊封裝在具備明確界面的程序代碼段落中。封裝是一種資訊隱藏手段。資訊系統中的分層設計是關注點分離的另外一個實施例(例如,表示層,業務邏輯層,數據訪問層,維持齊一層)。分離關注點使得解決特定領域問題的代碼從業務邏輯中獨立出來,業務邏輯的代碼中再也不含有針對特定領域問題代碼的調用(將針對特定領域問題代碼抽象化成較少的程式碼,例如將代碼封裝成function或是class),業務邏輯同特定領域問題的關係經過側面來封裝、維護,這樣本來分散在整個應用程序中的變更就能夠很好的管理起來。關注點分離的價值在於簡化計算機程序的開發和維護。當關注點分開時,各部分能夠重複使用,以及獨立開發和更新。具備特殊價值的是可以稍後改進或修改一段代碼,而無需知道其餘部分的細節必須對這些部分進行相應的更改。

View,即 Activity 和 Fragment 不該該主動更新 LiveData 數據來刷新 UI 狀態,由於這是 ViewModel 的職責。View 只應該是 LiveData 的觀察者。 所以咱們應該封裝 MutableLiveData 的使用,例如暴露 getter 方法或使用 Kotlin 的 後端屬性

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
    }
}
複製代碼

每次配置變化後從新建立 ViewModel 依賴項的實例

原文:Creating ViewModel’s dependencies after every configuration change

當屏幕旋轉引發配置變化時,ViewModel 不會從新建立(詳見Lifecycle),所以每次配置變化後建立 ViewModel 的依賴項是無心義的,有時可能致使意想不到的後果,尤爲當依賴的構造方法中存在業務邏輯時。 雖然這聽起來很明顯,但在使用 ViewModelFactory 時很容易忽略這一點,由於 ViewModeFactory 一般與它建立的 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 實例,從而沒必要要地會建立全部依賴項的新實例(假如這些依賴項沒有肯定的做用域)。咱們可使用 Provider 的懶加載來避免這個問題:

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)
    }

    //...
}
複製代碼

參考

聯繫

我是 xiaobailong24,您能夠經過如下平臺找到我:

相關文章
相關標籤/搜索