+63

Android - Architecture Components ViewModel - xử lý configuration changes chưa bao giờ đơn giản đến thế.

Rất cảm ơn tất cả các bạn đã đọc và ủng hộ cho 2 bài viết trước về Architecture Components

  1. Android - Bạn biết gì về Architecture Components
  2. Giới thiệu về Room Persistence Library

Tiếp tục seri về Architecture Components, lần này mình xin tiếp tục giới thiệu chi tiết về một Component nữa ViewModel

I. Giới thiệu chung về ViewModel

ViewModel là một class được thiết kế để lưu trữ và quản lý các dữ liệu trong một lifecycle riêng, nó cho phép dữ liệu được bảo toàn ngay cả khi màn hình bị xoay. Chắc hẳn trong các bạn ai cũng đã từng làm việc với việc xoay màn hình rồi nhỉ.

Như các bạn đã biết thì các bạn không phải là người quản lý vòng đời của UI controllers (activity và fragment) mà chính Hệ thống mới là BOSS thật sự, nó có thể quyết định khi nào thì huỷ hoặc tái khởi tạo lại UI controllers để đáp ứng các hành động nhất định của người dùng hoăc thiết bị nà các bạn không thể control được

Nếu hệ thống huỷ hoặc khởi tạo lại các UI controller, tất cả các dữ liệu tạm đang được lưu trữ sẽ biến mất.

Mình có 2 ví dụ như sau

  1. Ví dụ đầu tiên khá đơn giản Các bạn có một ứng dụng đơn giản hiển thị điểm của 2 đội A và B. Có các button + điểm, mỗi lần click vào button + điểm thì điểm của các đội tương ứng sẽ được tăng lên và hiển thị lên màn hình. Tuy nhiên khi bạn xoay màn hình điện thoại thì điểm hiện tại sẽ biến mất.

    Với các trường hợp đơn giản này các bạn có thể sử dụng hàm onSaveInstanceState() và restore data từ bundle ra trong hàm onCreate()

  2. Trường hợp phức tạp hơn chút Ứng dụng của bạn có một activity chứa một danh sách các user, khi activity bị khởi tạo lại do configuration change (có thể là do quay màn hình chẳng hạn), lúc này toàn bộ danh sách user của các bạn đã bị biến mất

Lúc này restore data từ bundle sẽ khó khăn hơn vì thông tin về dữ liệu về danh sách user lớn hoặc có chưa bitmap. Việc serialized và deserialized sẽ tốn nhiều thời gian và cách sử lý như thế này là chưa tốt.

II. Vòng đời của ViewModel

Chắc hẳn các bạn ai mới bắt đầu code đầu học qua Activity Lifecycle và thấy nó vô cùng phức tạp đúng không nào Và một acitvity sẽ trải qua rất nhiều state khi mà có configuration change thì các bạn cần phải lưu trữ dữ liệu hiện tại trên activity của các bạn lại và đổ nó ra khi acitivity được tái khởi động. Có thể là dữ liệu do người dùng input, có thể là dữ liệu được khởi tạo trong quá trình hoạt động hoặc cũng có thể là dữ liệu từ database .... Cụ thể trong trường hợp 1 là điểm của đội A và điểm của đội B, trường hợp thứ 2 là bitmap và danh sách user của recyclerview.

Trước khi sử dụng ViewModel thì tôi đã từng giải quyết bài toán này bắc cách sử dụng cách đầu tiên

  • sử dụng hàm onSaveInstanceState() và restore data từ bundle ra trong hàm onCreate() tuy nhiên cách này implement khá mất thời gian, mà trong khi đó dữ liệu hiện tại của chúng ta thì ko cần quan tâm đến lifecycle của activity. Dù trạng thái của activity thế nào thì giá trị của chúng vẫn không thay đổi.

Chính vì thế ViewModel đã ra đời

Như các bạn thấy thì ViewModel được tạo ra khi lần đầu tiên các bạn request đến ViewModel thông thường là trong onCreate của Activity hoặc onViewCreated của Fragment và nó tồn tại cho đến khi activity hoặc fragment bị huỷ. Hàm onCreate có thể được gọi lại nhiều lần nhưng các lần sau nếu viewmodel đã tồn tại thì nó không được tạo mới mà vẫn gọi là viewmodel cũ đã được khởi tạo trước đó (chú ý là trong trường hợp configuration change thôi nhé, còn trường hợp activity hoặc fragment bị huỷ thì sẽ là 1 viewmodel mới)

III. Cách implement ViewModel

Để implement ViewModel và LiveData cho project các bạn cần cấu hình như sau

  1. Mở build.gradle (Project: <your_app>) và thêm dòng code sau
