继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

【译】使用 Android Architecture Components 的五个常见误区

慕神8447489
关注TA
已关注
手记 1320
粉丝 174
获赞 957

正文

泄露 Fragment 中的 LiveData 观察者

原文:Leaking LiveData observers in Fragments

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 传入就会出现问题。
例如:

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() 方法也会被重复执行多次。

这个问题最初是在这里提出的,在这里可以找到更多细节。

推荐的解决方案是:通过 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 实例不会重新创建,也可能导致重新加载数据,而在大多数情况下,数据并没有变化,所以这种重新加载没有意义。
例如:

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
链接:https://www.jianshu.com/p/c1ee77f8237f


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP