VIEWMODEL trong Android

Android Apr 02, 2021

Đây là một chủ đề khá phổ biến trong Android chắc mọi người cũng đã biết đến ViewModel nhiều. Nhưng trong bài viết này mình sẽ đào sâu hơn về ViewModel trong Android và chia sẻ nhiều thứ có lẽ mọi người cũng sẽ Ah Oh khi đọc

VIEWMODEL LÀ GÌ

Các ViewModel  được thiết kế để lưu trữ và quản lý dữ liệu giao diện người dùng liên quan đến một cách có ý thức vòng đời. Trong Android việc xoay màn hình cũng khiến Activity khởi tạo lại đồng nghĩa với việc mọi thứ trong đó cũng sẽ bị khởi tạo lại theo nhưng ViewModel thì không và chúng ta cùng tìm hiểu tại làm sao ViewModel có thể làm được vậy.

public abstract class ViewModel {
   // Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
   @Nullable
   private final Map<String, Object> mBagOfTags = new HashMap<>();
   private volatile boolean mCleared = false;

   /**
    * This method will be called when this ViewModel is no longer used and will be destroyed.
    * <p>
    * It is useful when ViewModel observes some data and you need to clear this subscription to
    * prevent a leak of this ViewModel.
    */
   @SuppressWarnings("WeakerAccess")
   protected void onCleared() {
   }

...
rất nhiều 

VIEWMODEL TRONG ANDROID HOẠT ĐỘNG NHƯ THẾ NÀO?

Chúng ta cần bắt đầu từ Activity/Fragment nơi mà chúng ta sử dụng ViewModel phổ biến nhất. Vậy A/F cung cấp ViewModel cho chúng ta như thế nào để khởi tạo và giữ lại ngay cả khi màn hình bị xoay (trong Android khi xoay màn hình thì A/F sẽ bị khởi tạo lại).

Activity có lớp chính là ComponentActivity, lớp này được viết lại từ phiên bản Androidx. Trong ComponentActivity chúng ta cần để ý tới 1 biến

private ViewModelProvider.Factory mDefaultFactory;

Đây là 1 biến khai báo nơi sẽ khởi tạo ViewModel cụ thể là trong ViewModeProvider sẽ thực thi việc khởi tạo này bằng cách sử dụng interface Factory được tạo trong class đó. Và sử dụng 1 hàm get để lấy được instance của ViewModel

  ...
  
   /**
     * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
     */
    public interface Factory {
        /**
         * Creates a new instance of the given {@code Class}.
         * <p>
         *
         * @param modelClass a {@code Class} whose instance is requested
         * @param <T>        The type parameter for the ViewModel.
         * @return a newly created ViewModel
         */
        @NonNull
        <T extends ViewModel> T create(@NonNull Class<T> modelClass);
    }
    ...
...
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
   String canonicalName = modelClass.getCanonicalName();
   if (canonicalName == null) {
       throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
   }
   return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
...

Vậy instance của ViewModel được quản lý như thế nào?

Câu trả lời đó là ViewModelStore, chúng đã lưu ViewModel dưới dạng Key-Value

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

ngoài ra ViewModelStore còn được khai báo là 1 biến trong ViewModelProvider. ViewModelStore sẽ lưu trữ ViewModel dưới dạng Key-Value. Mặc dù là nơi khởi tạo ViewModel nhưng ViewModelStore chỉ chứa chúng khi và chỉ khi vòng đời của Activity chứa chúng vẫn còn sống.


Vậy Activity làm thế nào để có thể giữ được instance của class ViewModel ngay cả khi Activity bị phá huỷ, câu trả lời đó là static. Trong Java khái niệm static được chia làm 3 loại static class, variable static, function static. Chúng được sử dụng để lưu trữ một biến static trên một phân vùng bộ nhớ nhất định và dùng bộ nhớ đó cho đến khi app bị phá huỷ.

Trong Activity Component có sử dụng 1 class static đó là NonConfigurationInstance với 2 param là ViewModelStore và Custom object

...
static final class NonConfigurationInstances {
   Object custom;
   ViewModelStore viewModelStore;
}
...


Và class này hoạt động một cách có chủ đích với việc Lưu ViewModelStore vào trong nó.

Vậy ai là người có thể lấy ViewModelStore từ class trên theo Key -> câu trả lời đó là ViewModelStoreOwner.

public interface ViewModelStoreOwner {
   @NonNull
   ViewModelStore getViewModelStore();
}

ComponentActivity sẽ implementation thằng này để có thể getViewModel. quy trình get viewModel có thể gói gọn như sau nó sẽ kiểm tra ViewModelStore(được cung cấp bởi Activity) xem một viewModel phiên bản cho khóa đã cho đã được khởi tạo hay chưa. Nếu đối tượng viewModel đã có, nó chỉ trả về viewModel cá thể có trong ViewModelStore và nếu nó không hiện diện, thì đối tượng này -> ViewModelProvider sẽ sử dụng Factory thể hiện để tạo một viewModel đối tượng mới và cũng lưu trữ nó trong đó ViewModelStore.

Tóm váy lại thì chúng ta có thể hiểu đơn giản như sau là trong Activity sẽ có

1 thằng tạo ViewModel, (ViewModelProvider (Factory)

1 thằng lưu ViewModel (ViewModelStore)

1 thằng giữ instance ViewModel,  (NonConfigurationInstances)

1 thằng lấy ViewModel, (ViewModelStoreOwner)

ANDROIDVIEWMODEL

AndroidViewModel là một class được kế thừa từ ViewModel nhưng có context cụ thể ở đây là applicationContext. Tất cả chúng ta đều biết việc có phiên bản ngữ cảnh tĩnh là điều xấu vì nó có thể gây rò rỉ bộ nhớ !! Tuy nhiên, việc có ApplicationContext tĩnh không tệ như bạn nghĩ vì chỉ có một cá thể Application trong ứng dụng đang chạy.

Chúng ta nên sử dụng AndroidViewModel trong trường hợp app chúng ta được thiết kế với nhiều biến global cần context (ý kiến cá nhân).

SỬ DỤNG VIEWMODEL NHƯ THẾ NÀO CHO HIỆU QUẢ!

  1. Khởi tạo

Có khá nhiều cách để khởi tạo 1 ViewModel trong Kotlin. Tuy nhiên mới đây google phát hành Fragment-ktx kèm 1 tùy chọn khởi tạo viewModel ngắn gọn hơn và mới cho mọi người tham khảo.

Với lazy extensions

private val viewModel: YourViewModel by viewModels() 

tương đương với

private val viewModel: YourViewModel by lazy {
	ViewModelProvider(this).get(YourViewModel::class.java)
}

hoặc với Factory

private val viewModel: YourViewModel by viewModels { yourFactory }

tương đương với

private val viewModel: YourViewModel by lazy {
	ViewModelProvider(this,yourFactory).get(YourViewModel::class.java)
}

so sánh nhất thời thì mình thấy với extension chúng ta sẽ khởi tạo ngắn gọn hơn nhưng về bản chất thì chúng giống cách cũ

Cụ thể ở đây chúng ta sử dụng lazy

Khái niệm này được gọi là Lazy là khởi tạo lười ( như tên gọi của nó) Lazy là một hàm lấy lambda và trả về một instance của lazy, có thể coi như là một đại diện thực hiện một thuộc tính lazy: Lần đầu tiên gọi get() thực thi lambda để trả về kết quả lazy() và nhớ lại kết quả, các lần gọi get() tiếp theo thì chỉ cần trả về kết quả đã nhớ.

2. Sử dụng với LiveData, RxJava

Thông thường trong ViewModel sẽ có một biến LiveData hoặc 1 biến Observable (sử dụng Rx) chứa dữ liệu được cung cấp bởi Fragment/Activity hoặc Repository (DataManager)

class MyViewModel : ViewModel() {
    val _variable = MutableLiveData<Int>(10)
    val variable : LiveData<Int> get() = _variable

    fun select(item: Int) {
        _variable.value = item
    }
}

class MasterFragment : Fragment() {

    private lateinit var itemSelector: Selector

     // from the fragment-ktx artifact
    private val model: MyViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?){
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            model.select(100)
        }
        model.variable.observe(viewLifecycleOwner) { value ->
			uiView.text = value
		}
    }
}

Trường hợp muốn sử dụng ViewModel với Observable của Rxjava thì chúng ta nên làm như sau

class MyViewModel : ViewModel() {
    data class Input(
       val variable: Observable<Int>
    )
    
    data class Output(
       val outputVariable: Observable<Int>
    )
    
	fun transform(input: Input): Output {
        val mVariable = BehaviorSubject.create<Int>()
        
        input.variable.subscribe(mVariable)
        
        doCallApiExample(mVariable.value).subscribe { value ->
            //do something
        }.addTo(composite)
        
        return Output(mVariable)  
    }
}

class MasterFragment : Fragment() {
    private val mVariable = BehaviorSubject.create<Int>()
    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: MyViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?){
        super.onViewCreated(view, savedInstanceState)
        
        mVariable.onNext(1000)
        
        val output = model.transform(Input(
            variable = mVariable
        ))
        
        output.outputVariable.observeOn(AndroidSchedulers.mainThread())
            .subscribe {
            //do something with value
        }.addTo(composite)
    }

CHIA SẺ VIEWMODEL GIỮA CÁC MÀN HÌNH

có 3 trường hợp chúng ta có thể sử dụng ViewModel để chia sẻ data giữa các màn hình thay vì phải dùng callback

  1. Activity - Fragment
  2. Fragment - Fragment
  3. Fragment (cha) - Fragment (con)

em sẽ đưa ví dụ phổ biến nhất đó là Fragment - Fragment

Đầu tiên chúng ta có 1 ViewModel cần chia sẻ

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

và ViewModel này được dùng ở 2 Fragment

class FirstFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // 
    // ownerProduct is (ViewModelStoreOwner)
    private val model: SharedViewModel
    		by viewModels(ownerProduct = {requireActivity()})

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            model.select(Item())
        }
    }
}

và ở Fragment 2

class SecondFragment : Fragment() {

    // 
    // from the fragment-ktx artifact
    private val model: SharedViewModel 
    		by viewModels(ownerProduct ={ requireActivity() })

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner) { item ->
            // Update the UI
        })
    }
}

Bằng cách trên thì mỗi fragment sẽ biết được ViewModelStoreOwner thông quan requireActivity()  để có thể thực hiện lấy đúng instance của ViewModel

chi tiết sử dụng chia sẻ viewModel sẽ có ở bài present và link github dưới đây
https://github.com/nguyenvanminhc9nvm/DemoSharedViewModels


Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.