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 Android - Bạn biết gì về Architecture Components 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 ...
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
- Android - Bạn biết gì về Architecture Components
- 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
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
-
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() -
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.
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)
Để implement ViewModel và LiveData cho project các bạn cần cấu hình như sau
- Mở build.gradle (Project: <your_app>) và thêm dòng code sau
allprojects { repositories { jcenter() google() } }
- 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; } }
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
- Khi click vào một user ở trên MasterFragment tôi gửi Callback tới Activity
- Activity nhận được Callback gửi Callback này tới DetailFragment
- DetailFragment nhận được Callback sẽ update giao diện