
Même si vous ne commettez pas ces erreurs, il vaut la peine de s'en souvenir afin de ne pas rencontrer de problèmes à l'avenir.
1. Fuite d'observateurs LiveData en fragments
Les fragments ont un cycle de vie complexe, et lorsqu'un fragment se déconnecte et rejoint une activité, il n'est pas toujours détruit. Par exemple, les fragments enregistrés ne sont pas détruits lors des modifications de configuration. Lorsque cela se produit, l'instance de fragment reste et seule sa vue est détruite, donc onDestroy()
pas appelé et l'état DESTROYED n'est pas atteint.
Cela signifie que si nous commençons à observer LiveData dans onCreateView()
ou version ultérieure (généralement dans onActivityCreated()
) et passons le fragment en tant que 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) })
nous transmettrons ensuite une nouvelle instance identique d'Observer à chaque fois qu'un fragment se joint à nouveau, mais LiveData ne supprimera pas les observateurs précédents car LifecycleOwner (fragment) n'a pas atteint l'état DESTROYED. Cela conduit finalement à un nombre croissant d'observateurs identiques et actifs simultanément, et le même code de onChanged()
est exécuté plusieurs fois.
Le problème a été initialement signalé ici , et une explication plus détaillée peut être trouvée ici .
La solution recommandée consiste à utiliser getViewLifecycleOwner () ou getViewLifecycleOwnerLiveData () du cycle de vie du fragment, qui ont été ajoutés à la bibliothèque de support 28.0.0 et AndroidX 1.0.0, afin que LiveData supprime les observateurs chaque fois que le fragment View est détruit:
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. Redémarrage des données après chaque rotation d'écran
Nous sommes habitués à mettre la logique d'initialisation et de configuration dans onCreate()
dans Activity (et par analogie dans onCreateView()
ou plus tard dans des fragments), donc à ce stade, il peut être tentant de lancer le chargement de certaines données dans ViewModels. Cependant, selon votre logique, cela peut entraîner un rechargement des données après chaque rotation de l'écran (même si le ViewModel a été utilisé), ce qui dans la plupart des cas est tout simplement inutile.
Un exemple:
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()
La décision dépend également de votre logique. Si le référentiel mettra en cache les données, le code ci-dessus fonctionnera probablement correctement. En outre, ce problème peut être résolu d'autres manières:
- Utilisez quelque chose de similaire à AbsentLiveData et commencez à télécharger uniquement si les données n'ont pas été reçues;
- Commencez à télécharger des données lorsque cela est vraiment nécessaire. Par exemple, dans OnClickListener;
- Et probablement le plus simple: mettez des appels de charge dans le constructeur ViewModel et utilisez des getters simples:
class ProductViewModel( private val repository: ProductRepository ) : ViewModel() { private val productDetails = MutableLiveData<Resource<ProductDetails>>() private val specialOffers = MutableLiveData<Resource<SpecialOffers>>() init { loadProductsDetails()
3. Modèles de vue de fuite
Les développeurs d'architecture ont clairement indiqué que nous ne devrions pas transmettre de liens View au ViewModel.

mais nous devons également être prudents lors du passage de références à ViewModels à d'autres classes. Une fois l'activité (ou le fragment) terminée, le ViewModel ne peut être référencé à aucun objet auquel l'activité peut survivre, de sorte que le ViewModel peut être détruit par le garbage collector.
Dans cet exemple de fuite, l'écouteur de référentiel, écrit dans le style Singleton, est passé au ViewModel. Par la suite, l'auditeur n'est pas détruit:
@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 } } }
La solution ici pourrait être de supprimer l'écouteur dans la méthode onCleared()
, de l'enregistrer en tant que WeakReference
dans le référentiel ou d'utiliser LiveData pour communiquer entre le référentiel et 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. Autorisez View à modifier LiveData
Ce n'est pas un bug, mais cela contredit la séparation des intérêts. View - fragments et Activity - ne devrait pas pouvoir mettre à jour LiveData et, par conséquent, son propre état, car c'est la responsabilité de ViewModels. La vue doit pouvoir uniquement regarder LiveData.
Par conséquent, nous devons encapsuler l'accès à MutableLiveData:
class CatalogueViewModel : ViewModel() {
5. Création de dépendances ViewModel après chaque modification de configuration
Les ViewModels résistent aux changements de configuration, tels que la rotation de l'écran, donc si vous les créez chaque fois qu'un changement se produit, cela peut parfois conduire à un comportement imprévisible, en particulier si une logique est intégrée au concepteur.
Bien que cela puisse sembler assez évident, c'est quelque chose qui est facile à ignorer lors de l'utilisation de ViewModelFactory, qui a généralement les mêmes dépendances que le ViewModel qu'il crée.
ViewModelProvider enregistre une instance de ViewModel, mais pas une instance de ViewModelFactory, donc si nous avons ce code:
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) } ... }
puis chaque fois qu'un changement de configuration se produit, nous créerons une nouvelle instance de ViewModelFactory et, par conséquent, sans besoin particulier de créer de nouvelles instances de toutes ses dépendances (à condition qu'elles ne soient en aucune façon limitées).
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) } ... }