allprojects {
    repositories {
        jcenter()
       google()
    }
}
  1. Mở build.gradle (app) và thêm dependencies
dependencies {
    // ViewModel and LiveData
    implementation "android.arch.lifecycle:extensions:1.0.0"
    annotationProcessor "android.arch.lifecycle:compiler:1.0.0"
    // Other implementation
}

Đây là ví dụ mình đã thực hiện, các bạn cùng theo dõi nhé.

Bước 1. Khởi tạo một class ViewModel

Thông thường với mỗi 1 UI Controller (activity hoặc fragment) chúng ta sẽ tạo 1 class ViewModel để lưu trữ data cho Controller đó.

Trong ví dụ của mình như sau CountNumberViewModel

public class CountNumberViewModel extends AndroidViewModel {
    private MutableLiveData<Integer> mScoreTeamA = new MutableLiveData<>();
    private MutableLiveData<Integer> mScoreTeamB = new MutableLiveData<>();

    public CountNumberViewModel(@NonNull Application application) {
        super(application);
        mScoreTeamA.setValue(0);
        mScoreTeamB.setValue(0);
    }

    public MutableLiveData<Integer> getScoreTeamA() {
        return mScoreTeamA;
    }

    public MutableLiveData<Integer> getScoreTeamB() {
        return mScoreTeamB;
    }

    public void increaseScroeTeamA(int score) {
        mScoreTeamA.setValue(mScoreTeamA.getValue() + score);
    }

    public void increaseScroeTeamB(int score) {
        mScoreTeamB.setValue(mScoreTeamB.getValue() + score);
    }
}

Ở trong ví dụ này mình có sử dụng LiveData để có thể cập nhật giao diện real time, ngay khi scoreTeamA và scoreTeamB có sự thay đổi.

Bước 2. Liên kết UI Controller với ViewModel

UI Controller (activity hoặc fragment) cần biết tới ViewModel vì UI Controller cần ViewModel để hiển thị dữ liệu lên giao diện và update giao diện khi dữ liệu có sự thay đổi.

Như mình đã nói ở trên thì ViewModel sẽ luôn tồn tại ngay cả khi activity được khởi động lại do đó, trong ViewModel các bạn KHÔNG NÊN lưu trữ Activities, Fragments, hoặc Contexts và hơn nữa cũng không nên lưu trữ những phần tử mà có chứa references tới activitíes, fragments, hay contexts. Lý do là bởi vì trong quá trình activity bị khởi động lại thì Activities, Fragments, hoặc Contexts hoặc những phần tử liên kết tới chúng đều bị null.

Vậy trong trường hợp ViewModel của các bạn bắt buộc cần một Context thì sao nhỉ? Hãy sử dụng Application context nhé, vì Context của activities hoặc Fragments sẽ bị mất khi configuration change còn Application Context thì luôn tồn tại khi mà ứng dụng hoạt động.

Để có thể Request một ViewModel đã tạo ra ở trên các bạn có thể sử dụng ViewModelProviders thông qua câu lệnh

ViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class)

Trong ví dụ của mình như sau

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_count);
        mViewModel = ViewModelProviders.of(this).get(CountNumberViewModel.class);
        // ...
    }

Mặc dù CountNumberViewModel.java có constructor

 public CountNumberViewModel(@NonNull Application application) {
        super(application);
    }

Nhưng để request ViewModel các bạn đừng tạo bằng cách new CountNumberViewModel(this) nhé, với cách này thì bạn đã tạo ra một ViewModel mới chứ ko phải ViewModel đã được tạo trước đó đâu.

Bước 3: Dùng ViewModel để cập nhật giao diện

Để update giao diện bạn có thể sử dụng data đã lưu trữ trong ViewModel, cụ thể ở đây là

    private MutableLiveData<Integer> mScoreTeamA = new MutableLiveData<>();
    private MutableLiveData<Integer> mScoreTeamB = new MutableLiveData<>();

ViewModel hoạt động rất tốt với các Architeture Components khác như LiveData, ở bài viết tới mình sẽ giới thiêuij chi tiết hơn về LiveData nhé.

Ở ví dụ của mình mình update dữ liệu như sau CountNumberWithViewModelActivity.java

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_count);
        mViewModel = ViewModelProviders.of(this).get(CountNumberViewModel.class);
        registerLiveDataListenner();
        initViews();
        setTitle("Count with ViewModel");
    }
    
    public void registerLiveDataListenner() {
        mViewModel.getScoreTeamA().observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(@Nullable Integer integer) {
                mTextScoreTeamA.setText(String.valueOf(integer));
            }
        });
        mViewModel.getScoreTeamB().observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(@Nullable Integer integer) {
                mTextScoreTeamB.setText(String.valueOf(integer));
            }
        });
    }

