Triển khai mô hình MVP cho lập trình ứng dụng Android

0
714

Khi bắt tay vào xây dụng một ứng dụng, chúng ta thường nghĩ ngay đến việc sẽ sử dụng mô hình phát triển nào để dễ maintenance nhất. Bạn đã từng nghe đến mô hình MVC cho web chưa? Android cũng có một mô hình tương tự gọi là mô hình MVP(Model View Presenter)

Bài viết này sẽ chia sẻ với các bạn những bài học, sai lầm cũng như những lý do đằng sau những thay đổi của mô hình phát triển ứng dụng Android.

Mô hình MVP khi lập trình ứng dụng Android

Câu chuyện về mô hình phát triển ứng dụng cho Android

Thủa hàn vi

Trở lại năm 2012,  ứng dụng của mình sử dụng mô hình rất cơ bản. Ứng dụng không sử dụng bất kể Networking library nào, còn AsyncTasks chỉ là một công cụ đắc lực mỗi khi cần xử lý tác vụ nặng. Đây là mô hình mà ứng dụng sử dụng lúc đó:
Mô hình MVP khi lập trình ứng dụng Android
Về cơ bản, ứng dụng chia làm 2 layers:

  • Data Layer : Chịu trách nhiệm xử lý database như lưu dữ liệu từ REST API, thao tác CRUD vào SQLite…
  • View Layer: sẽ dùng để cập nhật dữ liệu trên giao diện ứng dụng(UI)

Mình có sáng tạo một chút khi tạo riêng một class có tên là APIProvider. Tại Class này mình viết các methods để Activity và Fragment có thể dễ dàng tương tác với REST API hơn.

Các methods này chủ yếu là mình viết dựa trên các API từ URLConnection Class của Android kết hợp với AsyncTasks. Mục đích là để thực hiện các Network request từ một Thread riêng biệt với UI Thread. Và trả lại kết quả cho Activity thông qua các Callbacks. Quá đơn giản phải không?

Tương tự, CacheProvider cung cấp các methods để dễ dàng truy xuất vào SharePreferences và cơ sở dữ liệu SQLite. Nó cũng sử dụng callbacks để truyền kết quả cho Activity.

Vấn đề bắt đầu nảy sinh

Vấn đề của mô hình cổ điển này là View Layer sẽ phải thực hiện quá nhiều việc. Mà các bạn biết đấy, View Layer mà xử lý càng nhiều thì khả năng maintenance càng kém.

Để minh họa cho bài viết, mình xây dựng một ứng dụng đơn giản như sau:

  • Một ứng dụng tải danh sách các bài viết(Posts) từ một blog online thông qua REST API.
  • Sau đó lưu trữ vào cơ sở dữ liệu SQLite.
  • Cuối cùng là hiển thị các post lên ListView

Với mô hình hiện tại thì Activity sẽ phải thực hiện các bước như sau:

  1. Gọi loadPost(callback) trong APIProvider để tải các Posts
  2. Đợi tải xong thì gọi hàm savePosts(callback) trong CacheProvider
  3. Khi CacheProvider thực hiện thành công, chúng ta hiển thị Posts trên ListView
  4. Cuối cùng là xử lý Errors callback từ APIProviderCacheProvider

Đây chỉ là một ví dụ đơn giản. Chứ thực tế thì API REST sẽ không trả dữ liệu có thể hiển thị ngay được trên View Layer.  Do đó, Activity sẽ cần phải xử lý dữ liệu nhận được từ REST API trước khi có thể hiển thị được nó.

Ngoài ra, nếu hàm loadpost()có một tham số mà tham số này lại cần phải lấy dữ liệu từ đâu đó (kiểu như: cần lấy địa chỉ email từ Play Services SDK. Rất có thể SDK sẽ trả về một email bằng cách gọi hàm bất đồng bộ sử dụng callback). Như vậy chúng ta có 3 tầng callback lồng nhau

Nếu cứ tiếp tục các thực hiện như vậy rất có thể chúng ta sẽ gặp phải lỗi gọi là Callback Hell.

