Cùng tìm hiểu về RxJava (Phần 1)

Reactive programming là gì?

Reactive programming là lập trình các luồng dữ liệu không đồng bộ.

1. Tại sao dùng Rx?

Chúng ta cùng xem xét các trường hợp:

Người dùng mong muốn dữ liệu luôn được cập nhật liên tục (realtime). Họ muốn đơn hàng được xác nhận ngay lập tức. Họ cần thông tin về giá cả của sản phẩm phải luôn được cập nhật chính xác ngay tại thời điểm mà họ xem món hàng. Các trò chơi trực tuyến của họ cần phải được phản hồi ngay lập tức. Là một developer, bạn muốn tin nhắn tự tìm đến mục tiêu của nó khi được gửi đi. Bạn không muốn bị hoãn lại vì phải chờ đợi kết quả. Bạn muốn kết quả được gửi đến bạn ngay khi nó đã sẵn sàng. Hơn thế nữa, khi làm việc với các tập kết quả, bạn muốn nhận từng kết quả riêng biệt ngay khi chúng sẵn sàng. Bạn không muốn chờ đợi xử lý hết toàn bộ tập kết quả trước khi nhìn thấy chúng. Các developer có tool để gửi dữ liệu (cái này thì easy). Hơn thế nữa, các developer cần tool để kiểm soát phản ứng với việc gửi dữ liệu.

Rx có nhiều lợi ích sau:

  • Unitive: các truy vấn trong Rx được thực hiện giống như các thư viện khác lấy cảm hứng từ functional programming, chẳng hạn như các Java stream. Trong Rx, ta có thể sử dụng các phép biến đổi kiểu chức năng trên các luồng sự kiện.
  • Extensible: RxJava có thể được mở rộng bằng các operator tùy biến.
  • Declarative: Chuyển đổi các chức năng thì có thể được đọc bằng cách khai báo.
  • Composable: Các Rx operator có thể được kết hợp để tạo ra các operator phức tạp hơn.
  • Transformative: Các Rx operator có thể chuyển hóa một kiểu dữ liệu sang kiểu khác, reduce, map hoặc mở expand các luồng khi cần thiết.

Khi nào thích hợp khi sử dụng Rx:

Rx phù hợp với việc compose và consume các chuỗi của các sự kiện(event sequence).

Nên dùng Rx:

  • Các UI event như di chuyển chuột, click chuột.
  • Các Domain event như thay đổi thuộc tính, tập hợp được cập nhật, “Đơn hàng được điền xong”, “Việc đăng ký hoàn tất”,…
  • Các Infrastructure event như theo dõi file, các sự kiện hệ thống và WMI.
  • Các Integration event như broadcast từ message bus hoặc gửi sự kiện từ WebSockets API hoặc low latency middleware như Nirvana.
  • Tích hợp với CEP engine như StreamInsight hoặc StreamBase.

Có thể dùng Rx:

  • Kết quả của việc sử dụng Future pattern hoặc các pattern tương tự.

Không nên dùng Rx:

  • Translate các iterable sang các observable chỉ vì lợi ích khi làm việc đó thông qua một thư viện Rx.

Điều hay nhất là bạn được cung cấp một hộp công cụ tuyệt vời của các hàm để kết hợp, tạo và lọc bất kỳ luồng nào trong số các luồng đó.

2. Các Key type:

Rx dựa trên hai loại cơ bản, một số loại khác được mở rộng chức năng dựa trên các loại cốt lõi. Hai loại cốt lõi là Observable và Observer.

Rx xây dựng dựa trên Observer pattern. Event handling đã tồn tại trong Java (EventHandler của JavaFX). Đó là những cách tiếp cận đơn giản, nhưng sẽ rất “thốn” nếu không dùng Rx để thực hiện:

  • Các event thông qua các event handler rất khó để làm.
  • Chúng không thể truy vấn theo thời gian.
  • Chúng có thể là nguyên nhân gây ra memory leaks.
  • Đây không phải là cách tiêu chuẩn để thực hiện việc hoàn thành các tín hiệu.
  • Yêu cầu xử lý một cách thủ công đồng thời và đa luồng.

2.1. Observable:

Observable là loại cốt lõi đầu tiên chúng ta đề cập tới. Class này bao gồm rất nhiều triển khai của Rx, bao gồm tất cả các operator cốt lõi. Chúng ta sẽ tìm hiểu về nó từng bước một trong series bài viết này của mình. Hiện tại, chúng ta phải hiểu phương thức subscribe. Đây là một phương thức overload:

Đây là phương thức mà bạn dùng để nhận các value được phát ra bởi observable. Khi các value được gửi đi, chúng được gửi đến các subscriber mà sau đó chịu trách nhiệm về hành vi mà consumer đã dự định. Subscriber ở đây là một triển khai của một Observer interface.

Một Oservable phát ra 3 loại event là:

  • Các value.
  • Completion mà biểu thị rằng sẽ không có các value sẽ được phát ra thêm nữa.
  • Các Error, nếu có gì đó khiến chuỗi thất bại. Những sự kiện này cũng có nghĩa là chuỗi bị chấm dứt gián đoạn.

2.2. Observer:

Chúng ta vừa xem qua một abstract implementation của Observer là SubscriberSubscribertriển khai một số các chức năng bổ sung và nên được sử dụng để làm cơ sở cho việc triển khai Observer. Bây giờ, hãy cùng hiểu interface Observer nhé:

Ba phương thức trên là các hành vi được thực hiện mỗi lần observable phát đi một value. Observer sẽ có phương thức onNext của nó được gọi 0 hoặc nhiều lần, tùy chọn tiếp theo sẽ là một onCompleted hoặc một onError. Sẽ không có cuộc gọi nào nữa nếu có cuộc gọi đến onCompleted hoặc onError.

Khi phát triển code Rx, bạn sẽ gặp rất nhiều Observable, nhưng sẽ không Observer nhiều lắm. Điều quan trọng là hiểu Observer, có nhiều cách ngắn gọn để loại bỏ những thứ cần thiết khi khởi bạn tự khởi tạo nó.

2.3. Triển khai Observable và Observer:

Bạn có thể triển khai Observer một cách thủ công hoặc mở rộng Observable. Thực tế thì thường là sẽ không cần thiết vì Rx đã cung cấp tất cả các building block mà bạn cần. Nó cũng nguy hiểm, như sự tương tác giữa các phần của Rx bao gồm các convention và internal blumping khá khó đối với beginner. Điều này cũng đơn giản và an toàn hơn khi sử dụng nhiều tool mà Rx đem đến cho bạn để tạo ra chức năng mà bạn cần.

Để subscribe tới một oservable, không cần thiết phải cung cấp các instance của Observer. Có các overload để subscribe mà đơn giản lấy các chức năng được thực thi onNextonError và onSubscribe, che giấu sự khởi tạo Observer tương ứng. Cũng không cần thiết phải cung cấp một trong các chức năng đó. Bạn có thể cung cấp một tập con của chúng (vd: chỉ cần onNextonError và onSubscribe).

2.4. Subject:

Các subject là một sự mở rộng của Observable mà cũng triển khai interface Observer. Nghe có vẻ hơi lạ nhưng chúng khiến mọi việc đơn giản hơn trong một số trường hợp. Chúng có thể có các event được gửi đến chúng (giống như các observer), sau đó gửi đến các subscriber của chúng (giống như các observer). Điều này khiến chúng trở thành các entry point lý tưởng vào bên trong code Rx: khi bạn có các value đến từ bên ngoài Rx, bạn có thể gửi chúng vào bên trong một Subject, chuyển chúng thành observable. Bạn có thể nghĩ chúng là các entry point đến một Rx pipeline.

Subject có 2 parameter type: input type và output type. Điều này được thiết kế để dành cho sự trừu tượng hóa và không phải vì việc sử dụng phổ biến cho các subject liên quan đển các value được chuyển đổi. Sẽ có các operator chuyển đổi để làm điều đó, chúng ta sẽ xem sau.

Có một vài cách triển khai khác nhau của Subject. Chúng ta sẽ cùng thử một ví dụ quan trọng nhất.