Mỗi khi có sự thay đổi về điểm số của teamA và teamB mình sẽ update lên giao diện thông qua 2 TextView. Và khi user click vào button cộng điểm thì mình sẽ update dữ liệu về điểm trong ViewModel CountNumberWithViewModelActivity.java

@Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_plus_a_3:
                mViewModel.increaseScroeTeamA(3);
                break;
            case R.id.button_plus_a_2:
                mViewModel.increaseScroeTeamA(2);
                break;
            case R.id.button_plus_a_1:
                mViewModel.increaseScroeTeamA(1);
                break;
            case R.id.button_plus_b_3:
                mViewModel.increaseScroeTeamB(3);
                break;
            case R.id.button_plus_b_2:
                mViewModel.increaseScroeTeamB(2);
                break;
            case R.id.button_plus_b_1:
                mViewModel.increaseScroeTeamB(1);
                break;
        }
    }

IV. Truyền data giữa các fragments thông qua ViewModel

Một ưu điểm nữa của ViewModel là các bạn có thể chia sẻ data giữa 2 hoặc nhiều fragments trên cùng 1 activity mà không cần phải tạo connection gì cả. Bài toán đặt ra Trong 1 activity mình có 2 fragment MasterFragment và DetailFragment MasterlFragment chứa danh sách các User DetailFragment hiển thị chi tiết thông tin của User Mỗi khi click vào một user ở MasterFragment thì thông tin detail sẽ được hiển thị trên DetailFragment.

Với cách thông thường tôi sẽ làm như sau

  1. Khi click vào một user ở trên MasterFragment tôi gửi Callback tới Activity
  2. Activity nhận được Callback gửi Callback này tới DetailFragment
  3. DetailFragment nhận được Callback sẽ update giao diện 😡 hầy dài dòng quá.

Với ViewModel thì khác nè. Các fragments trong cùng một activity hoàn toàn có thể chia sẻ ViewModel với nhau để có thể giao tiếp với nhau mà không cần thông qua activity. Các bạn có thể tham khảo đoạn code sau.

public class MasterViewModel extends AndroidViewModel {
   // Trong master có một biến là selected item, mỗi khi có sự kiện click thì selected item được thay đổi
   private MutableLiveData<String> mSelectedItem = new MutableLiveData<>();

   public MasterViewModel(@NonNull Application application) {
       super(application);
   }
   public MutableLiveData<String> getSelectedItem() {
       return mSelectedItem;
   }

   public void setSelectedItem(String selectedItem) {
       mSelectedItem.setValue(selectedItem);
   }
}
//************************************************
public class MasterFragment extends Fragment implements MasterAdapter.OnItemClickListenner{
   private MasterViewModel mViewModel;
   
   @Override
   public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       mViewModel = ViewModelProviders.of(getActivity()).get(MasterViewModel.class);
       mViewModel.getData().observe(this, new Observer<List<String>>() {
           @Override
           public void onChanged(@Nullable List<String> strings) {
               mAdapter.addData(strings);
           }
       });
   }
   
    @Override
   public void onClick(String result) {
       // Đây là sự kiện click vào một item trong recyclerview
       mViewModel.setSelectedItem(result);
   }
   
//************************************************
 public class DetailFragment extends Fragment{
    private MasterViewModel mViewModel;
     
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       // Trong DetailFragment sử dụng MasterViewModel, và lắng nghe sự kiện getSelectedItem
       mViewModel = ViewModelProviders.of(getActivity()).get(MasterViewModel.class);
       mViewModel.getSelectedItem().observe(this, new Observer<String>() {
           @Override
           public void onChanged(@Nullable String s) {
               mUserName.setText(s);
           }
       });
   }
}

Lưu ý: Tất cả các fragments muốn chia sẻ ViewModel cần sử dụng hàm getActivity() khi khởi tạo ViewModel thông qua ViewModelProvider. Với những điểm trên ViewModel mang lại những giá trị

  1. Đơn giản, Activity không cần biết tới và điều khiển sự liên kết giữa các fragments
  2. Độc lập, Các fragments không cần biết tới nhau, nếu một fragment biến mất các fragment khác vẫn hoạt động bình thường.

Đây là sample của mình

V. Tổng kết

Trên đây là bài hướng dẫn của mình về ViewModel

  • Bài viết có tham khảo nguồn
  1. https://developer.android.com/topic/libraries/architecture/viewmodel.html
  2. https://medium.com/google-developers/viewmodels-a-simple-example-ed5ac416317e
  • Link project demo tại đây
  • Mình cũng làm một project về MVVM + Architecture Components + Dagger2 các bạn có thể theo dõi tại đây và feedback cho mình nhé

Cám ơn các bạn đã đón đọc và chúc các bạn học tốt!


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í