Tóm gon lại, vấn đề gặp phải là:

  • Activity và Fragment ngày càng trở nên cồng kềnh và khó maintenance
  • Quá nhiều Callback lồng nhau làm cho code trở nên rắc rối và khó hiểu. Sau này sẽ rất khó khăn cho việc thay đổi code hoặc thêm tính năng mới. Bạn biết câu chuyện chí phèo code chưa?(Nếu chưa tham khảo ở đây nhé )
  • Rất khó để viết Unit Testing nếu không muốn nói là không thể. Bởi vì có rất nhiều hàm logic được thực hiện trên Activity và Fragment

Xây dựng mô hình mới với RxJava

Mình đã sử dụng mô hình cố điển trên được khoảng 2 năm. Trong thời gian đó, mình cũng đã thực hiện một số cải tiến để giảm thiểu những nhược điểm của nó.

Một số cải tiến của mình như:

  • Thêm một số Helper class để giảm các code logic thực hiện trong Activity và Fragment
  • Sử dụng thư viện Volley trong APIProvider.

Mặc dù vậy, code vẫn rất khó khăn để thực hiện Unit test. Còn vấn đề callback lồng nhau thì vẫn chưa khắc phục được.

Cho đến năm 2014, mình đọc được một bài viết về RxJava. Sau khi cố gắng viết thử một vài sample project, mình nhận thấy đây chính là giải pháp để xử “vấn nạn” callback lồng nhau.

Theo như tài liệu mô tả: RxJava cho phép bạn quản lý dữ liệu thông qua các luồng bất đồng bộ (asynchronous streams). Nó cung cấp cho bạn rất nhiều cách thức để bạn có thể xử lý các stream như transform, lọc(filter) hay tổng hợp dữ liệu.

Sau rất nhiều kinh nghiệm “xương máu” trong quá khứ, mình luôn đau đáu suy nghĩ về một mô hình ứng dụng mới. Và rồi một ngày, mình bắt gặp một mô hình khá thú vị sử dụng RxJava như hình bên dưới

Mô hình MVP khi lập trình ứng dụng Android

#1. Mô hình với RxJava thực hiện như thế nào?

Cách tiếp cận mới này khá giống với kiểu cổ điển, mô hình này tách lớp quản lý dữ liệu (Data Layer) ra khỏi View Layer. Data Layer bao gồm: DataManagerHelpers class. Phần còn lại gồm: Activity, Fragment, ViewGroup … sẽ “biên chế” vào View layer.

Helper class có nhiệm vụ khá đặc biệt. Nó giúp đơn giản hóa việc tương tác với cơ sở dữ liệu hay Network… Trong hầu hết các dự án thì người ta hay xây dựng các Helper classes cho việc truy cập các REST API hay lớp trung gian để đọc ghi vào cơ sở dữ liệu (SQLite) hoặc tương tác với các 3rd party SDK.

Các ứng dụng Android khác nhau sẽ viết các Helper class khác nhau. Nhưng tựu chung lại sẽ có một số cách sử dụng thông dụng như:

  • PreferencesHelper : Đọc và lưu dữ liệu vào SharedPreferences
  • DatabaseHelper: Xử lý các truy cập (CRUD) vào SQLite
  • Retrofit services: Thực hiện gọi REST API. Thời điểm này thì mình sử dụng Retrofit thay vì Volley bởi vì nó hỗ trợ RxJava.

Hầu hết các method bên trong Helper class sẽ trả về là RxJava Observables.

DataManager là thành phần quan trọng nhất của mô hình này. Nó sử dụng các phương thức của RxJava để combine, filter và tranform dữ liệu được lấy từ các Helper classes. Mục đích của DataManager là giảm khối lượng công việc mà Activity, Fragment phải thực hiện bằng cách cung cấp dữ liệu “tinh” cho View Layer. View Layer chỉ việc hiển thị mà không phải xử lý gì cả.

Lúc này View Layer sẽ thực sự là view, tức là nó chỉ có duy nhất một nhiệm vụ nhận dữ liệu và hiển thị.

#2. Cách Implement mô hình với RxJava

Để mọi người có thể dễ hình dung về vai trò của DataManager , mình ví dụ một một tính năng trong ứng dụng minh họa ở trên:

Lấy tất cả các bài viết trên blog được đăng trong ngày hôm nay và hiển thị ra ListView

  • Đầu tiên, nó thực hiện gọi Retrofit service để lấy danh sách các bài viết từ server thông qua REST API
  • Lưu các bài viết vào trong database bằng cách sử dụng DatabaseHelper
  • Lọc các bài viết được đăng trong hôm nay
