0

5 lỗi thường xuyên gặp khi sử dụng Android Architecture Components

Đầ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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí