May 6, 2020

5 распространенных ошибок при использовании архитектурных компонентов Android

Даже если вы не делаете этих ошибок, стоит о них помнить, чтобы не столкнуться с некоторыми проблемами в будущем.

1. Утечка наблюдателей LiveData во фрагментах

У фрагментов сложный жизненный цикл, и когда фрагмент отсоединяется и повторно присоединяется к Activity, то он не всегда уничтожается. Например, сохранённые фрагменты не уничтожаются во время изменений конфигурации. Когда это происходит, экземпляр фрагмента остаётся, а уничтожается только его View, поэтому onDestroy() не вызывается и состояние DESTROYED не достигается.

Это означает, что если мы начнём наблюдать LiveData в onCreateView() или позже (обычно в onActivityCreated()) и передадим фрагмент как LifecycleOwner:

class BooksFragment: Fragment() {

    private lateinit var viewModel: BooksViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.fragment_books, container)

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

        // передача фрагмента как LifecycleOwner
        viewModel.liveData.observe(this, Observer {
            updateViews(it)
        })
    }
    ...
}

то мы будем передавать новый идентичный экземпляр Observer каждый раз, когда фрагмент повторно присоединяется, но LiveData не удалит предыдущих наблюдателей, потому что LifecycleOwner (фрагмент) не достиг состояния DESTROYED. Это в конечном итоге приводит к тому, что растёт число идентичных и одновременно активных наблюдателей, и один и тот же код из onChanged() выполняется несколько раз.

О проблеме изначально сообщалось здесь, а более подробное объяснение можно найти здесь.

Рекомендуемое решение состоит в том, чтобы использовать getViewLifecycleOwner() или getViewLifecycleOwnerLiveData() из жизненного цикла фрагмента, которые были добавлены в библиотеку поддержки 28.0.0 и AndroidX 1.0.0, так что LiveData будет удалять наблюдателей при каждом уничтожении View фрагмента:

class BooksFragment : Fragment() {

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

        viewModel.liveData.observe(viewLifecycleOwner, Observer {
            updateViews(it)
        })
    }
    ...
}

2. Перезагрузка данных после каждого поворота экрана

Мы привыкли помещать логику инициализации и настройки в onCreate() в Activity (и по аналогии в 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>> {
        // Загрузка ProductDetails
        repository.getProductDetails()
        // Получение ProductDetails и обновление productDetails LiveData
        ...
        return productDetails
    }

    fun loadSpecialOffers() {
        // Загрузка SpecialOffers
        repository.getSpecialOffers()
        // Получение SpecialOffers и обновление specialOffers LiveData
        ...
    }
}
class ProductActivity : AppCompatActivity() {

    lateinit var productViewModelFactory: ProductViewModelFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel = ViewModelProvider(this, productViewModelFactory)
                .get(ProductViewModel::class.java)

        // (возможно) Повторный запрос productDetails после поворота экрана
        viewModel.getProductsDetails().observe(this, Observer {
            /*...*/
        })
        viewModel.loadSpecialOffers()                                       // (возможно) Повторный запрос specialOffers после поворота экрана
    }
}

Решение также зависит от вашей логики. Если репозиторий будет кешировать данные, то приведённый выше код, вероятно, будет работать корректно. Также эту проблему можно решить другими способами:

  • Использовать что-то похожее на AbsentLiveData и начинать загрузку, только если данные не были получены.
  • Начинать загрузку данных, когда это действительно необходимо. Например, в OnClickListener.
  • И, вероятно, самое простое: поместить вызовы загрузки в конструктор ViewModel и использовать простые геттеры.
class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

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

    init {
        // ViewModel создаётся только один раз
        // в ходе жизненных циклов Activity или фрагмента
        loadProductsDetails()
    }

    private fun loadProductsDetails() {
        repository.getProductDetails()  
        ...                             
    }

    fun loadSpecialOffers() {           
        repository.getSpecialOffers()   
        ...                            
    }

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

    fun getSpecialOffers(): LiveData<Resource<SpecialOffers>> {    
        return specialOffers
    }
}
class ProductActivity : AppCompatActivity() {

    lateinit var productViewModelFactory: ProductViewModelFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel = ViewModelProvider(this, productViewModelFactory)
                .get(ProductViewModel::class.java)

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

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

3. Утечка ViewModels

Разработчики архитектуры чётко дали понять, что мы не должны передавать ссылки View во ViewModel.

но мы также должны быть осторожны при передаче ссылок на ViewModel другим классам. После завершения Activity (или фрагмента), ViewModel нельзя ссылаться ни на один объект, который может пережить Activity, чтобы ViewModel могла быть уничтожена сборщиком мусора.

В этом примере утечки во ViewModel передаётся слушатель Repository, который написан в стиле Singleton. Впоследствии слушатель не уничтожается:

@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 {
            liveData.value = it
        }
    }
}

Решением здесь может быть удаление слушателя в методе onCleared(), сохранение его как WeakReference в репозитории или использование LiveData для связи между репозиторием и 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 {
        repository.setOnLocationChangedListener {   
            liveData.value = it                    
        }
    }

    override onCleared() {                           
        repository.removeOnLocationChangedListener() 
    }  
}

4. Позволять View изменять LiveData

Это не баг, но это противоречит разделению интересов. View — фрагменты и Activity — не должны иметь возможность обновлять LiveData и, следовательно, своё собственное состояние, потому что это ответственность ViewModel. View должны быть в состоянии только наблюдать LiveData.

Следовательно, мы должны инкапсулировать доступ к MutableLiveData:

class CatalogueViewModel : ViewModel() {

    // ПЛОХО: позволять изменять MutableLiveData
    val products = MutableLiveData<Products>()

    // ХОРОШО: инкапсулировать доступ к MutableLiveData
    private val promotions = MutableLiveData<Promotions>()

    fun getPromotions(): LiveData<Promotions> = promotions

    // ХОРОШО: инкапсулировать доступ к MutableLiveData
    private val _offers = MutableLiveData<Offers>()
    val offers: LiveData<Offers> = _offers

    fun loadData(){
        // Другие классы смогут изменять products
        products.value = loadProducts()
        // Только CatalogueViewModel может изменять promotions
        promotions.value = loadPromotions()
        // Только CatalogueViewModel может изменять _offers
        _offers.value = loadOffers()
    }
}

5. Создание зависимостей ViewModel после каждого изменения конфигурации

ViewModels выдерживают изменения конфигурации, такие как поворот экрана, поэтому если создавать их каждый раз, когда происходит изменение, то это может иногда приводить к непредсказуемому поведению, особенно если в конструкторе заложена некая логика.

Хотя это может показаться довольно очевидным, но это то, что легко упустить из виду при использовании ViewModelFactory, который обычно имеет те же зависимости, что и создаваемая им ViewModel.

ViewModelProvider сохраняет экземпляр ViewModel, но не экземпляр ViewModelFactory, поэтому, если у нас есть такой код:

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

    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

    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movies)

        injectDependencies()

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

то каждый раз, когда происходит изменение конфигурации, мы будем создавать новый экземпляр ViewModelFactory и, следовательно, без особой надобности создавать новые экземпляры всех его зависимостей (при условии, что они каким-то образом не ограничены).

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

    ...
}
class MoviesViewModelFactory(
    private val repository: Provider<MoviesRepository>,             
    private val stringProvider: Provider<StringProvider>,         
    private val authorisationService: Provider<AuthorisationService>
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {  
        return MoviesViewModel(repository.get(),                    
                               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)

        injectDependencies()

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

Источник: 5 распространенных ошибок при использовании архитектурных компонентов Android