public Observable<Post> loadTodayPosts() {
        return mRetrofitService.loadPosts()
                .concatMap(new Func1<List<Post>, Observable<Post>>() {
                    @Override
                    public Observable<Post> call(List<Post> apiPosts) {
                        return mDatabaseHelper.savePosts(apiPosts);
                    }
                })
                .filter(new Func1<Post, Boolean>() {
                    @Override
                    public Boolean call(Post post) {
                        return isToday(post.date);
                    }
                });
}

Các thành phần trong View Layer như Activity/Fragment sẽ chỉ đơn giản là gọi hàm loadTodayPosts() rồi đăng ký với Observable. Sau khi đăng kí thành công, các bài viết khác sẽ được tự động thêm vào Adapter để hiện thị ra ListView mà không cần phải đăng kí lại.

Có một thành phần khá đặc biệt trong mô hình này là Event Bus. Event Bus có tác dụng như một “tổng đài” thông báo các sự kiện xảy ra trong Data Layer. Vì vậy, các components trong View Layer có thể đăng kí để theo dõi và nhận các thông tin từ Data Layer và cập nhật lên giao diện.

Ví dụ: Hàm signOut()trong DataManager sẽ đẩy một sự kiện (event) khi đăng xuất ứng dụng thành công. Lúc này các Activities đã đăng kí sự kiện “signOut” trước đó có thể biết được ứng dụng đã ở trạng thái Sign out và chuyển giao diện sang trạng thái Sign out.

#3. Tại sao mô hình với RxJava lại tốt hơn?

  • Ưu điểm lớn nhất của mô hình này so với với mô hình cổ điển đó là RxJava Observales cho phép chúng ta tránh phải gọi callback lồng nhau
    Tại sao mô hình MVP tốt hơn
  • DataManager Layer sẽ thực hiện toàn bộ các tác vụ liên quan đến quản lý Database (Điều mà trước kia thực hiện ở View Layer). Điều này sẽ làm cho code của Activity và Fragment gọn nhẹ hơn rất nhiều.
  • Việc chuyển phần code thao tác với database từ Activity, Fragment sang DataManager sẽ giúp cho việc viết Unit Test dễ dàng hơn => tăng khả năng maintaince dự án

#4. Nhược điểm của mô hình với RxJava là gì?

  • Với các dự án lớn và phức tạp thì DataManager sẽ trở nên cồng kềnh và khó bảo trì. Theo ý kiến cá nhân của mình thì không biết nên coi đây là nhược điểm hay thách thức. Vì coder lành nghề, người ta không bao giờ vứt tất cả code vào một Class. Nếu mô hình không có định nghĩa rõ ràng thì người ta cũng sẽ cố gắng tách thành nhiều class theo chức năng
  • Mặc dù Activity/Fragment đã tinh giảm nhưng chúng vẫn phải xử lý logic về quản lý đăng kí RxJava, xử lý lỗi…Điều này vẫn chưa thực sự là cái đích cuối cùng của mình. Mình muốn View Layer thực sự chỉ có lấy dữ liệu, detect sự kiện từ người dùng và hiển thị dữ liệu mà không thực hiện bất kì hàm logic nào cả.

Mô hình MVP(Model View Presenter)

#1. Mô hình MVP là gì?

Trong những năm gần đây, có một số mô hình như MVP hay MVVM rất phổ biến trong cộng đồng Android. Sau khi tìm hiểu và trải nghiệm một số dự án thì mình thấy MVP thực sự là một mô hình tốt. Mô hình MVP mang những cải tiến giá trị có thể giải quyết được vấn đề mà mình đã gặp phải ở trên.

Bởi vì mô hình hiện tại của mình đang chia thành 2 layer (View Layer và Data Layer) nên việc trển khải MVP sẽ dễ dàng hơn nhiều so với mô hình MVVM
Kien truc MVP

#2. Cách triển khai mô hình MVP trong Android

Data Layer

Data Layer vẫn như cũ, chỉ có điều bây giờ nó gọi là Model Layer.

Presenters Layer

Presenters có 2 nhiệm vụ tách bạch:

  • Tải dữ liệu từ Model Layer (Database)
  • Đẩy dữ liệu lên View Layer sau khi đã xử lý xong.

