Model-View-ViewModel Achitecture in Android without Data Binding library
Chào mọi người, chắc hẳn chúng ta khi bắt đầu start một dự án mới đều phải đau đầu suy nghĩ về kiến trúc dự án sẽ áp dụng, mô hình sẽ xây dựng sao cho phù hợp nhất, hiệu quả nhất, sẵn sàng mở rộng và dễ dàng bảo trì. Tuy nhiên, việc xác định đúng đắn Structure sẽ sử dụng không hề đơn giản và đòi ...
Chào mọi người, chắc hẳn chúng ta khi bắt đầu start một dự án mới đều phải đau đầu suy nghĩ về kiến trúc dự án sẽ áp dụng, mô hình sẽ xây dựng sao cho phù hợp nhất, hiệu quả nhất, sẵn sàng mở rộng và dễ dàng bảo trì. Tuy nhiên, việc xác định đúng đắn Structure sẽ sử dụng không hề đơn giản và đòi hỏi kinh nghiệm cũng như một sự hiểu biết nhất định. Người ta đã xây dựng rất nhiều Structure Pattern khác nhau để phục vụ cho nhu cầu đó, và dĩ nhiên mỗi Pattern đều có ưu nhược điểm riêng biệt và cần áp dụng đúng đắn để đem lại hiệu quả. Bài viết hôm nay, tôi sẽ giới thiệu đến các bạn một Structure Pattern khá cũ nhưng chưa bao giờ lỗi thời - Model-View-ViewModel(MVVM) và áp dụng thực tế trong dự án Android mà không sử dụng đến thư viện DataBinding.
Chúng ta đều ít nhất một vài lần nghe đến chúng - Model-View-Presenter và Model-View-ViewModel. Chính xác chúng là những Structure Pattern cơ bản mà mọi lập trình viên nên biết và áp dụng.
MVP
Trong MVP, Presenter đóng vai trò là nơi xử lý logic và điều khiển View. Nói cách khác, mọi business của app sẽ được tách ra và xử lý riêng biệt ở layer này. Presenter sau khi nhận event từ View layer sẽ request Model layer và nhận notification từ nó và tiến hành update ngược trở lại lên View layer. Chúng ta đến với sơ đồ bên dưới.
Trong MVP, Presenter và View có quan hệ 1-1, tức là một Presenter chỉ tương tác với một View và ngược lại. Vì mọi logic đều xử lý trong Presenter dẫn đến nó có thể bị overload - một cách gọi khi quá nhiều thứ tập trung ở đây - và tất nhiên, bạn rất khó để kiểm soát nó, code sẽ trở nên rối và khó tiếp cận hơn. Để giải quyết vấn đề đó, chúng ta có thể tách việc xử lý logic nhỏ thành nhiều feature khác nhau bằng cách tạo một Service layer trung gian đóng vai trò connect giữa Presenter và Model, hoặc sẽ gộp các logic về data process tập trung theo Feature hoặc Model trước khi expose cho Presenter, và tôi nhận thấy Repository Pattern rất hiệu quả trong việc này.
MVVM
MVVM thực chất được xây dựng dựa trên MVP và cấu trúc cơ bản là tương đồng, tuy nhiên có vài điểm khác biệt. Nếu như MVP tập trung vào behavior của Presenter để interact với View thì MVVM lại khác, nó dựa vào notification từ ViewModel khi có thay đổi về data và update View, tất nhiên View sẽ đăng ký nhận notification từ ViewModel. Nói cách khác, ViewModel không có bất kỳ behavior nào để tương tác với View mà chính View sẽ lắng nghe sự thay đổi từ ViewModel. Bên cạnh đó, nếu như Presenter connect với View theo quan hệ 1-1 thì ViewModel với View lại là 1-n, tức là một ViewModel có thể connect với nhiều View.
Android đã hỗ trợ MVVM rất tốt bằng cách tạo ra thư viện DataBinding để developer apply nó dễ dàng và tường minh hơn. Tuy nhiên, bỏ qua sự tuyệt vời này, hôm nay tôi sẽ giới thiệu với các bạn một cách apply MVVM khác mà không dùng đến DataBinding library.
Chúng ta biết rằng, View lắng nghe sự thay đổi của luồng dữ liệu từ ViewModel và tiến hành update. Nhắc đến sự thay đổi của luồng dữ liệu, chúng ta sẽ liên tưởng ngay đến Observe Pattern - mọi người có thể tìm đọc bài viết trước của mình về pattern này tại đây. Và dĩ nhiên, việc apply nó ở đây không phải là một ngoại lệ. Một implementation rất nổi tiếng hiện nay dựa trên Observe Pattern là RxJava (thư viện hỗ trợ Reactive Programing) - các bạn có thể tìm đọc tại đây và đây. Chúng ta bắt đầu implement nó nhé.
- POJO class
public class User extends BaseModel { @StringDef({ Sex.FEMALE, Sex.MALE, Sex.OTHER }) @interface Sex { String MALE = "male"; String FEMALE = "female"; String OTHER = "other"; } @PrimaryKey private String id; private String name; private String email; private long dob; @Sex private String sex; // Define getter/setter }
- Local Data Source là cầu nối tương tác với Local Database. Chúng ta sẽ inject nó như một Singleton Object.
public class UserLocalDataSource extends LocalDataSource { @Inject public UserLocalDataSource(){ super(); } public Observable<User> getUser(@NonNull String userId){ // Get User from database } public Observable<User> persist(@NonNull User user){ // Persist user in database } public Observable<?> delete(@NonNull User user){ // Delete in database } public Observable<User> update(@NonNull User user){ // Update in database } public Observable<List<User>> persist(@NonNull List<User> users){ // Persist users in database } }
- Remote Data Source đóng vai trò cung cấp các thao tác connect API server. Khai báo như một Singleton Object
public class UserRemoteDataSource extends RemoteDataSource { @Inject public UserRemoteDataSource(){ super(); } public Observable<User> getUser(@NonNull String userId){ // Get User from server } public Observable<User> persist(@NonNull User user){ // Persist user in server } public Observable<?> delete(@NonNull User user){ // Delete in server } public Observable<User> update(@NonNull User user){ // Update in server } public Observable<List<User>> persist(@NonNull List<User> users){ // Persist users in server } }
- LiveData là đảm nhận vai trò tiếp nhận những thay đổi từ dữ liệu và tiến hành update lên View.
public class LiveData<T> { private T data; private List<ObserveListener<T>> listeners = new ArrayList<>(); public T get() { return data; } public void set(T data) { if (this.data != data) { this.data = data; notifyChange(); } } public void subscribe(@NonNull ObserveListener<T> listener) { if (listeners.contains(listener)) return; listeners.add(listener); } public void unSubscribe(){ listeners.clear(); } private void notifyChange() { if (listeners.isEmpty()) return; for (ObserveListener<T> listener : listeners) { listener.onChanged(data); } } public interface ObserveListener<T> { void onChanged(T data); } }
- Repository đảm nhiệm vai trò xử lý logic về mặt dữ liệu, mix các luồng dữ liệu và thực hiện logic trước khi expose cho client. Nó thực sự hiệu quả trong việc tách biệt các phần logic dữ liệu và logic xử lý View. Dễ nhận thấy ở ViewModel hoặc Presenter, bạn sẽ gọi nó mà chỉ quan tâm đến kết quả trả về và update lên View, còn việc xử lý logic data, bạn không cần biết đến.
public class UserRepository implements Repository { private UserRemoteDataSource remoteDataSource; private UserLocalDataSource localDataSource; @Inject public UserRepository(UserRemoteDataSource remoteDataSource, UserLocalDataSource localDataSource) { this.remoteDataSource = remoteDataSource; this.localDataSource = localDataSource; } /** * Get user from remote server then save in local database * * @param userId User Id * @return User object persisted in local DB */ public LiveData<User> getUser(@NonNull String userId) { final LiveData<User> liveData = new LiveData<>(); remoteDataSource.getUser(userId).flatMap(new Func1<User, Observable<User>>() { @Override public Observable<User> call(User user) { return localDataSource.persist(user); } }).subscribe(new Action1<User>() { @Override public void call(User user) { liveData.set(user); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { // TODO handle error } }); return liveData; } public LiveData<User> update(@NonNull User user) { // Do something to mix data between client and server } public LiveData<User> persist(@NonNull User user) { // Do something to mix data between client and server } public LiveData<?> delete(@NonNull final User user) { // Do something to mix data between client and server } public LiveData<List<User>> persist(@NonNull List<User> users) { // Do something to mix data between client and server } }
Dễ dàng nhận thấy việc mix giữa local data và remote data là vô cùng hiệu quả và lợi hại. Sau khi gọi method getUser() từ API server, tôi sẽ tiến hành lưu lại ngay dưới local database trước khi expose kết quả đến client (ViewModel). Chúng ta có thể tiến hành mix nhiều stream với nhau ở Repository để tạo thành một luồng dữ liệu đồng nhất và chứa mọi logic cần thiết trước khi expose nó về cho client.
Có một thao tác khác là cache dữ liệu trêm memory nếu bạn không muốn save lại nó vào database. Chúng ta sẽ thêm một biến :
private Map<String, LiveData<User>> cache = new HashMap<>();
sau đó tiến hành modify method getUser() như sau:
public LiveData<User> getUser(@NonNull final String userId) { if (cache.containsKey(userId)) { return cache.get(userId); } final LiveData<User> liveData = new LiveData<>(); remoteDataSource.getUser(userId).flatMap(new Func1<User, Observable<User>>() { @Override public Observable<User> call(User user) { return localDataSource.persist(user); } }).subscribe(new Action1<User>() { @Override public void call(User user) { liveData.set(user); cache.put(userId, liveData); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { // TODO handle error } }); return liveData; }
- ViewModel đóng vai trò là cầu nối giữa Model và View. ViewModel sẽ tương tác với Model và nhận notification, xử lý các logic liên quan đến View hay nhận các sự kiện từ View và gọi xử lý đến Model. Theo quan điểm của bản thân, tôi vẫn prefer ViewModel và Presenter chỉ đảm nhiệm việc xử lý logic liên quan đến View như validate dữ liệu truyền vào, tạo các wrapper data để hiển thị lên View, các business logic liên quan đến việc hiển thị lên View, và hoàn toàn không xử lý logic liên quan đến data.
public class UserViewModel extends ViewModel { private UserRepository userRepo; private LiveData<User> liveData; private String userId; UserViewModel(UserRepository userRepo) { this.userRepo = userRepo; } public void setUserId(String userId){ this.userId = userId; } public LiveData<User> getLiveData() { return liveData; } public void onStart(){ liveData = userRepo.getUser(userId); } public void onStop() { if (liveData != null) liveData.unSubscribe(); } }
- View - quá rõ ràng - là nơi xử lý các tương tác người dùng. Ở đây, tôi sẽ sử dụng Dagger 2 để inject ViewModel vào trong View và Butter Knife để ngắn gọn hơn trong các xử lý liên quan đến tương tác người dùng. Nếu các bạn muốn đọc hiểu thêm về Dagger - một thư viện được khuyến nghị để implement Dependency Injection trong Android thì mời đọc ở đây.
public class UserActivity extends AppCompatActivity { @Inject UserViewModel viewModel; @BindView(R.id.name) TextView textName; @BindView(R.id.email) TextView textEmail; // Define more component @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_user); ButterKnife.bind(this); viewModel.setUserId("user-id"); } @Override protected void onStart() { super.onStart(); viewModel.onStart(); viewModel.getLiveData().subscribe(new LiveData.ObserveListener<User>() { @Override public void onChanged(User user) { notifyUserChanged(user); } }); } @Override protected void onStop() { viewModel.onStop(); super.onStop(); } private void notifyUserChanged(@NonNull User user) { setTextName(user.getName()); setDob(user.getDob()); setEmail(user.getEmail()); setSex(user.getSex()); // Define more component listen on User change } private void setTextName(String name) { textName.setText(name); } private void setDob(long dob) { // Set DOB to TextView DOB } private void setSex(@User.Sex String sex) { // Set sex to TextView Sex } private void setEmail(String email) { textEmail.setText(email); } }
Cuối cùng, tư tưởng mà tôi muốn nhấn mạnh ở bài viết này chính là làm rõ về MVVM và cách thức implement của nó. Tất nhiên, nếu bạn muốn ngắn gọn và dễ hiểu hơn, DataBinding vẫn là một thư viện mạnh mẽ của Google hỗ trợ hoàn hảo cho việc triển khai MVVM.
Hy vọng những chia sẻ của tôi sẽ giúp các bạn có cái nhìn rõ nét hơn về Structure Pattern trong một dự án và cụ thể là MVVM pattern - một thứ cũ nhưng chưa bao giờ hết hot.
The error syntax or bugs maybe occurred because I coded by my imagination and have not run yet ... hehe