2.4.1. PublishSubject:

PublishSubject là một loại subject rõ ràng nhất. Khi một value được gửi vào bên trong một PublishSubject, subject sẽ gửi nó đến tất cả các subscriber mà đã subsribe đến nó tại thời điểm đó.

Output:

Trong ví dụ trên, chúng ta có thể thấy rằng 1 không được in ra bởi vì chúng ta chưa subscribe khi nó được gửi đi. Sau khi chúng ta subscribe, chúng ta bắt đầu nhận các value được gửi đến subject.

2.4.2. ReplaySubject:

ReplaySubject có một tính năng đặc biệt là cache tất cả các value được gửi đến nó. Khi có một subscription mới được tạo ra, chuỗi event được phát lại từ lúc bắt đầu cho subscriber mới. Sau khi hoàn thành, tất cả subscriber nhận các event mới như bình thường.

Output:

Tất cả các giá trị đều được nhận bởi các subscriber, kể cả subscribe muộn. Cũng lưu ý rằng subscriber muộn được phát lại tất cả mọi thứ trước khi tiến hành value tiếp theo.

Cache mọi thứ nhiều khi cũng không tốt, vì một chuỗi observable có thể chạy trong thời gian dài. Có cách để giới hạn kích thước của internal buffer. ReplaySubject.createWithSize giới hạn kích thước của buffer, trong khi ReplaySubject.createWithTime giới hạn thời gian mà một tượng có thể được cache.

Output:

Subscriber muộn bây giờ sẽ bị lỡ value đầu tiên vì không còn ở trong cache (giới hạn cache là 2). Tương tự, giá trị cũ bị bỏ ra khỏi bộ nhớ đệm khi qua một thời gian được chỉ định, khi một subject được tạo bằng createWithTime.

Output:

Tạo một ReplaySubject với time sẽ yêu cầu một Scheduler (cách mà Rx giữ time). Hiện tại chúng ta có thể bỏ qua chúng, mình sẽ đề cập tới các scheduler ở phần sau.

ReplaySubject.createWithTimeAndSize giới hạn cả 2 (1 trong 2 đạt điều kiện).

2.4.3. BehaviorSubject:

 

BehaviorSubject chỉ ghi nhớ value cuối cùng. Tương tự với ReplaySubject kết hợp với kích thước buffer là 1. Value khởi tạo có thể được cung cấp khi tạo ra, vì thế hãy đảm bảo rằng đó là một giá trị sẽ có sẵn ngay lập tức khi subscribe.

Output:

đây là VD khi là event cuối cùng:

Value khởi tạo được cung cấp để có sẵn nếu mọi người subscribe trước khi value đầu tiên được gửi đến.

Output:

Vì vai trò của một BehaviorSubject là luôn luôn có một value có sẵn, nó thường ko được tạo mà không có value khởi tạo. Nó thường không chấm dứt một BehaviorSubject.

2.3.4. AsyncSubject:

 

AsyncSubject cũng cache value cuối cùng. Sự khác nhau là nó không phát bất kỳ value nào đến khi chuỗi hoàn tất. Nó dùng để phát một value đơn và complete ngay lập tức.

Output:

Lưu ý rằng, nếu chúng ta không thực hiện s.onCompleted(); Ví dụ này sẽ không in ra gì cả.

2.5. Các nguyên tắc ngầm định:

Có các nguyên tắc trong Rx không hiển nhiên ở trong code. Ví dụ cần biết là một điều quan trọng rằng không có các event được phát ra sau termination event (onError hoặc onComplete). Các subject được triển khai tôn trọng điều đó và phương thức subscribe cũng ngăn chặn một số vi phạm nguyên tắc:

Output:

Những mạng lưới an toàn giống như vậy đều không được đảm bảo trong toàn bộ triển khai của Rx. Quan trọng nhất là bạn ghi nhớ để không vi phạm nguyên tắc này, nếu không có thể dẫn đến những hành vi khó xác định.

3. Quản lý vòng đời:

Ý tưởng đằng sau Rx là không biết khi nào một chuỗi phát ra các value hoặc kết thúc, nhưng bạn vẫn có thể kiểm soát khi nào bạn có thể bắt đầu và dừng nhận các values. Các subscription có thể được liên kết với các tài nguyên được phân bổ mà bạn muốn giải phóng khi kết thúc thúc chuỗi. Rx cung cấp kiểm soát các subscription của bạn để cho phép bạn làm điều đó.

3.1. Subscribing:

Có một số overload với Observable.subscribe mà cùng viết tắt cho cùng một điều.

subscribe() consume các event nhưng không thực hiện các hành động. Các overload mà nhận một hoặc nhiều hơn Action sẽ xây dựng một Subscriber với các chức năng mà bạn cung cấp. Nếu bạn không thêm một action, event sẽ bị bỏ qua. VD sau sẽ xử lý error khi chuỗi thất bại:

Output:

3.2. Unsubscribing:

Bạn cũng có thể ngừng nhận các value trước khi chuỗi kết thúc. Mọi subscribe overload trả về một instance của Subscription (là một interface có 2 phương thức):

Output:

Unsubscribing một observer sẽ không gây trở ngại tới các observer khác trong cùng một observable

Output:

3.3. onError và onCompleted:

onError và onCompleted nghĩa là sự kết thúc của chuỗi. Một observable mà tuân thủ Rx contract sẽ không phát ra bất kỳ thứ gì sau khi một trong 2 event trên xảy ra. Điều này nên ghi nhớ khi sử dụng Rx và khi triển khai các observable theo cách của bạn.

Output:

3.4. Giải phóng các tài nguyên:

Một Subscription được gắn với các tài nguyên mà nó dùng. Vì lí do này, bạn nên ghi nhớ rằng hãy luôn hủy subscription. Bạn có thể tạo binding giữa một Subscription và các tài nguyên cần thiết bằng cách sử dụng Subscriptions factory.

Output:

Subscriptions.create nhận một action mà sẽ được thực thi trên unsubscription để giải phóng các tài nguyên. Chúng cũng là các ngắn gọn cho các action hay gặp khi tạo ra một chuỗi.

Subscriptions.empty() trả về một Subscription mà không làm gì cả khi được bỏ đăng ký. Hữu dụng khi bạn được yêu cầu trả về một instance của Subscription, nhưng triển khai của bạn không thực sự cẩn giải phóng bất kỳ các tài nguyên nào Subscriptions.from(Subscription... subscriptions) trả về một Subscription khi nó được bỏ đăng ký.

Subscriptions.unsubscribed() trả về một Subscription mà đã được bỏ đăng ký. Có một vài triển khai của Subscription:

  • BooleanSubscription
  • CompositeSubscription
  • MultipleAssignmentSubscription
  • RefCountSubscription
  • SafeSubscriber
  • Scheduler.Worker
  • SerializedSubscriber
  • SerialSubscription
  • Subscriber
  • TestSubscriber

Phần tiếp theo mình sẽ nói rõ hơn về chúng. Cũng lưu ý rằng Subscriber cũng triển khai Subscription. Điều này có nghĩa là chúng ta cũng có thể sử dụng một tham chiếu tới Subscriber để chấm dứt một subscription.

Mình hi vọng bạn thích bài viết này. Mình sẽ cố gắng viết phần tiếp theo sớm nhất có thể 😄

Tác giả: Nguyễn Anh Thiện (Mike Nguyen)

Một số lời khuyên về mô hình Model-View-Presenter trong Android

Có rất nhiều bài viết và ví dụ nói về cấu trúc MVP và có rất nhiều các cách để triển khai mô hình MVP khác nhau. Có một sự nỗ lực không ngừng bởi cộng đồng các dev để áp dụng mô hình này vào ứng dụng Android một cách tốt nhất có thể.

Nếu bạn quyết định áp dụng mô hình này, bạn phải hiểu rằng bạn đang lựa chọn một kiến trúc và phải hiểu rằng codebase, cách tiếp cận các tính năng mới của bạn sẽ thay đổi (để code tốt hơn). Bạn cũng phải biết rằng bạn sẽ phải đối mặt với một số vấn đề trong Android như vòng đời của Activity và bạn có thể tự hỏi bản thân các câu hỏi như sau:

  • Mình có nên save state của presenter?
  • Mình có nên lưu trữ dữ liệu ở presenter?
  • Presenter có nên có vòng đời không?

Ở bài viết này, mình sẽ tổng hợp một số hướng dẫn và cách hay nhất để:

  • Giải quyết các vấn đề hay gặp nhất khi sử dụng mô hình này.
  • Tối đa hoá các lợi ích khi sử dụng mô hình này.

Đầu tiên, hãy xem qua mô hình này:

Model:

Là một interface chịu trách nhiệm quản lý dữ liệu. Trách nhiệm của Model bao gồm sử dụng APIs, cache dữ liệu, quản lý databases và tương tự như vậy. Model cũng có thể là một interface để giao tiếp với các module khác cũng chịu trách nhiệm về quản lý dữ liệu. Ví dụ, nếu bạn đang sử dụng Repository Pattern thì model có thể là Repository.

Presenter:

Presenter là middle-man (lớp trung gian) giữa model và view. Tất cả  logic của bạn đều thuộc về nó. Presenter chịu trách nhiệm truy vấn model và cập nhật view, phản ứng với tương tác của người dùng khi cập nhật model.

View:

Nó chỉ chịu trách nhiệm biểu thị dữ liệu bằng một cách được quyết định bởi Presenter. View có thể được thực hiện bởi Activities, Fragments và bất kỳ Android widget nào hoặc bất kỳ thành phần nào có thể thực hiện các hoạt động như hiển thị ProgressBar, cập nhật TextView và tương tự.

1. Làm cho View bị động:

Một trong những vấn đề lớn nhất của Android là các view (Activities, Fragments,…) là đều không dễ test vì sự phức tạp của Android framework. Để giải quyết vấn đề này, bạn nên thực thi mô hình View bị động. Việc triển khai mô hình này giảm thiểu tối đa các xử lý logic của view bằng cách sử dụng Presenter. Cách này làm tăng khả năng test một cách đáng kể.

Ví dụ, nếu bạn có form username / password và một nút “submit” thì bạn ko viết validation logic ở bên trong view mà nên viết ở Presenter. View của bạn chỉ nên lấy username và password của form và gửi chúng đến để xử lý ở Presenter.

2. Làm cho Presenter độc lập với framework:

Để cho nguyên tắc trước thực sự hiệu quả (tăng khả năng test), hãy đảm bảo rằng Presenter không phụ thuộc vào các class của Android. Chỉ viết Presenter với các Java dependencies bởi 2 lí do: đầu tiên bạn trừu tượng hoá Presenter lên từ chi tiết của việc triển khai (Android framework) và kết quả là bạn có thể viết test cho Presenter dễ hơn, chạy test nhanh hơn ở JVM local của bạn mà không cần một emulator.

Nếu mình cần tới một Context thì sao?

Câu trả lời là gạt nó đi. Trong trường hợp này, bạn nên tự hỏi bản thân mình tại sao cần Context. Ví dụ, bạn có thể sử dụng Context để truy cập shared preferences hoặc resources. Nhưng bạn ko nên làm điều đó trong Presenter: bạn nên truy cập vào resources trong view và truy cập vào preferences trong Model. Đây chỉ là ví dụ đơn giản nhưng hầu hết trong các trường hợp thì nó chỉ là vấn đề của việc làm sai trách nhiệm trong MVP.

3. Viết Contract mô tả tương tác giữa View và Presenter:

Khi bạn bắt đầu viết một tính năng mới, sẽ là một thói quen tốt khi viết Contract đầu tiên. Contract mô tả sự giao tiếp giữa view và Presenter, nó giúp bạn thiết kế sự tương tác một cách sạch hơn.
Ưu tiên sử dụng giải pháp được đề xuất bởi Google trong Android Architecture: nó bao gồm một interface với 2 inner interfaces, một cho View và một cho Presenter:

Chỉ nêu tên các method mà bạn có thể hiểu use-case mà contract này đang mô tả.
Như trong ví dụ trên, các View method đều rất đơn giản để nhận biết rằng chúng không có bất kỳ logic nào ngoại trừ UI.

