Android Architecture Patterns (phần 2): Model-View-Presenter
Áp dụng architecture patterns vào các ứng dụng Android chỉ là việc sớm muộn phải làm. Trên thực tế, Google cũng đã cung cấp Android Architecture Blueprints để hỗ trợ các dev.
Ngoài ra, bạn có thể tham khảo ví dụ MVP & RxJava mà Erik Hellman và chúng tôi đã cùng nhau làm việc tại ĐÂY.
Pattern Model-View-Presenter
Dưới đây là vai trò của mỗi component:
- Model — layer data. Chịu trách nhiệm chính trong việc xử lý business logic và tương tác với network và các layers database
- View — layer UI. Hiển thị dữ liệu ở trên và thông báo Presenter về các actions của user
- Presenter —lấy data từ Model, áp dụng UI logic và quản trị tình trạng của View, quyết định những gì cần hiển thị và react lại các notifications input của user từ View
Vì View và Presenter phối hợp gần gũi với nhau, chúng cần phải tham chiếu đến nhau. Để Presenter unit test được với JUnit, View phải được trừu tượng và 1 interface cho nó sử dụng. Mối quan hệ giữa Presenter và View tương ứng được define trong class interfaceContract
, nhờ đó có thể đọc được code và sự liên kết giữa Presenter và View cũng dễ hiểu hơn.
Model-View-Presenter Pattern & RxJava trong Android Architecture Blueprints
Mẫu blueprint là 1 ứng dụng ”To Do”, cho phép user tạo, đọc, update và xóa các nhiệm vụ “To Do”, cũng như áp dụng các filters vào danh sách tasks được hiển thị. RxJava được sử dụng để di chuyển khỏi thread chính và có thể xử lý các operations bất đồng bộ.
Model
Model làm việc với các nguồn data nội bộ và data ở xa để nhận và lưu data. Đây là nơi để xử lý business logic. Ví dụ, khi request danh sách Task
s, Model sẽ cố gắng lấy các task đó từ nguồn dữ liệu local. Nếu trống, nó sẽ truy vấn network, lưu phản hồi trong nguồn dữ liệu local, sau đó trả lại danh sách.
Nhờ có RxJava, chúng ta có thể thu hồi lại các tasks:
1
2
3
4
5
|
public Observable & lt ; List & lt ; Task & gt ; & gt ; < strong class = "markup--strong markup--pre-strong" > getTasks < / strong > ( ) {
. . .
}
|
Model nhận các tasks làm tham số trong constructor interfaces của các nguồn data local và data từ xa, nhờ đó Model có thể hoàn toàn độc lập với các Android classes bất kì và dễ unit test với JUnit. Ví dụ, để test getTasks
request data từ nguồn local, chúng ta sẽ thực hiện test như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@ Mock
private TasksDataSource mTasksRemoteDataSource ;
@ Mock
private TasksDataSource mTasksLocalDataSource ;
. . .
@ Test
public void < strong class = "markup--strong markup--pre-strong" > getTasks_requestsAllTasksFromLocalDataSource < / strong > ( ) {
< em class = "markup--em markup--pre-em" > // Given that the local data source has data available</em>
setTasksAvailable ( mTasksLocalDataSource , TASKS ) ;
< em class = "markup--em markup--pre-em" > // And the remote data source does not have any data available</em>
setTasksNotAvailable ( mTasksRemoteDataSource ) ;
< em class = "markup--em markup--pre-em" > // When tasks are requested from the tasks repository</em>
TestSubscriber & lt ; List & lt ; Task & gt ; & gt ; testSubscriber =
new TestSubscriber & lt ; & gt ; ( ) ;
mTasksRepository . getTasks ( ) . subscribe ( testSubscriber ) ;
|
1
2
3
4
5
6
|
< em class = "markup--em markup--pre-em" > // Then tasks are loaded from the local data source</em>
verify ( mTasksLocalDataSource ) . getTasks ( ) ;
testSubscriber . assertValue ( TASKS ) ;
}
|
View
View làm việc với Presenter để hiển thị data và notifies Presenter về các actions của user. Trong MVP Activities, Fragements và custom Android views có thể là Views. Lựa chọn của chúng tôi là sử dụng Fragments.
Tất cả Views implement cùng interface BaseView – interface cho phép setting 1 Presenter.
1
2
3
4
5
|
public interface < strong class = "markup--strong markup--pre-strong" > BaseView < / strong > & lt ; T & gt ; {
void setPresenter ( T presenter ) ;
}
|
View notifies Presenter rằng Views đã sẵn sàng update bằng cách gọi method subscribe
của Presenter trong onResume
. View gọi presenter.unsubscribe()
trong onPause
để nói Presenter rằng View không còn hứng thú với việc cập nhật nữa. Nếu implement của View là Android custom view thì các method subscribe
và unsubscribe
phải được gọi bằngonAttachedToWindow
vàonDetachedFromWindow
. Các actions của Users, như các clicks vào button sẽ kích hoạt các methods tương ứng trong Presenter – đây chính là yếu tố quyết định chuyện gì sẽ xảy ra tiếp theo.
Views được test với Espresso. Ví dụ, màn hình số liệu thống kê cần phải hiển thị số lượng tasks đã hoàn thành và số lượng tasks active. Test kiểm tra việc này đầu tiên sẽ đặt vài tasks vàoTaskRepository
; sai đó launch StatisticsActivity
và kiểm tra content của views:
1
2
3
4
5
6
7
|
@ Before
public void < strong class = "markup--strong markup--pre-strong" > setup < / strong > ( ) {
< em class = "markup--em markup--pre-em" > // Given some tasks
< / em > TasksRepository . destroyInstance ( ) ;
TasksRepository repository = Injection . provideTasksRepository ( InstrumentationRegistry . getContext ( ) ) ;
|
1
2
3
4
|
repository . saveTask ( new Task ( "Title1" , "" , false ) ) ;
repository . saveTask ( new Task ( "Title2" , "" , true ) ) ;
|
1
2
3
4
5
6
|
< em class = "markup--em markup--pre-em" > // Lazily start the Activity from the ActivityTestRule
< / em > Intent startIntent = new Intent ( ) ;
mStatisticsActivityTestRule . launchActivity ( startIntent ) ;
}
|
1
2
3
4
5
6
7
8
9
10
|
@ Test
public void < strong class = "markup--strong markup--pre-strong" > Tasks_ShowsNonEmptyMessage < / strong > ( ) throws Exception {
< em class = "markup--em markup--pre-em" > // Check that the active and completed tasks text is displayed
< / em > Context context = InstrumentationRegistry . getTargetContext ( ) ;
String expectedActiveTaskText = context
. getString ( R . string . statistics_active_tasks ) ;
String expectedCompletedTaskText = context
. getString ( R . string . statistics_completed_tasks ) ;
|
1
2
3
4
5
6
7
|
onView ( withText ( containsString ( expectedActiveTaskText ) ) )
. check ( matches ( isDisplayed ( ) ) ) ;
onView ( withText ( containsString ( expectedCompletedTaskText ) ) )
. check ( matches ( isDisplayed ( ) ) ) ;
}
|
Presenter
Presenter và View tương ứng được tạo ra bởi Activity. Các tham chiếu đến View và đến TaskRepository
– Model – được đưa đến cho constructor của Presenter. Trong quá trình thực thi của constructor, Presenter sẽ gọi method setPresenter
của View. Có thể đơn giản hóa cách thức này bằng cách sử dụng 1 framework dependency injection cho phép thêm inject Presenters trong các corresponding views, giảm việc coupling của các classes. Việc thực thi ToDo-MVP với Dagger được giải quyết trong ví dụ này.
Tất cả Presenters thực thi cùng interface BasePresenter.
1
2
3
4
5
6
|
public interface < strong class = "markup--strong markup--pre-strong" > BasePresenter < / strong > {
void subscribe ( ) ;
void unsubscribe ( ) ;
}
|
Khi gọi đến method subscribe
, Presenter sẽ bắt đầu request dữ liệu Model, sau đó áp dụng UI logic vào dữ liệu đã nhận được và set nó đến View. Ví dụ, trong StatisticsPresenter
, tất cả các task được request từ TaskRepository
– sau đó các tasks đã được lấy lại sẽ được dùng để compute số lượng các tasks đã hoàn thành và active. Những con số này sẽ được sử dụng như parameters cho method showStatistics(int numberOfActiveTasks, int numberOfCompletedTasks)
của View.
Rất dễ để thực thi 1 unit test để kiểm tra rằng method showStatistics
thực sự được gọi với các giá trị chính xác. Chúng tôi đang thử nghiệm TaskRepository
và StatisticsContract.View
và xem các objects đã được thử nghiệm như các parameters đến constructor của 1 object StatisticsPresenter
.
Quá trình thực thi như sau:
1
2
3
4
5
6
7
|
@ Test
public void < strong class = "markup--strong markup--pre-strong" > loadNonEmptyTasksFromRepository_CallViewToDisplay < / strong > ( ) {
< em class = "markup--em markup--pre-em" > // Given an initialized StatisticsPresenter with
< / em > < em class = "markup--em markup--pre-em" > // 1 active and 2 completed tasks
< / em > setTasksAvailable ( TASKS ) ;
|
1
2
3
4
|
< em class = "markup--em markup--pre-em" > // When loading of Tasks is requested
< / em > mStatisticsPresenter . subscribe ( ) ;
|
1
2
3
4
5
|
< em class = "markup--em markup--pre-em" > // Then the correct data is passed on to the view
< / em > verify ( mStatisticsView ) . showStatistics ( 1 , 2 ) ;
}
|
Vai trò của method unsubscribe
là làm rõ các subscriptions của Presenter, để tránh các lổ hổng về bộ nhớ.
Ngoài trừ subscribe
và unsubscribe
, mỗi Presenter hiển thị các methods khác, tương ứng với user actions trong View. Ví dụ, AddEditTaskPresenter
, thêm các methods như createTask
– có thể gọi được khi user nhấn button tạo 1 task mới. Việc này đảm bảo tất cả các user actions – và hệ quả là tất cả UI logic – đi qua Presenter và đều được unit test.
Khuyết điểm của Pattern Model-View-Presenter
Để giảm lượng interfaces được sử dụng, 1 vài dev đã loại bỏ interface classContract
và interface cho Presenter.
Một trong những điểm bất lợi của MVP sẽ xuất hiện khi áp dụng UI logic vào Presenter: Presenter hiện đã trở thành 1 class “cái gì cũng biết” với hàng ngàn dòng code. Để giải quyết vấn đề này, chia code ra và nhớ tạo các classes chỉ có 1 nhiệm vụ duy nhất và có thể unit test được.
Kết luận
Model-View-Controller pattern có 2 khuyết điểm chính: đầu tiên, View có 1 tham chiếu đến cả Controller và Model; thứ hai, nó không giới hạn áp dụng UI logic vào 1 single class, nhiệm vụ này đang được chia sẻ giữa Controller và View hoặc Model. Pattern Model-View-Presenter giải quyết cả 2 vấn đề này bằng cách phá vỡ sự liên kết giữa View và Model, chỉ tạo một class có thể xử lý mọi thứ liên quan đến sự hiện diện của View – Presenter: 1 single class rất dễ unit test.
Chuyện gì sẽ xảy ra nếu chúng ta có 1 event-based architecture, nơi mà View reacts các thay đổi? Hãy cập nhật các patterns tiếp theo được làm mẫu trong Android Architecture Blueprints để hiểu cách implementation hoặc đọc implementation của Model-View-ViewModel pattern trong ứng dụng upday.
Nguồn: Medium
- Microsoft thừa nhận tablet và mobile đang chiếm ưu thế hơn destop và laptop
- Cách xem số điện thoại
- Hướng dẫn quản trị Magento, admin Magento user guide
- Mobility nền tảng điện toán di động, Công nghệ cho tương lai
- Q3/2013: Android chiếm 81% vẫn đứng ở số 1
- Chọn Native app hay Web app ( mobile web )?
- Hướng dẫn viết module cho SugarCRM
- Làm sao để web lên Google, Bing và các bộ máy tìm kiếm khác nhanh?
- Đăng nhập không cần password, tại sao không?
- Android, iOS, BlackBerry hệ điều hành nào bảo mật hơn
- Mã nguồn website công ty phần mềm ERP
- Sổ tay Git cơ bản cần phải biết khi đi làm
DVMS chuyên:
- Tư vấn, xây dựng, chuyển giao công nghệ Blockchain, mạng xã hội,...
- Tư vấn ứng dụng cho smartphone và máy tính bảng, tư vấn ứng dụng vận tải thông minh, thực tế ảo, game mobile,...
- Tư vấn các hệ thống theo mô hình kinh tế chia sẻ như Uber, Grab, ứng dụng giúp việc,...
- Xây dựng các giải pháp quản lý vận tải, quản lý xe công vụ, quản lý xe doanh nghiệp, phần mềm và ứng dụng logistics, kho vận, vé xe điện tử,...
- Tư vấn và xây dựng mạng xã hội, tư vấn giải pháp CNTT cho doanh nghiệp, startup,...
Vì sao chọn DVMS?
- DVMS nắm vững nhiều công nghệ phần mềm, mạng và viễn thông. Như Payment gateway, SMS gateway, GIS, VOIP, iOS, Android, Blackberry, Windows Phone, cloud computing,…
- DVMS có kinh nghiệm triển khai các hệ thống trên các nền tảng điện toán đám mây nổi tiếng như Google, Amazon, Microsoft,…
- DVMS có kinh nghiệm thực tế tư vấn, xây dựng, triển khai, chuyển giao, gia công các giải pháp phần mềm cho khách hàng Việt Nam, USA, Singapore, Germany, France, các tập đoàn của nước ngoài tại Việt Nam,…
Quý khách xem Hồ sơ năng lực của DVMS tại đây >>
Quý khách gửi yêu cầu tư vấn và báo giá tại đây >>