iOS Architecture: MVVM-C, Coordinators

1. Coordinator là gì?

Coordinator là một Object (kiểu Class trong Swfit) mà có trách nhiệm duy nhất đó là điều phối (coordinate) lo cái việc navigation của app (màn hình nào sẽ được show, màn hình nào sẽ xuất hiện tiếp). 

Các bước Coordinator phải làm: 

  • Khởi tạo ViewController’s & ViewModel’s
  • Khởi tạo và nhúng các dependencies vào ViewController’s and ViewModel’s
  • Present hoặc push các ViewController’s lên các screen

2. Tại sao nên sử dụng Coordinator

Coordinators làm giúp ViewController cái việc mà có vẻ không thuộc về phận sự của nó, làm cho VC nhìn gọn hơn và dễ reuse hơn. Điều này giúp tuân thủ single responsibility principle trong SOLID. Mình có viết loạt bài về SOLID, các bạn có thể đọc ở đây.

Một VC không nên biết nó đến từ đâu, đi đến đâu tiếp theo và cần phải đem theo những cái gì. Công việc của nó chỉ nên xà quần xung quanh các View/ SubViews và chỉ handle những chuyện liên quan đến UIKit. Tất cả những cái liên quan đến navigation trên VC nên để cho Coordinator handle

3. Coordinator base class

OK, vào code thôi. Đầu tiên các bạn tạo một lớp base Coordinator như sau: 

Đây là lớp abstract ban đầu, tất cả các lớp khác muốn sử dụng Coordinator thì cũng phải kế thừa. Nếu bạn muốn tạo 1 coordinator, trước hết bạn cần subclass cái base này. Tiếp theo override 2 methods là start và finish. Sau đó bạn viết các method handle việc add hoặc remove các child coordinator’s. Rất giống với cách làm việc của UIViewController đúng không?

4. Coordinator base class

Ứng với mỗi màn hình or scene tuỳ tình huống nên có 1 coordinator cụ thể để quản lý.  Và luôn có 1 “main” AppCoordinator sẽ được dùng ở App Delegate.

Đoạn code dưới đây là ví dụ về basic AppCoordinator. Tất cả những co-or khác đều là children của AppCo-or.

Đoạn code trên có 3 phần cơ bản:

  • Properties: Những properties mà object sở hữuTrong trường hợp này vì đó là AppCoordinator, nó phải sở hữu window. Nó cũng sở hữu root và các ViewController khác, và bất kỳ dependencies nào mà nó cần, giống như ApiClient trong trường hợp này.
  • Init method: Ở đây bạn phải lấy bất kỳ dependencies nào bạn cần trong coordinator này. Trong trường hợp ở đây vì đó là coordinator đầu tiên, dependency duy nhất mà ta cần là app’s window.
  • Coordinator: Ở đây bạn phải override lên các method Start() và Finish(). Method Start() là method quan trọng nhất, là nơi bạn present UI của bạn lên screen. Trong trường hợp này, ta set root view controller  của ta lên trên window.

Đây là cách bạn start app của mình từ AppDelegate sử dụng AppCoordinator. 

Bạn sẽ dùng AppCoordinator như là 1 property trong AppDelegate của mình, y hệt cái cách mà Window đã giữ như mình thấy trước đây. Và bên trong hàm application sẽ khởi tạo AppCoordinator với UIWindow và call method start() với nó

5. Child Coordinators

Rõ ràng, app của bạn sẽ cần ít nhất 1 hoặc nhiều hơn child coordinator để handle  
Your app will obviously need at least one, if not many more, child coordinator’s to all scenes trong app.

Child coordinator sẽ giống với AppCoordinator nhưng sẽ có nhiều việc hơn tuỳ vào scene của mình phức tạp như thế nào.

Phân tích code như sau:

  • Properties: Ta có một reference đến storyboard liên quan đến coordinator/scene ở đây, và tất cả view controller’s (trong ví dụ trên storyboard Search có 2 VC) và dependencies khác. 
  • Init, Root View Controller: SearchInputCoordinator này được initialized với 1 root ViewController, không phải 1 window giống như trong AppCoordinator.
  • Init, Api Client: Lưu ý rằng ta cũng get luôn ApiClient trong initializer. Như mình đã nói về AppCoordinator, ta sẽ get tất cả các dependencies mà ta cần trong bộ khởi tạo. Vì ApiClient có session headers quan trọng, ta muốn dùng 1 cái giống nhau cho toàn bộ app. Đây là 1 điểm mạnh của coordinators, bạn không cần Singletons nữa. Ta chỉ có instance của ApiClient này, và ta pass nó đi nơi khác bằng cách dùng dependency injection, Ta không còn dùng static global variable để access nó từ khắp mọi nơi - điều mà được xem như anti-pattern.
  • Start: Về cơ bản, kiểu root “container” mà ta khởi tạo với các công việc mà ta sẽ làm ở method Start(). Trong trường hợp này, vì nó là một TabBarController, ta sẽ add chính ta vào nó. Nếu đó là một NavigationController, ta sẽ tự push mình vào nó, và nếu đó là một ViewController, ta sẽ present chính ta trên đó.
  • Finish: Ở đây ta sẽ dọn sạch bất kì VC nào mà ta có trên navigation stack hay ở bất cứ đâu. Ngoài ra còn để cancel lớp server với bất kì requests nào còn pending. Và cuối cùng, ta gọi delegate của ta, nếu có thì send tín hiệu để biết là ta đã xong.

