5 lỗi thường xuyên gặp khi sử dụng Android Architecture Components
Bài đăng này đã không được cập nhật trong 6 năm
Đầu tiên chúng ta hãy cũng nhắc lại 1 chút đó là Android Architecture Components (AAC) là gì? Thì ta hiểu đơn giản đó là một tập hợp các thư viện mạnh mẽ hỗ trợ developer trong việc thiết kế, test và maintain dễ dàng. Có thể quản lí vòng đời UI và xử lí dữ liệu của chúng. Android Architecture Components
Tuy nhiên trong quá trình áp dụng bộ thư viện này vào có thể bạn sẽ gặp những vấn đề dưới đây khiến nó có thể đi ngược lại tác dụng mà nó sinh ra. Dù bạn đã từng sử dụng AAC rồi hay là chưa thì hãy cùng tôi điểm qua 5 lỗi cơ bản mà sẽ dễ gặp nhất của các developer khi sử dụng AAC nhé. Từ đó chúng ta có thể tránh gặp phải khi áp dụng nó vào dự án của mình nhé.
1. Leaking LiveData observers in Fragments
Các Fragments có vòng đời khác phức tạp và khi 1 fragment detach và re-attach thì không phải lúc nào nó cũng rơi vào destroy. Ví dụ các retain fragment nó không bị destroy khi mà configure change. Khi điều đó xảy ra instance của fragment sẽ tồn tại và chỉ các view của nó destroy vì thế hàm onDestroy() sẽ không được gọi và trạng thái DESTROYED sẽ không có.
Điều đó có nghĩa là nếu chúng ta lắng nghe LiveData trong onCreateView() hoặc sau đó như là onActivityCreated() và truyển Fragment như một LifecycleOwner như dưới đây:
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
}
...
}
Theo như code phía trên thì mỗi khi Fragment re-attach sẽ có thêm 1 Observer được tạo ra, vì LiveData chưa remove observer trước đó do LifecyclerOwner chưa rơi vào trạng thái DETROYED. Điều đó sẽ dần đến ngày tăng lên các observer đồng nghĩa với việc hàm onChange() sẽ được gọi nhiều lần.
Issuse này đã được báo cáo tại đây và nếu cần nhiều hơn thế thì bạn có thể tham khảo ở đây
Giải pháp được đưa ra ở đây là ta dùng view lifecycle của fragment qua hàm getViewLifecycleOwner() hoặc getViewLifecycleOwnerLiveData() 2 hàm này được thêm vào lib Support Library 28.0.0 và AndroidX 1.0.0. Vì thế LiveData sẽ remove observer mỗi khi view của fragment 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 logic khởi tạo trong onCreate() trong các Activity hoặc onCreateView() trong Fragment để có thể tải dữ liệu trong ViewModel tại thời điểm này. Tuy nhiên điều này có thể xảy ra lỗi mỗi khi xoay điện thoại (Nếu app của bạn hỗ trợ rotation) , gây ra nhiều vấn đề không mong 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
}
}
Giải pháp cho vấn đề này thì còn phụ thuộc vào logic của bạn. Ví dụ Repository cache data thì code phía trên sẽ ổn. Một số giải pháp khác sẽ là :
- Sử dụng thứ gì đó giống như AbsentLiveData và chỉ loading data khi mà data đó chưa được set
- Chỉ load dữ liệu khi thực sự cần thiết, ví dụ như khi onClickListener
- Hay đơn giản nhất ta cho hàm loading vào constructor của ViewModel vàn tạo getter setter cho nó
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
Việc này đã được nhấn mạnh rõ ràng rẳng chúng ta không nên truyền tham chiếu View vào ViewModel
Nhưng chúng ta cũng nên thận trọng khi truyền tham chiếu của ViewModel đến các class khác. Sau khi Activity hoặc Fragment kết thúc thì ViewModel không nên tham chiếu đến bất cứ đối tượng nào, vì vậy ViewModel sẽ bị bộ dọn rác thu hồi.
Một ví dụ leak có thể được truyền trong ViewModel 1 listener đến Repository, 1 singleton và không rõ ràng listener 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 có thể là remove listener trong onCleared(), và lưu trữ nó dạng WeakReference trong Repository, sử dụng LiveData để giao tiếp giữ Repository và 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 { // 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
Đây không phải là bug nhưng nó lại đi ngược lại separation of concerns. Views - Fragments - Activities - không thể cập nhật LiveData và trạng thái do đó là responsibility của ViewModels. View nên lắng nghe LiveData. Do đó chúng ta nên set quyền truy cập vào MutableLiveData ví dụ setter getter và một số thuộc tính khác.
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
}
}
5. Creating ViewModel’s dependencies after every configuration change
ViewModel bị ảnh hưởng khi rơi vào configure change như là rotation vì vậy tạo phụ thuộc mỗi khi thay đổi xảy ra chỉ đơn giản là thừa và đôi khi dẫn đến những thứ không mong muốn, đặc biệt logic đưa vào các hàm phụ thuộc. Mặc dù điều này nghe có vẻ khá đơn giản nhưng dễ bị quên hay bỏ qua khi sử dụng ViewModelFactory, thường có các phụ thuộc gióng như ViewModel mà nó tạo ra. ViewModelProvider tạo ra ViewModel intance nhưng không phải là ViewModelFactory instance, vì thế nếu chúng ta có đoạn code như sau :
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 khi configure change xảy ra, chúng ta sẽ tạo ra 1 instance mới của ViewModelFactory và do đó không cần tạo ra các instance của các phụ thuộc của nó. Các giải quyết ở đây là trì hoãn việc tạo các phụ thuộc cho đến khi hàm create() thực sự được gọi bởi vì nó chỉ được gọi 1 lần trong suốt vòng đời Activity và Fragment . Chúng ta có thể làm như sau:
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)
}
...
}
Như vậy tôi đã điểm qua 5 lỗi cơ bản mà khi làm việc với Android Architecture Components dễ mắc phải, qua bài viết này tôi mong các bạn có thể học hỏi được những kiến thức thú vị để phục vụ cho công việc và học tập.
Nguồn : ProAndroidDev
All rights reserved