+1

Clean Architecture và MVVM trên iOS (Swift) phần 2

phần 1 chúng ta đã tìm hiểu về Clean Architecture. Trong phần 2 này, chúng ta sẽ tiếp tục với mô hình MVVM trong series Clean Architecture và MVVM trên iOS (Swift)

MVVM

Model-View-ViewModel pattern (MVVM) phân tách rõ ràng giữa UI và domain. Khi sử dụng chúng (MVVM) với Clean Architecture có thể phân tách rõ ràng giữa các layer UI và Presentation.

Việc triển khai các View khác nhau có thể sử dụng chung ViewModel. Ví dụ: bạn có thể triển khai CarsAroundListViewCarsAroundMapView và sử dụng CarsAroundViewModel cho cả hai. Bạn cũng có thể triển khai một View UIkit và View khác với SwifUI. Điều quan trọng cần nhớ là không import UIkit, WatchKit, SwiftUI bên trong ViewModel của bạn, điều này giúp bạn có thể tái sử dụng nó trong các platform khác nếu cần.

° ° °

image.png

Ví dụ, liên kết dữ liệu giữa ViewViewModel có thể được thực hiện với các closures, Delegate hoặc Observerble (ví dụ: RxSwift). CombineSwiftUI cũng có thể được sử dụng nhưng chỉ khi hệ thống iOS >= 13. View có mối quan hệ trực tiếp với ViewModel và thông báo cho nó bất cứ khi nào một event bên trong View xảy ra. Từ ViewModel, không có tham chiếu trực tiếp đến View (chỉ Data Binding).

Trong ví dụ này, chúng tôi sẽ sử dụng kết hợp đơn giản giữa Closure và didSet để tránh sự phụ thuộc của bên thứ ba:

public final class Observable<Value> {
    
    private var closure: ((Value) -> ())?

    public var value: Value {
        didSet { closure?(value) }
    }

    public init(_ value: Value) {
        self.value = value
    }

    public func observe(_ closure: @escaping (Value) -> Void) {
        self.closure = closure
        closure(value)
    }
}

Lưu ý: Đây là phiên bản Observable rất đơn giản, để xem toàn bộ triển khai với multiple observerbleobserver removal : Observerble.

Một ví dụ về Data Binding của ViewController:

final class ExampleViewController: UIViewController {
    
    private var viewModel: MoviesListViewModel!
    
    private func bind(to viewModel: ViewModel) {
        self.viewModel = viewModel
        viewModel.items.observe(on: self) { [weak self] items in
            self?.tableViewController?.items = items
            // Important: Bạn không thể sử dụng viewmodel bên trong closure này, nó có thể là nguyên nhân rò rỉ bộ nhớ (viewModel.items.value not allowed)
            // self?.tableViewController?.items = viewModel.items.value // nó có thể retain cycle. bạn có thể access viewModel chỉ với self?.viewModel
        }
        // hoặc là chỉ trên 1 line
        viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bind(to: viewModel)
        viewModel.viewDidLoad()
    }
}


protocol ViewModelInput {
    func viewDidLoad()
}

protocol ViewModelOutput {
    var items: Observable<[ItemViewModel]> { get }
}

protocol ViewModel: ViewModelInput, ViewModelOutput {}

Note: Không được phép truy cập viewModel từ observing closure. Nó là nguyên nhân của retain cycle(memory leak). Bạn có thể truy cập viewModel chỉ bằng self: self?.viewModel.

Một ví dụ data binding trên TableViewCell (Reusable Cell):

final class MoviesListItemCell: UITableViewCell {

    private var viewModel: MoviesListItemViewModel! { didSet { unbind(from: oldValue) } }
  
    func fill(with viewModel: MoviesListItemViewModel) { 
        self.viewModel = viewModel
        bind(to: viewModel)
    }
    
    private func bind(to viewModel: MoviesListItemViewModel) {
        viewModel.posterImage.observe(on: self) { [weak self] in self?.imageView.image = $0.flatMap(UIImage.init) }
    }
    
    private func unbind(from item: MoviesListItemViewModel?) {
        item?.posterImage.remove(observer: self)
    }
}

Note: Chúng ta phải hủy liên kết nếu View có thể reusable (ví dụ: UITableViewCell)

MVVM Templates có thể xem ở đây

MVVMs Communication

Delegation

ViewModel của một MVVM (screen) giao tiếp with ViewModel khác của MVVM(screen) khác sử dụng delegation pattern:

image.png

Ví dụ, chúng ta có ItemsListViewModelItemEditViewModel. Sau đó tạo một protocol ItemEditViewModelDelegate với phương thức ItemEditViewModelDidEditItem(item). Và cho viewmodel conform với protocol này: extension ListItemsViewModel: ItemEditViewModelDelegate

// Step 1: Define delegate and add it to first ViewModel as weak property
protocol MoviesQueryListViewModelDelegate: class {
    func moviesQueriesListDidSelect(movieQuery: MovieQuery)
}
...
final class DefaultMoviesQueryListViewModel: MoviesListViewModel {
    private weak var delegate: MoviesQueryListViewModelDelegate?
    
    func didSelect(item: MoviesQueryListViewItemModel) { 
        // Note: We have to map here from View Item Model to Domain Enity
        delegate?.moviesQueriesListDidSelect(movieQuery: MovieQuery(query: item.query))
    }
}

// Step 2:  Make second ViewModel to conform to this delegate
extension MoviesListViewModel: MoviesQueryListViewModelDelegate {
    func moviesQueriesListDidSelect(movieQuery: MovieQuery) {
        update(movieQuery: movieQuery)
    }
}

Note: Chúng ta cũng có thể đặt tên Delegates trong trường hợp này như là một Responders: ItemEditViewModelResponder

Closures

Một cách khác để giao tiếp là sử dụng closures được chỉ định hoặc đưa vào bởi FlowCoordinator. Trong example project chúng ta có thể thấy cách mà MoviesListViewModel sử dụng action closure showMovieQueriesSuggestions để show MoviesQueriesSuggestionsView. Nó cũng truyền parameter (_ didSelect: MovieQuery) -> Void nên chúng cũng có thể được gọi từ View đó. cách giao tiếp này được kết nối bên trong MoviesSearchFlowCoordinator, xem ví dụ bên dưới:

// MoviesQueryList.swift
// Step 1: Define action closure to communicate to another ViewModel, e.g. here we notify MovieList when query is selected
typealias MoviesQueryListViewModelDidSelectAction = (MovieQuery) -> Void

// Step 2: Call action closure when needed
class MoviesQueryListViewModel {
    init(didSelect: MoviesQueryListViewModelDidSelectAction? = nil) {
        self.didSelect = didSelect
    }
    func didSelect(item: MoviesQueryListItemViewModel) {
        didSelect?(MovieQuery(query: item.query))
    }
}

// MoviesQueryList.swift
// Step 3: When presenting MoviesQueryListView we need to pass this action closure as paramter (_ didSelect: MovieQuery) -> Void
struct MoviesListViewModelActions {
    let showMovieQueriesSuggestions: (@escaping (_ didSelect: MovieQuery) -> Void) -> Void
}

class MoviesListViewModel { 
    var actions: MoviesListViewModelActions?

    func showQueriesSuggestions() {
        actions?.showMovieQueriesSuggestions { self.update(movieQuery: $0) } 
        //or simpler actions?.showMovieQueriesSuggestions(update)
    }
}

// FlowCoordinator.swift
// Step 4: Inside FlowCoordinator we connect communication of two viewModels, by injecting actions closures as self function
class MoviesSearchFlowCoordinator {
    func start() {
        let actions = MoviesListViewModelActions(showMovieQueriesSuggestions: self.showMovieQueriesSuggestions)
        let vc = dependencies.makeMoviesListViewController(actions: actions)  
        present(vc)
    }

    private func showMovieQueriesSuggestions(didSelect: @escaping (MovieQuery) -> Void) {
        let vc = dependencies.makeMoviesQueriesSuggestionsListViewController(didSelect: didSelect)
        present(vc)
    }
}

Tách layer thành frameworks (Modules)

Bây giờ, với mỗi layer (Domain, Presentation, UI, Data, Infrastructure Network) của example app có thể dễ dàng tách thành frameworks riêng biệt.

New Project -> Create Project… -> Cocoa Touch Framework