6. Navigation

Navigation phần quan trọng trong Coordinators. Ta sẽ add 1 extension đến coordinator để handle all navigation, có 1 method cho mỗi navigation mà có thể xảy ra.

Ở đây có 1 sự khác biệt giữa 2 func như sau: 

  • goToLocationSearch khởi tạo 1 VC và presents nó trên ViewController được passed ở phía trên(cái đoạn truyền vào parameter ấy). Đặc biệt, phần ViewController này vẫn là 1 phần của scene này.
  • goToSearch là khác với cái đầu vì ở đây ta đang chuẩn bị out khỏi Scene. Đây là phần mà ta chuẩn bị move sang 1 scene khác nên nó sẽ được handle bởi một coordinator khác. Cho nên ở đây, ta chỉ khởi tạo cái coordinator khác, và gửi các dependencies cần thiết, như là root ViewController. Chúng ta call start() ở đó, và quan trọng nhất là ta add nó như 1 child coordinator. Nếu không có đoạn này, không ai sẽ hold coordinator và nó sẽ được released.

7. Coordinator Callbacks

Phần quan trọng khác của coordinator là delegate/ callbacks, ta sẽ lấy từ ViewModel’s (or ViewControllers) và những child coordinators khác.

Trước tiên, ta có thể gọi callbacks từ child coordinator của ta. Nếu 1 child coordinator phải inform ta về cái gì đó, nó sẽ có 1 delegate protocol mà ta buộc phải implement.

// MARK: - Coordinator Callback's
extension SearchInputCoordinator: SearchCoordinatorDelegate {
  func didFinish(from coordinator: SearchCoordinator) {
    removeChildCoordinator(coordinator)
  }
}

Trong trường hợp này, ta sẽ implemente the didFinishFrom: delegate method của child coordinator. Ở đây, nếu child coordinator của ta đã xong, finish method của nó sẽ được gọi. Và hàm finish nên gọi delegate của nó để báo nó biết là nó đã xong. Sau đó, ta remove child coordinator ra khỏi stack của ta. Để đảm bảo nó bị removed khỏi bộ nhớ.

8. ViewModel Callbacks

// MARK: - ViewModel Callback's
extension SearchInputCoordinator: SearchInputViewModelCoordinatorDelegate {

  func didSelectOrigin(from controller: UIViewController) {
    goToLocationSearch(searchType: .origin, from: controller)
  }

  func didSelectDestination(from controller: UIViewController, fromPlace place: Place) {
    goToLocationSearch(searchType: .destination(from: place), from: controller)
  }
}

extension SearchInputCoordinator: LocationSearchViewModelCoordinatorDelegate {

  func didSelect(place: TripPlace, from controller: UIViewController) {
    if place.type == .origin {
      searchInputViewModel.state.origin = place
    } else if place.type == .destination {
    searchInputViewModel.state.destination = place
    }
  controller.dismiss(animated: true, completion: nil)
  }

  func didSelectClose(from viewModel: LocationSearchViewModel, from controller: UIViewController) {
    controller.dismiss(animated: true, completion: nil)
  }
}

Cuối cùng, các kiểu callbacks khác mà mình có thể có từ ViewModel của mình (hoặc ViewControllers nếu bạn không sử dụng ViewModel).

Các callbacks này cho ta biết nếu người dùng muốn navigate to phần khác của app và từ đây ta sẽ gọi các method navigation goTo khác. Để ý cái cách mà cái request ViewController này là một paremeter trong những delegate methods này. Điều này làm cho present view controllers lên trên chúng mà không bị giữ reference hoặc đi tìm View Controller trong hierarchy của ta trở nên dễ dàng hơn.

Tổng kết:
Phần coordinator này vừa dễ mà vừa khó, dễ hình dung nhưng áp dụng có vẻ không phải đơn giản. Qua bài này, hi vọng các bài có cái nhìn cơ bản về cách sử dụng MVVM-C

Tham khảo: https://medium.com/sudo-by-icalia-labs/ios-architecture-mvvm-c-coordinators-3-6-3960ad9a6d85

Bình luận
* Các email sẽ không được công bố trên trang web.
I BUILT MY SITE FOR FREE USING