Presenter sẽ là một layer trung gian giữa Model và View layer. Nó giống như một cái máy làm xúc xích vậy. Tức là bạn đưa con lợn vào một đầu, đầu kia sẽ lòi ra xúc xích. Nếu cần có thể là xúc xích đã nướng nóng thơm phức.

Tóm lại, toàn bộ việc xử lý logic, xử lý lỗi sẽ được thực hiện ở Presenter Layer.

Ví dụ: Chúng ta cần phải lọc dữ liệu được trả về từ Model layer. Và nếu bộ lọc này không tái sử dụng ở bất kì đâu trong dự án thì việc code bộ lọc sẽ được thực hiện ở Presenter Layer chứ không phải Model Layer.

Dưới đây là đoạn code mẫu được viết trong Presenter Layer. Đoạn code có mục đích là đăng kí Observable được trả về bởi dataManager.loadTodayPosts()mà chúng ta đã định nghĩa ở phần trên của bài viết

public void loadTodayPosts() {
    mMvpView.showProgressIndicator(true);
    mSubscription = mDataManager.loadTodayPosts().toList()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe(new Subscriber<List<Post>>() {
                @Override
                public void onCompleted() {
                    mMvpView.showProgressIndicator(false);
                }

                @Override
                public void onError(Throwable e) {
                    mMvpView.showProgressIndicator(false);
                    mMvpView.showError();
                }

                @Override
                public void onNext(List<Post> postsList) {
                    mMvpView.showPosts(postsList);
                }
            });
}

mMvpView là một instance của một Activity, Fragment hay ViewGroup. Nó có tác dụng để Presenter cập nhập dữ liệu lên View Layer.

View Layer

Cũng giống như mô hình trước, View Layer chứa các view component của android như: ViewGroups, Fragment hay Activities. Sự khác biệt duy nhất ở đây là các components này không có đăng kí trực tiếp với các Observabes.

Thay vào đó, View Layer sẽ implement các interfaces để cập nhật dữ liệu lên giao diện. View Layer thường có các interfaces như: showError(), showProgressindicator()… cho Presenters Layer sử dụng.

Các View components tương tác với người dùng như detect sự kiện. Sau đó truyền xuống Presenter Layer để tiếp tục xử lý logic.

Ví dụ: Chúng ta có một Button và khi click vào button này thì sẽ tải và hiển thị một danh sách các bài viết. Activity sẽ chỉ đơn giản gọi hàm presenter.loadTodayPosts()từ onClick

Bạn có thể download full source code của project mà mình đang minh họa tại đây

#3. Ưu điểm của mô hình MVP

  • Code phần Activities, Fragment sẽ cực kì nhẹ nhàng. Chúng chỉ đơn giản là dectect sự kiện từ người dùng sau đó gửi dữ liệu cho Presenter Layer. Cuối cùng là hiển thị dữ liệu ra cho người dùng.
  • Với mô hình MVP, việc viết Unit Tests sẽ đơn giản hơn rất nhiều. Bởi vì giờ đây Presenter và Model layer sẽ thuần là code java xử lý logic.
  • Nếu Data Manager trở lên cồng kềnh thì chúng ta có xử lý vấn đề này bằng cách đẩy một phần code sang Presenter Layer. Tuy nhiên, theo quan điểm cá nhân mình thì cách này chỉ là cheat thôi.

Kết luận

Điều quan trọng mình muốn đề cập trong bài viết này không phải là tìm kiếm một mô hình hoàn hảo có thể đáp ứng tất cả các yêu cầu. Mà đơn giản chỉ là đưa ra một số mô hình với ưu và nhược điểm. Và mô hình đó có thể giải quyết được một số bài toán thực tế mà bạn đang gặp phải khi xây dựng ứng dụng cho Android.

Hệ sinh thái Android vẫn sẽ tiếp tục phát triển với tốc độ nhanh chóng mặt. Do đó, chúng ta phải liên tục học hỏi, khám phá để tìm thấy những các tiếp cận tốt hơn.

Mình hi vọng bạn sẽ thích bài viết này. Nếu có đừng có quên chia sẻ bải viết và comment bên dưới ủng hộ mình nhé. Hẹn gặp lại ở bài viết sau.

Bình luận. Đặt câu hỏi cũng là một cách học

avatar
  Theo dõi bình luận  
Thông báo