Thế giới Flux Architecture trong iOS (phần 2)

82

PHẦN 1

Thực thi 1 tính năng sử dụng Flux pattern

Cùng tìm hiểu chi tiết về implementation của các tính năng được xây dựng vời Flux pattern.

Trong 1 ví dụ ở 2 phần tiếp theo, chúng ta sẽ sử dụng 1 tính năng đã được dùng trong production của ứng dụng PlanGrid. Tính năng này cho phép người dùng lọc các chú ý trong bản kế hoạch chi tiết:

Tính năng mà chúng ta đề cập sẽ nằm ở phần popover bên thay trái của screenshot.

Bước 1: Định rõ state

Thông thường, tôi bắt đầu thực thi 1 tính năng mới bằng cách xác định state phù hợp cho tính năng đó. State đó sẽ thể hiện mọi thứ mà UI cần biết để render phần thể hiện của 1 tính năng nào đó.

Ví dụ của chúng tôi sẽ cho thấy state của tính năng lọc các chú thích:

State gồm danh sách các filters khác nhau, 1 nhóm filter mới được chọn lọc và 1 boolean flag, cho biết có filters nào đang active hay không.

State này được điều chỉnh cho phù hợp với yêu cầu của UI. Danh sách các filters được render trong 1 table view. Nhóm filter được lọc được sử dụng để hiển thị/ ẩn chi tiết về 1 nhóm filter được chọn lọc riêng biệt. Flag isFiltering được dùng để xác định có nên kích hoạt hoặc vô hiệu 1 button xóa tất cả các filters trong UI hay không.

Bước 2: Định dạng các actions

Sau khi định dạng hình dạng của state cho 1 tính năng nào đó tôi thường nghĩ về các đột biến state khác nhau trong bước tiếp theo. Trong Flux architiecture các đột biến state được tạo mô hình theo dạng actions để mô tả loại state change nào dự kiến sẽ xảy ra. Đối với tính năng lọc chú thích, danh sách các actions khá ngắn:

Không cần khả năng hiểu sâu về tính năng thì danh sách các actions phải dễ hiểu, cho biết các actions đó đã khởi tạo các chuyển đổi state nào. Một trong những lợi ích của Flux architecture là danh sách actions này có quan điểm toàn diện về tất cả các thay đổi state có thể được kích hoạt cho tính năng cụ thể này.

Bước 3: Thực thi phản hồi cho các Actions trong Store

Trong bước này, chúng ta thực hiện logic business cốt lõi của 1 tính năng. Cá nhân tôi có khuynh hướng thực thi bước này bằng cách sử dụng TDD. Implementation của 1 store có thể được tóm gọn như sau:

  1. Đăng kí store với dispatcher dành cho tất cả các actions mà dispatcher quan tâm. Một ví dụ hiện này gồm tất cả AnnotationFilteringActions.
  2. Thực thi 1 handler được gọi cho mỗi hành động riêng biệt
  3. Trong handler, thực hiện business logic cần thiết và cập nhật state đến khi hoàn thành

Vị dụ cụ thể sau sẽ giúp chúng ta biết được cách AnnotationFilterStore xử lý toggleFilterAction:

Ví dụ này cố tình không được đơn giản hóa. Vì vậy, hãy chia nhỏ chúng ra. handleToggleFilterAction được kích hoạt bất cứ khi nào ToggleFilterAction được gửi đi. ToggleFilterAction mang thông tin về filer chuyên biệt nào nên được chuyển chế độ.

Là một bước gần như chính yếu đầu tiên khi implement business logic này, method trên đơn giản sẽ chuyển đổi filter bằng cách chuyển đổi giá trị của filter.enabled.

Sau đó, chúng ta thực thi vài business logic tùy chỉnh cho tính năng này. Khi làm việc với filters đã được xác định để lọc các chú thích vấn đề, chúng tôi gặp 1 vài trường hợp cần phải activate issueTypeFilter. Bạn không cần phân tích sâu về tính năng PlanGrid nhưng method này có thể bao bọc bất kì business logic nào liên quan tới các filters chuyển đổi.

Ở đoạn cuối method, chúng ta sẽ gọi method _applyFilter() . Đây là 1 method đã được chia sẻ và được nhiều action handlers sử dụng:

Gọi self._annotationFilterService.applyFilter() thực sự sẽ kích hoạt filtering của các chú thích hiển thị trên 1 sheet. Lọc logic khá phức tạp nên bạn có thể chuyển logic này sang type riêng biệt, chuyên dụng.

Vai trò của mỗi store là cung cấp thông tin state phù hợp với UI và trở thành coordination point (điểm phối hợp) để cập nhật state. Điều này không đồng nghĩa là toàn bộ business logic cần phải được thực thị trong chính store của nó.

Bước cuối cùng trong mỗi action handler là cập nhật state. Trong method _applyFilter(), chúng tôi cập nhật giá trị state isFiltering bằng cách kiểm tra liệu có bất kì filter nào đang được kích hoạt không.

Có 1 điều quan trọng cần lưu ý về store này: bạn mong muốn được thấy 1 cập nhật bổ sung về các giá trị của filters lưu trữ trong AnnotationFilterState. Nhìn chung, đây là cách implement các store của chúng ta, nhưng implementation này khá đặc biệt.

Bởi vì các filters được lưu trữ trong AnnotationFilterState cần phải tương tác với nhiều code Objective-C hiện tại của chúng tôi, chúng tôi đã quyết định sẽ làm mô hình filters thành các classes. Khi đó, chúng sẽ là các reference types, store và chú thích lọc UI chia sẻ 1 reference đến cùng các instances. Theo đó, tất cả các thay đổi xảy đến với filters trong store được mặc nhiên là UI nhìn thấy. Nhìn chung, chúng tôi cố gắng tránh việc này bằng cách chỉ sử dụng các value types trong state structs – nhưng đây là 1 blog post về thế giới Flux thực và trong trường hợp cụ thể này, thỏa hiệp tạo Objective-C interop dễ dàng hơn đã được chấp nhận.

Nếu các filters là các value types thì chúng ta cần chuyển nhương các filter values đã được cập nhật vào state property của chúng ta để UI có thể quan sát được những thay đổi. Bởi vì chúng ta đang sử dụng reference types ở đây, nên thay vào đó, chúng ta sẽ thực hiện 1 cập nhật về phantom state.

Việc chuyển sang property _state sẽ khởi động cơ chế cập nhật UI.

Chúng ta đã phân tích khá sâu về những chi tiết implementation, vì vậy chúng tôi muốn kết thúc section với reminder về các nhiệm vụ store cấp độ cao:

  1. Đăng kí store với dispatcher của tất cả các actions mà dispatcher quan tâm. Trong ví dụ gần đây, đó sẽ là tất cả AnnotationFilteringActions.
  2. Implement 1 handler sẽ được gọi cho mỗi hành động đơn lẻ
  3. Trong handler, hãy thực hiện business logic cần thiết và cập nhật state sau khi hoàn thành

Tiếp theo, chúng ta sẽ nói về cách Ui nhận các cập nhật state từ store.

Step 4: Gắn kết UI với Store

Một trong những concepts Flux cốt lõi chính là 1 cập nhật UI tự động sẽ được kích hoạt bất kì thời điểm nào state update xảy ra. Điều này đảm bảo rằng UI luôn luôn thể hiện state mới nhất và loại bỏ bất kì code nào được yêu cầu để duy trì thủ công các cập nhật này. Bước này tương tự như các ràng buộc của 1 View với ViewModel trong architecture MVVM.

Có rất nhiều cách để thực hiện điều này – trong PlanGrid chúng tôi đã quyết định sử dụng ReactiveCocoa để store có thể cung cấp 1 state property quan sát được. Đoạn code dưới đây sẽ hướng dẫn cách AnnotationFilterStore thực thi pattern này:

Property _state được sử dụng trong store để biến đổi state. Những khách hàng muốn đăng kí store thì sẽ sử dụng property state. Cách này cho phép những người đăng kí store nhận được các cập nhật state nhưng không cho phép họ chuyển đổi các cập nhật state trực tiếp (việc chuyển đổi state chỉ xảy ra thông qua các actions!).

Trong initializer, property nội bộ quan sát được đơn giản sẽ ràng buộc đến signal producer bên ngoài:

Bây giờ, bất kì cập nhật nào của _state sẽ tự động gửi giá trị state mới nhất thông qua signal producer được lưu trữ trong state.

Phần bên trái là code, đảm bảo rằng các UI cập nhật bất cứ khi nào giá trị state mới phát ra. Đây có thể là 1 trong những phần khó nhất khi bắt đầu với pattern Flux trong iOS. Trên trang web của mình, Flux phối hợp rất tốt với React framework của Facebook. React được thiết kế cho tình huống đặc biệt: re-render UI sau khi state cập nhật mà không yêu cầu bất kì code nào thêm.

Khi làm việc với UIKit, chúng tôi không được thoải mái như vậy, thay vào đó chúng tôi cần phải implementcasc cập nhật UI thủ công. Điểm mấu chốt chính là chúng tôi đã xây dựng vài components cung cấp 1 React như API cho UITableView và UICollectionView, chúng tôi sẽ xem xét chúng sau.

Nếu muốn nghiên cứu về những components này, bạn có check out bài nói chuyện tôi thực hiện gần đây, cũng như 2 repositories GitHub đi kèm với nó (AutoTable, UILib).

Hãy xem vài real world code (trong trường hợp này, code đã được giản lược đi 1 chút) từ tính năng lọc chú thích. Code này xuất hiện trong AnnotationFilterViewController:

Trong codebase, chúng tôi làm theo 1 quy ước là mỗi view controller có 1 method  _bind được gọi từ trong viewWillAppear. Method _bind này chịu trách nhiệm đăng kí state của store và cung cấp code cập nhật UI khi các thay đổi state xảy ra.

Bởi vì chúng ta cần phải thực thi các cập nhật UI 1 phần và không thể phụ thuộc vào 1 framework như React, method này thường có code mô tả cách 1 state nào đó cập nhật maps vào 1 cập nhật UI. ReactiveCocoa rất tiện dụng vì nó cung cấp nhiều operators khác nhau (skipUntil, take, map …) hỗ trợ thiết lập các quan hệ này dễ dàng hơn. Nếu bạn chưa dùng 1 thư viện Reactive trước đây thì code này có thể hơi rắc rối – nhưng bạn có thể nghiên cứu 1 phần nhỏ ReactiveCocoa mà chúng tôi sử dụng.

Line đầu tiên trong ví dụ method _bind ở trên đảm bảo là table view được cập nhật bất cứ khi nào 1 cập nhật state xảy ra. Chúng tôi sử dụng operator ReactiveCocoa ignoreNil() để đảm bảo là chúng tôi không khởi động các cập nhật cho 1 state trống. Sau đó, chúng tôi sử dụng operatore map để phác họa lại state mới nhất từ store đến 1 mô tả cách hiển thị của table view.

Phác thảo biểu đồ sẽ được thực hiện trong method annotationFilterViewProvider.tableViewModelForState Đây lúc wrapper UIKit như React tùy chỉnh nhập cuộc.

Tôi sẽ không nghiên cứu tất cả các chi tiết về implementation, nhưng dưới đây là cách hiển thị của method tableViewModelForState

Nguồn: IDE Academy via Blog.Benjamin (còn tiếp)