5 errores comunes al usar componentes arquitectónicos de Android

5 errores comunes al usar componentes arquitectónicos de Android


Incluso si no comete estos errores, vale la pena recordarlos para no tener problemas en el futuro.


1. Fuga de observadores de LiveData en fragmentos


Los fragmentos tienen un ciclo de vida complejo, y cuando un fragmento se desconecta y vuelve a unirse a una Actividad, no siempre se destruye. Por ejemplo, los fragmentos guardados no se destruyen durante los cambios de configuración. Cuando esto sucede, la instancia del fragmento permanece y solo se destruye su Vista, por lo que onDestroy() no se llama y no se alcanza el estado DESTRUIDO .


Esto significa que si comenzamos a observar LiveData en onCreateView() o posterior (generalmente en onActivityCreated() ) y pasamos el fragmento como 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 } ... } 

luego transmitiremos una nueva instancia idéntica de Observer cada vez que se vuelva a unir un fragmento, pero LiveData no eliminará los observadores anteriores porque LifecycleOwner (fragmento) no ha alcanzado el estado DESTRUIDO. En última instancia, esto conduce a un número creciente de observadores idénticos y simultáneamente activos, y el mismo código de onChanged() se ejecuta varias veces.


El problema se informó originalmente aquí , y se puede encontrar una explicación más detallada aquí .


La solución recomendada es usar getViewLifecycleOwner () o getViewLifecycleOwnerLiveData () del ciclo de vida del fragmento, que se agregaron a la biblioteca de soporte 28.0.0 y AndroidX 1.0.0, para que LiveData elimine observadores cada vez que se destruya el fragmento de Vista:


 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. Reiniciar datos después de cada rotación de pantalla


Estamos acostumbrados a poner la lógica de inicialización y configuración en onCreate() en Activity (y por analogía en onCreateView() o más adelante en fragmentos), por lo que en este punto puede ser tentador iniciar la carga de algunos datos en ViewModels. Sin embargo, dependiendo de su lógica, esto puede conducir a una recarga de datos después de cada rotación de la pantalla (incluso si se utilizó ViewModel), que en la mayoría de los casos simplemente no tiene sentido.


Un ejemplo:


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

La decisión también depende de tu lógica. Si el repositorio almacenará datos en caché, entonces el código anterior probablemente funcionará correctamente. Además, este problema se puede resolver de otras maneras:


  • Use algo similar a AbsentLiveData y comience a descargar solo si no se han recibido datos;
  • Comience a descargar datos cuando sea realmente necesario. Por ejemplo, en OnClickListener;
  • Y probablemente el más simple: coloque llamadas de carga en el constructor ViewModel y use captadores simples:

 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. Modelos de vista de fuga


Los desarrolladores de arquitectura dejaron en claro que no debemos pasar los enlaces de Vista al ViewModel.


Advertencia


pero también debemos tener cuidado al pasar referencias a ViewModels a otras clases. Una vez que se completa la Actividad (o fragmento), no se puede hacer referencia al ViewModel a ningún objeto que la Actividad pueda sobrevivir, por lo que el Recolector de basura puede destruir el ViewModel.


En este ejemplo de fuga, el oyente del repositorio, escrito en estilo Singleton, se pasa al ViewModel. Posteriormente, el oyente no se destruye:


 @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 solución aquí podría ser eliminar el oyente en el método onCleared() , guardarlo como WeakReference en el repositorio o usar LiveData para comunicarse entre el repositorio y 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. Permitir que View modifique LiveData


Esto no es un error, pero contradice la separación de intereses. View (fragmentos y actividad) no debería poder actualizar LiveData y, por lo tanto, su propio estado, ya que es responsabilidad de ViewModels. View solo debería poder ver LiveData.


Por lo tanto, debemos encapsular el acceso a 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. Crear dependencias de ViewModel después de cada cambio de configuración


Los ViewModels resisten los cambios de configuración, como la rotación de la pantalla, por lo que si los crea cada vez que se produce un cambio, a veces puede conducir a un comportamiento impredecible, especialmente si el diseñador tiene algo de lógica.


Aunque esto puede parecer bastante obvio, es algo que es fácil pasar por alto cuando se utiliza ViewModelFactory, que generalmente tiene las mismas dependencias que ViewModel que crea.


ViewModelProvider guarda una instancia de ViewModel, pero no una instancia de ViewModelFactory, así que si tenemos este código:


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

luego, cada vez que se produce un cambio de configuración, crearemos una nueva instancia de ViewModelFactory y, por lo tanto, sin necesidad especial de crear nuevas instancias de todas sus dependencias (siempre que no estén limitadas de ninguna manera).


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

Source: https://habr.com/ru/post/454424/


All Articles