Как стать автором
Обновить

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

Время на прочтение 7 мин
Количество просмотров 8K
Автор оригинала: Michal Baran

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? {
        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) })  // передача фрагмента как LifecycleOwner
    }

    ...
}

то мы будем передавать новый идентичный экземпляр 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 = ViewModelProviders.of(this).get(BooksViewModel::class.java)

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

    ...
}

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


Мы привыкли помещать логику инициализации и настройки в onCreate() в Activity (и по аналогии в onCreateView() или позже во фрагментах), поэтому на этом этапе может быть заманчиво инициировать загрузку некоторых данных во ViewModels. Однако, в зависимости от вашей логики, это может привести к перезагрузке данных после каждого поворота экрана (даже если использовалась 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()  // Загрузка ProductDetails
        ...                             // Получение ProductDetails и обновление productDetails LiveData
        return productDetails
    }

    fun loadSpecialOffers() {
        repository.getSpecialOffers()   // Загрузка SpecialOffers
        ...                             // Получение SpecialOffers и обновление 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 { /*...*/ })  // (возможно) Повторный запрос productDetails после поворота экрана
        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 {
        loadProductsDetails()           // ViewModel создаётся только один раз в ходе жизненных циклов Activity или фрагмента
    }

    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 = ViewModelProviders.of(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.


Предупреждение


но мы также должны быть осторожны при передаче ссылок на ViewModels другим классам. После завершения 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 и, следовательно, своё собственное состояние, потому что это ответственность ViewModels. 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.value = loadProducts()     // Другие классы смогут изменять products
        promotions.value = loadPromotions() // Только CatalogueViewModel может изменять promotions
        _offers.value = loadOffers()        // Only CatalogueViewModel может изменять _offers
    }
}

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

    ...
}
Теги:
Хабы:
+13
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн