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

前言

本文翻译自【5 common mistakes when using Architecture Components】,介绍了在使用 Android Architecture Components 的五个常见误区。水平有限,欢迎指正讨论。
轻微的疏忽或多或少会带来严重的后果 —— 即使你没有犯这些错误,也应该了解并记住它们,以避免将来遇到一些问题。本文将介绍以下五个误区:

正文

泄露 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,您可以通过以下平台找到我:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容