5 lỗi chung khi sử dụng Architecture Components
Bài đăng này đã không được cập nhật trong 5 năm
1. Leaking LiveData observers in Fragments
Fragment có vòng đời khá phức tạp và khi đã detach hoặc re-attach, không phải lúc nào nó cũng được destroy. Ví dụ, fragment còn lại không bị destroy suốt quá trình configure thay đổi, vấn đề bắt đầu xảy ra, instance của fragment vẫn tồn tại và chỉ các view của nó bị destroy, dẫn đến onDestroy()
không được gọi, cuối cùng trạng thái Destroy sẽ không được reach.
Điều này có nghĩa là nếu chúng ta bắt đầu observe LiveData
trong onCreateView()
hoặc sau đó (thông thường là trong onActivityCreated()
) và truyền Fragment dưới dạng LifecycleOwner
như:
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
}
...
}
Chúng ta kết thúc bằng việc truyền vào một instance mới của Observer giống hệt nhau mỗi lần fragment được re-attach, nhưng LiveData lại không remove các observer trước đó. Bởi vì, LifecycleOwner (Fragment) không reach trạng thái Destroy. Cuối cùng dẫn đến kết quả số lượng lớn các observer giống nhau active cùng một lúc và cùng một code từ onChanged()
được thực thi nhiều lần.
Vấn đề ban đầu được report ở đây và để hiểu sâu hơn bạn có thể tìm thấy ở đây.
Để giải quyết vấn đề này chúng ta có thể dùng vòng đời của view trong fragment qua getViewLifecycleOwner()
hoặc getViewLifecycleOwnerLiveData()
, các function này được add vào Support Library 28.0.0 và AndroidX 1.0.0, để LiveData remove các observers mỗi lần view của fragment bị destroy.
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
}
...
}
2. Reloading data after every rotation
Chúng ta thường đặt việc khởi tạo hoặc setup logic trong onCreate()
của Activity (tương tự việc đặt trong onCreateView()
hoặc sau đó của Fragment). Do vậy, chúng ta có thể loading data trong ViewModels tại thời điểm này luôn. Tất cả phụ thuộc vào logic của chúng ta, tuy nhiên điều này có thể gây ra việc re-load dữ liệu sau mỗi lần chúng ta rotation (mặc dù ViewModel đã được sử dụng), trong hầu hết các trường hợp thường chỉ là ngoài ý muốn.
Ví dụ:
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
}
}
Có nhiều solution cho vấn đề này. Ví dụ như Repository sẽ lưu trữ data trong cache hoặc một số solution khác là:
- Sử dụng LiveData khác tương tự
AbsentLiveData
và bắt đầu loading chỉ khi data không được set. - Bắt đầu load data khi nó thật sự cần, ví dụ có một Click event chẳng hạn.
- Hoặc đơn giản nhất: Thực hiện việc gọi loading trong constructor của ViewModel.
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() }
}
}
3. Leaking ViewModels
Highlight dưới đây yêu cầu rõ ràng rằng chúng ta không nên truyền tham chiếu của view cho ViewModel.
Nhưng chúng ta cũng nên cẩn thận về việc truyền tham chiếu của các class khác đến ViewModel. Sau khi Activity (tương tự với Fragment) được finish. ViewModel không được tham chiếu đến bất kì object nào khác có thể tồn tại lâu hơn Activity, điều này để đảm bảo rằng ViewModel có thể được garbage collected.
Một ví dụ về leak đó là chúng ta truyền vào ViewModel một listener của Repository, cái này có scoped là Singleton, và listener không được dọn dẹp sau đó.
@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
}
}
}
Giải pháp ở đây đó là chúng ta có thể remove listener trong onCreated()
, lưu trữ nó như WeakReference
trong Repository, sử dụng LiveData để giao tiếp giữa Repository và ViewModel hoặc về cơ bản là bất cứ điều gì miễn nó phù hợp và đảm bảo garbage collection chính xác.
@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
}
}
4. Exposing LiveData as mutable to views
Các View, Fragment và Activity - có thể không thể cập nhật được LiveData và trạng thái của chúng bởi vì đó là trách nhiệm của ViewModels. Các View chỉ có thể quan sát LiveData.
Do đó, chúng ta nên đóng gói quyền truy cập vào MutableLiveData, vd. getters hoặc sao lưu các thuộc tính.
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
}
}
view raw
5. Creating ViewModel’s dependencies after every configuration change
ViewModels tồn tại các thay đổi cấu hình như rotations. Do đó, việc tạo ra các phụ thuộc của chúng mỗi khi thay đổi xảy ra chỉ đơn giản là dư thừa và đôi khi có thể dẫn đến hành vi ngoài ý muốn, đặc biệt là nếu có logic đưa vào các hàm khởi tạo phụ thuộc.
Mặc dù điều này nghe có vẻ khá rõ ràng, nhưng nó có một điều mà chúng ta có thể bỏ qua khi sử dụng ViewModelFactory, đó là thường có cùng các phụ thuộc như ViewModel được tạo ra.
ViewModelProvider giữ instance của ViewModel, chứ không phải là instance ViewModelFactory, vì vậy nếu chúng ta có code như thế này:
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)
}
...
}
Mỗi lần config thay đổi xảy ra, chúng ta sẽ tạo một instance mới của ViewModelFactory và do đó không cần thiết phải tạo ra các instance mới của tất cả các phụ thuộc của nó (giả sử rằng chúng không phải là scoped nào khác).
Giải pháp lần này đó là trì hoãn việc tạo ra các phụ thuộc cho đến khi phương thức create()
thật sự được gọi, bởi vì nó chỉ được gọi một lần trong suốt vòng đời của Activity/Fragment. Chúng ta có thể làm điều này bằng việc sử dụng khởi tạo lazy, ví dụ: Providers
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)
}
...
}
Tham khảo
All rights reserved