The View Contract

View được thực thi bởi một Activity (hoặc một Fragment). Presenter phải phụ thuộc vào View interface và không trực tiếp trên Activity: bằng cách này, bạn tách rời Presenter khỏi việc triển khai View.
Chúng ta có thể sửa đổi View mà không cần thay đổi code ở Presenter. Hơn thế nữa, chúng ta có thể dễ dàng thực hiện unit-test cho Presenter bằng cách tạo mock View.

The Presenter Contract

Chúng ta có thực sự cần Presenter interface không?

Thực sự là Không, nhưng mình sẽ nói Có.

Có hai kiểu suy nghĩ khác nhau về chủ đề này.

Một số người nghĩ bạn nên viết Presenter interface bởi vì bạn đang tách phần view khỏi phần Presenter.

Tuy nhiên, một số dev nghĩ rằng bạn đang trừu tượng hoá cái gì đó mà đã là một sự trừu tượng (của View) và bạn không cần phải viết một interface. Hơn thế nữa, bạn sẽ không bao giờ viết một Presenter thay thế, vì thế nó sẽ tốn thời gian code.

Dù nghĩ theo hướng nào thì việc có một interface có thể giúp bạn viết một mock của Presenter, nhưng nếu bạn sử dụng các tools như Mockito thì bạn không cần bất kỳ interface nào.

Về cá nhân, mình thích viết Presenter interface hơn vì 2 lí do đơn giản (ngoài các lí do đã được liệt kê trước đó):

1. Mình không viết viết một interface cho Presenter. Mình viết Contract để mô tả sự tương tác giữa View và Presenter.

2. Nó không tốn nhiều công sức để viết.

4. Định nghĩa một nguyên tắc đặt tên để tách riêng biệt các trách nhiệm:

Presenter có thể có 2 thể loại method:

  • Các Action (như là method load()): chúng mô tả presenter để làm gì.
  • Các User event (như là method queryChanged(...)): chúng mô tả các hành động được kích hoạt bởi user như “viết vào search view” hoặc “click vào một item”.

Càng nhiều các action thì càng nhiều logic bên trong View. Thay vào đó các user event gợi ý rằng chúng mặc cho Presenter quyết định phải làm gì. Ví dụ cụ thể, một search chỉ có thể được triển khai khi có ít nhất một lượng ký tự có độ dài nhất định được nhập vào bởi user. Trong trường hợp này, View chỉ gọi method queryChanged(...) và Presenter sẽ quyết định khi nào triển khai search.

Thay vào đó, method loadMore() được gọi khi user cuộn đến cuối danh sách, sau đó Presenter load trang kết quả khác. Nghĩa là khi user cuộn đến cuối thì View sẽ biết rằng trang mới cần được tải thêm. Để “reverse” logic này, có thể đặt tên method là onScrolledToEnd() để phần Presenter quyết định phải làm gì.

Nghĩa là trong giai đoạn thiết kế “Contract”, bạn phải quyết định mỗi user event và action tương ứng nó và logic nên thuộc về phần nào.

5. Đừng tạo các Activity-lifecycle-style callback trong interface Presenter:

Nghĩa là Presenter không nên có các method như onCreate(...), onStart(), onResume() vì nhiều lí do:

  • Bằng cách này, Presenter sẽ được kết hợp đặc biệt với Activity lifecycle.
  • Present không nên có sự kết hợp với lifecycle quá phức tạp. Thực tế là các component chính của Android được thiết kế theo cách này, không có nghĩa là bạn phải làm tương tự hành vi này ở khắp mọi nơi. Nếu bạn có cơ hội đơn giản hoá thì hãy làm như vậy.

Thay vì gọi một method cùng tên, trong một Activity lifecycle callback, bạn có thể gọi action của Presenter. Ví dụ, bạn gọi load() ở cuối method Activity.onCreate(...).

6. Presenter quan hệ 1 – 1 với View:

Presenter không hợp lý khi ko có View. Có View thì sẽ có Presenter và ngược lại. Presenter sẽ kiểm soát một view trong một thời điểm.

Bạn có thể xử lý View dependency trong Presenter bằng nhiều cách. Một giải pháp đó là cung cấp một vài method như attach(View view)detach() trong Presenter interface. Vấn đề của việc triển khai này là view có thể null (nullable), sau đó bạn phải thêm null-check mỗi lần Presenter cần nó. Điều này gây nhàm chán…

Vì mối quan hệ giữa presenter và view là 1-1. Chúng ta có thể tận dụng lợi thế này. Presenter có thể lấy instance của view giống như một constructor parameter. Bạn cũng có thể cần một method để đăng ký Presenter với một vài events. Vì thế, tôi đề nghị định nghĩa method start() (hoặc cái gì đó tương tự) để chạy công việc của Presenter.

Về detach() thì sao?

Nếu bạn có method start(), bạn có thể cần ít nhất một method để giải phóng các dependencies. Vì chúng ta đã gọi method để Presenter đăng ký một số event tại start(), nên mình gọi cái này là stop().

7. Đừng save state bên trong Presenter:

Ý mình là sử dụng Bundle. Bạn không thể làm điều này nếu bạn muốn làm theo nguyên tắc thứ 2 ở trên. Bạn không thể serialize data vào trong một Bundle vì Presenter sẽ kết hợp với Android class.

Mình không nói rằng nên để stateless Presenter. Presenter ít nhất nên có page number / offset.

Vì thế, bạn phải giữ lại Presenter, đúng không?

8. Không. Đừng giữ lại Presenter:

Mình không thích giải pháp này chủ yếu vì mình nghĩ rằng Presenter không phải là một cái gì đó mà chúng ta nên lưu trữ, rõ ràng là nó không phải là một data class.

Một số đề xuất cung cấp cách để giữ Presenter trong khi configuration changes bằng cách sử dụng các fragment được giữ hoặc Loader. Mình không nghĩ rằng đây là giải pháp tốt nhất. Với mẹo này, Presenter sẽ không có vấn đề gì khi orientation changes, nhưng khi Android kills process và huỷ Activity thì sau đó chúng được tạo lại cùng nhau với Presenter mới. Vì lí do này, giải pháp này chỉ giải quyết một nửa vấn đề.

Thế thì…?

9. Cung cấp một cache cho Model để lưu trữ state của View:

Theo quan điểm của mình, để giải quyết vấn đề “restore state” sẽ đòi hỏi một chút thích nghi với app architecture. Một giải pháp tuyệt vời để giải quyết vấn đề này được đề xuất trong bài viết này. Cơ bản thì tác giả gợi ý sử dụng caching network results bằng cách sử dụng một interface giống Repository hoặc bất kỳ cái gì hướng đến quản lý dữ liệu, phạm vi trong ứng dụng và không trong Activity (để nó có thể giữ được khi orientation changes).

Interface này chỉ là một Model thông minh hơn. Sau đó nên cung cấp ít nhất một chiến lược disk-cache và có khả năng là một in-memory cache. Vì thế, kể cả nếu process bị huỷ, Presenter có thể phục hồi view state bằng cách sử dụng disk cache.

View chỉ cần quan tâm đến bất kỳ các request parameter cần thiết để restore state. Ví dụ, chúng ta chỉ cần save query.Bây giờ, bạn có 2 lựa chọn:

  • Bạn trừu tượng hoá hành vi này trong tầng model để khi Presenter gọi repository.get(params) thì nếu trang đã ở trong cache thì nguồn dữ liệu chỉ cần trả về nó, nếu không thì các APIs sẽ được gọi.
  • Bạn quản lý điều này bên trong presenter bằng việc thêm method khác trong contract để restore view state. restore(params), loadFromCache(params) hoặc reload(params) khác tên mà mô tả cùng một hành động do bạn chọn.

Đây là kiến thức của mình về Model-View-Presenter áp dụng cho Android.
Mình hi vọng bạn thích bài viết này.

Tác giả: Nguyễn Anh Thiện (Mike Nguyen) (Dịch và chỉnh sửa từ chia sẻ của Francesco Cervone).