iOS · redux

Redux trong Swift – Phần 2

Sau hai tuần tập trung cho ứng dụng của công ty, tuần này mình sẽ tiếp tục với phần tiếp theo về Redux. Trong phần này, mình sẽ giới thiệu chi tiết hơn về những thành phần cơ bản của Redux như: Store, Action và Reducer bằng cách thực hiện một ví dụ đơn giản.

Phần 1: Giới thiệu Redux
Phần 2: Sử dụng Redux trong Swift: Action, Store, Reducer
Phần 3: Sử dụng Middleware, ActionCreator

Chúng ta thực hiện một ứng dụng đơn giản như hình phía dưới. Ứng dụng có hai tabs, ở tab thứ nhất khi người dùng nhấn nút Increase thì giá trị tăng thêm 1, nếu người dùng nhấn nút Decrease thì giá trị giảm 1. Ở tab thứ hai, khi người dùng nhấn nút Reset thì giá trị sẽ bằng giá trị ban đầu.

This slideshow requires JavaScript.

Như trong phần trước  mình có giới thiệu, việc phản hồi của Store cho Views mang tính Reactive, nên mình viết thư viện RxRedux dựa trên  ReSwift của Benjamin Encz, đồng thời kết hợp Redux và Reactive Programming. Nhờ đó, chúng ta có thể tận dụng sức mạnh của ReactiveX như: map, flatMap, filter, … giúp cho việc hiển thị dữ liệu dễ dàng hơn, đồng thời ReactiveX còn là công cụ hữu hiệu để giảm thiểu việc hiện thị lại view khi có thay đổi không cần thiết bằng cách sử dụng các toán tử như map, flatMap, combine, distinctUntilChange,…

Trong ví dụ này, mình sẽ sử dụng RxRedux. Mặc dù ứng dụng khá đơn giản, chưa thể hiện hết những ưu điểm của Redux, nhưng hi vọng, sau khi hoàn thành ứng dụng, chúng ta có thể nắm bắt được những phần sau:

  • Khởi tạo một ứng dụng mới sử dụng Redux.
  • Cách thực hiện State, Actions, Reducers
  • Hiển thị dữ liệu từ store
  • Chia sẻ dữ liệu giữa các Views

Chúng ta bắt đầu bằng cách tạo một project mới chọn Tabbed Application. Sau đó thêm các thư viện cần thiết vào project:

pod 'RxSwift', '~> 3.0'
pod 'RxCocoa', '~> 3.0'
pod 'RxRedux', :git => 'https://github.com/hungdv136/rx.redux.git'

Yêu cầu: xCode 8, Swift 3.0

1. State

State là một đối tượng chứa trạng thái và dữ liệu của ứng dụng. Một ứng dụng chỉ có duy nhất một state (nguyên lý 1 – single source of truth) chứa tất các những đối tượng cần thiết cho ứng dụng.

Trong Swift thì State nên là một struct để đảm bảo nguyên lý 2 – State is read only. Trong phần trước mình đã giới thiệu ba nguyên lý của Redux, nguyên lý thứ hai đề cập tới việc read-only của State. State chỉ được thay đổi thông qua một action tại Reducer. Khi state được gán cho các thành phần khác như views, thì việc thay đổi đó chỉ có giá trị nội tại mỗi thành phần đó, không làm ảnh hưởng tới State cũng như các thầnh phần khác. Ngoài ra, việc sử dụng struct giúp chúng ta tránh được những vấn đề phức tạp khi sử dụng class, các bạn có thể tham khảo thêm tại đây, mình không muốn chi tiết cho phần này , vì không thể làm tốt hơn . Sau khi xem xong, các bạn có thể hiểu thêm được phần nào lý do, rất nhiều đối tượng trong Swift 3.0 được chuyển qua struct thay vì class như Swift 2.x.

Với ứng dụng chúng ta đang thực hiện thì State sẽ đơn giản như sau:

struct AppState: StateType {
    var counter: Int = 0
}

2. Actions

Action là đối tượng dữ liệu diễn tả một hành động trong hệ thống. Không có bất kỳ một sự rằng buộc nào về kiểu dữ liệu của action trong Redux, nên chúng ta thấy có ứng dụng sử dụng chuỗi hằng số, có ứng dụng sử dụng struct, hoặc enum. Miễn sao kiểu dữ liệu của action được đồng nhất trong cùng một dự án. Trong các ứng dụng thực tế thì chúng ta thường sử dụng struct hoặc enum để có thể truyền thêm những tham số cần thiết.

enum CounterAction: Action {
    case increase
    case decrease
    case reset
}

2. Reducers

Reducer có nhiệm vụ chuyển đổi trạng thái của ứng dụng từ trạng thái hiện tại qua trạng thái mới. handleAction phải là một pure function để đảm bảo hai yếu tố sau:

  • Không thay đổi thông số truyền vào. Mục đích là không làm ảnh hưởng tới các thành phần liên quan trong ứng dụng. Việc phản hồi sẽ do store đảm nhiệm sau khi Reducer hoàn thành
  • Không phụ thuộc vào các trạng thái bên ngoài, handleAction phải đảm bảo cùng một tham số truyền vào, sẽ nhận được cùng một kết quả trả về. Việc này giúp cho State có thế đoán biết trước được (state is a predictable container).

Trong Redux thì chỉ có một Reducer. Trong các ứng dụng thực tế, chúng ta có thể tách Reducer thành nhiều Reducers khác nhau, mỗi Reducer xử lý các miền dữ liệu khác nhau. Sau đó chúng ta sử dụng combineReducer để kết hợp các Reducer này thành một Reducer duy nhất. Việc sử dụng combineReducer sẽ được đề cập trong phần tiếp theo, với ứng dụng này thì Reducer sẽ đơn giản như sau:

struct CounterReducer: Reducer {
    func handleAction(state: AppState, action: Action) -> AppState {
        var state = state
        guard let action = action as? CounterAction else { return state }
        
        switch action {
        case .increase:
            state.counter += 1
        case .decrease:
            state.counter -= 1
        case .reset:
            state.counter = 0
        }
        return state
    }
}

3. Views

Chúng ta sẽ thực hiện hai views, mục đích của việc này giúp chúng ta có thể thấy được các view chia sẻ dữ liệu với nhau như thế nào.

Để hiện thị giá trị hiện tại lên label chúng ta sử dụng store.state.map…drive như ở phần Data Binding bên dưới. Khi có bất kỳ sự thay đổi nào từ store thì label sẽ được tự động cập nhật lại giá trị

Khi người dùng nhấn nút decrease, view sẽ dispatch ra một action:  store.dispatch(action: CounterAction.decrease). Tương tự, khi người dùng nhấn nút increase. Lúc đó chúng ta sẽ thấy giá trị trên label được thay đổi tương ứng với hành động của người dùng.

final class CounterViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // MARK: Actions
        decreaseButton.rx.tap.asDriver().drive(onNext: { _ in
            store.dispatch(action: CounterAction.decrease)
        }).addDisposableTo(disposeBag)

        increaseButton.rx.tap.asDriver().drive(onNext: { _ in
            store.dispatch(action: CounterAction.increase)
        }).addDisposableTo(disposeBag)

        // MARK: Data Binding
        store.state.map{$0.counter}.drive(onNext: { [weak self] value in
            self?.countLabel.text = "\(value)"
        }).addDisposableTo(disposeBag)
    }

    private let disposeBag = DisposeBag()

    @IBOutlet private weak var countLabel: UILabel!
    @IBOutlet private weak var increaseButton: UIButton!
    @IBOutlet private weak var decreaseButton: UIButton!
}

Chúng ta thực hiện tương tự cho view thứ hai

final class ResetViewController : UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // MARK: Data Binding
        store.state.map{$0.counter}.drive(onNext: { [weak self] value in
            self?.label.text = "\(value)"
        }).addDisposableTo(disposeBag)

        // MARK: Actions
        resetButton.rx.tap.asDriver().drive(onNext: { _ in
            store.dispatch(action: CounterAction.reset)
        }).addDisposableTo(disposeBag)
    }

    private let disposeBag = DisposeBag()

    @IBOutlet private weak var resetButton: UIButton!
    @IBOutlet private weak var label: UILabel!
}

Chúng ta đã hoàn thành ứng dụng Counter sử dụng RxRedux:

rx-redux-counter

4. Đánh giá

CounterViewController  và ResetViewController chia sẻ dữ liệu với nhau khá dễ dàng, triệt để. Khi có bất kỳ sự thay đổi nào từ một bên, bên còn lại sẽ nhận được kết quả tức thì mà không cần quan tâm tới sự tồn tại, hay cách thực hiện bên trong của nhau. Nếu chúng ta sử dụng mô hình MVC, chúng ta có thể phải sử dụng delegate, notification, hoặc sử dụng chung một View Model.

ViewController không kiêm nhiệm quá nhiều nhiệm vụ. Khi user thực hiện một tác vụ, ViewController chỉ dispatch ra một action, kết thúc xử lý, không cần phải chờ đợi kết quả của action, mọi thao tác khác sẽ có thành phần khác xử lý, sau đó phản hồi lại cho View.

5. Kết

Trong phần này mình chỉ giới thiệu những thành phần cơ bản trong Redux, giúp chúng ta có thể dễ dàng nắm bắt các khái niệm trong Redux. Trong phần tiếp theo, mình sẽ giới thiệu về ActionCreator và Middleware, cũng như thực hiện một ứng dụng với một số yêu cầu thường gặp trong thực tế.

Source Code

7 thoughts on “Redux trong Swift – Phần 2

      1. Hôm nay đi nghe anh chia sẽ về reactive programming thấy rất hay và bổ ích. Đối với những loại mô hình kiến trúc REDUX như vầy, em nghĩ a nên làm 1 example đơn gian và không nên dùng thêm các thư viện hổ trợ nó tại vì làm như vậy các bên sẽ phụ thuộc nhau(retain cycle). Anh nên giới thiệu nó với 1 ví dụ bằng các kỹ thuật đơn giản có trong iOS, xong rồi mới tối ưu nó làm nó nhanh hơn và đơn giản hơn với thư viện hỗ trợ.
        Thay cho một món quà tặng có giá trị, xin gửi những lời chúc tốt đẹp, hạnh phúc tới Anh. Chúc Anh một mùa Giáng sinh an lành, một năm mới nhiều may mắn, một mùa đông thật ấm áp bên gia đình và người thương yêu.
        Merry Christmas!

        Liked by 1 person

      2. Thanks bạn. Mình giới thiệu RxRedux với mục địch, nếu bạn nào đang dùng Rx và muốn trải nghiệm Rx + Redux, có thể tham khảo. Nếu bạn muốn tìm một ví dụ về Redux sủ dụng thuần các thư viện iOS có thể tham khảo các ví dụ trong Reswift cuả Benz https://github.com/ReSwift/ReSwift. Mọi khái niệm giữa RxRedux và Reswift là tương đương, trừ phẩn phản hồi Store -> View. Mình không muốn lập lại, vì nó hơi lãng phí.

        Số lượt thích

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s