Sau đó bạn có thể sử dụng các frameworks đó vào project bằng cách sử dụng CocoaPods. Bạn có thể thấy example project ở đây:

Note: Bạn cần phải delete ExampleMVVM.xcworkspace và run pod install để tạo ra một cái mới, bởi vì đó là một issue cấp phép.

° ° °

image.png

° ° °

Dependency Injection Container

Dependency injection là một kỹ thuật theo đó một đối tượng cung cấp các phụ thuộc của một đối tượng khác. DIContainer trong ứng dụng của bạn là đơn vị trung tâm của tất cả các lần injection.

Using dependencies factory protocols

Một trong những option để khai báo một dependencies protocol là delegates, sự khởi tạo của dependency cho DIContainer. Để làm điều này, chúng ta cần define MoviesSearchFlowCoordinatorDependencies protocol và cho MoviesSceneDIContainer confirm chính protocol này, và sau đó, đưa vào bên trong MoviesSearchFlowCoordinator như một param để khởi tạo và present MoviesListViewController. Dưới đây là các bước :

// Define Dependencies protocol for class or struct that needs it
protocol MoviesSearchFlowCoordinatorDependencies  {
    func makeMoviesListViewController() -> MoviesListViewController
}

class MoviesSearchFlowCoordinator {
    
    private let dependencies: MoviesSearchFlowCoordinatorDependencies

    init(dependencies: MoviesSearchFlowCoordinatorDependencies) {
        self.dependencies = dependencies
    }
...
}

// Make the DIContainer to conform to this protocol
extension MoviesSceneDIContainer: MoviesSearchFlowCoordinatorDependencies {}

// And inject MoviesSceneDIContainer `self` into class that needs it
final class MoviesSceneDIContainer {
    ...
    // MARK: - Flow Coordinators
    func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
        return MoviesSearchFlowCoordinator(navigationController: navigationController,
                                           dependencies: self)
    }
}

Using closures

Một option khác là closures. Để làm điều này chúng ta cần khai báo closure bên trong class cần injection và sau đó chúng ta inject closure này. Ví dụ:

// Define makeMoviesListViewController closure that returns MoviesListViewController
class MoviesSearchFlowCoordinator {
   
    private var makeMoviesListViewController: () -> MoviesListViewController

    init(navigationController: UINavigationController,
         makeMoviesListViewController: @escaping () -> MoviesListViewController) {
        ...
        self.makeMoviesListViewController = makeMoviesListViewController
    }
    ...
}

// And inject MoviesSceneDIContainer's `self`.makeMoviesListViewController function into class that needs it
final class MoviesSceneDIContainer {
    ...
    // MARK: - Flow Coordinators
    func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
        return MoviesSearchFlowCoordinator(navigationController: navigationController,
                                           makeMoviesListViewController: self.makeMoviesListViewController)
    }
    
    // MARK: - Movies List
    func makeMoviesListViewController() -> MoviesListViewController {
        ...
    }
}

Source code

Resources

Advanced iOS App Architecture

The Clean Architecture

The Clean Code

Kết luận

Architectural patterns được sử dụng hầu hết trong mobile are Clean Architecture(Layered), MVVM, and Redux.

MVVM và Clean Architecture tất nhiên có thể sử dụng riêng biệt, nhưng MVVM chỉ tách biêt bên trong của Presentation Layer, trong khi Clean Architecture tách code của bạn thành các modular layer điều đó có thể làm có project có thể dễ dàng test, tái sử dụng.

IĐiều quan trọng là không được bỏ qua việc tạo Use Case, mặc dù Use Case không làm điều gì khác ngoài calling Repository. Bằng cách này, kiến trúc của bạn sẽ dễ hiểu khi một dev mới nhìn thấy các Use Case của ban.

Mặc dù điều này sẽ hữu ích như một điểm khởi đầu, nhưng không có sản phẩm, phương pháp hay thủ thuật nào có thể đảm bảo đó là kết quả tốt nhất . Tùy vào nhu cầu của dự án mà bạn có thể chọn một kiến trúc phù hợp.

Clean Architecture work thực sự tốt với (Test Driven Development) TDD. Architecture này làm cho project của bạn mang tính testable và các layer có thể thay thế dễ dàng (UI and Data).

END

link gốc bài viết : https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí