Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MVVM as a good (anti)pattern in iOS - RSConf 2019

MVVM as a good (anti)pattern in iOS - RSConf 2019

Nowadays there are a lot of materials, presentations & talks about how great MVVM architecture is. The hype about the view models looks like a never-ending story. There are a lot of developers who like this approach and use it in their own projects. I tried to go against the grain and show a different perspective of MVVM.

Avatar for Mateusz Szklarek

Mateusz Szklarek

August 09, 2019

More Decks by Mateusz Szklarek

Other Decks in Programming

Transcript

  1. Agenda 1. Short explanation WHY? 2. MVVM - where it

    comes from? 3. MVVM in i OS World 4. Alternative solutions 5. Conclusions 6. Q & A @SzklarekMateusz RSConf 2019 3
  2. Swift code 4 including mine 3 years ago, 4 in

    other companies, @SzklarekMateusz RSConf 2019 10
  3. Swift code 4 including mine 3 years ago, 4 in

    other companies, 4 open source projects available on github, @SzklarekMateusz RSConf 2019 10
  4. Swift code 4 including mine 3 years ago, 4 in

    other companies, 4 open source projects available on github, 4 tons of articles which promote MVVM. @SzklarekMateusz RSConf 2019 10
  5. Invented by Microsoft architects Ken Cooper & Ted Peters to

    simplify event-driven programming of user interfaces. @SzklarekMateusz RSConf 2019 15
  6. Introduced for the first time in 2005 by John Gossman

    - one of Microsoft's WPF1 and Silverlight architects. 1 Windows Presentation Foundation @SzklarekMateusz RSConf 2019 16
  7. Windows Presentation Foundation is a graphical subsystem created by Microsoft

    for rendering user interfaces in Windows-based applications. Released as part of .NET Framework 3.0 in 2006. @SzklarekMateusz RSConf 2019 17
  8. public class CustomerViewModel { private Customer obj = new Customer();

    public string TxtCustomerName { get { return obj.CustomerName; } set { obj.CustomerName = value; } } public string TxtAmount { get { return Convert.ToString(obj.Amount) ; } set { obj.Amount = Convert.ToDouble(value); } } ... } @SzklarekMateusz RSConf 2019 19
  9. public class CustomerViewModel { ... public string LblAmountColor { get

    { if (obj.Amount > 2000) { return "Blue"; } else if (obj.Amount > 1500) { return "Red"; } return "Yellow"; } } public bool IsMarried { get { if (obj.Married == "Married") { return true; } else { return false; } } } } @SzklarekMateusz RSConf 2019 20
  10. private void DisplayUi(CustomerViewModel customer) { lblName.Content = customer.TxtCustomerName; lblAmount.Content =

    customer.TxtAmount; BrushConverter brushconv = new BrushConverter(); lblBuyingHabits.Background = brushconv.ConvertFromString(customer.LblAmountColor) as SolidColorBrush; chkMarried.IsChecked = customer.IsMarried; } @SzklarekMateusz RSConf 2019 21
  11. Gossman introduced MVVM as a standardized way to leverage core

    features of WPF to simplify the creation of user interfaces. In that sense, I consider MVVM to be a tailor-made for the WPF and Silverlight platforms. -- Josh Smith (Microsoft), February 2009 @SzklarekMateusz RSConf 2019 26
  12. View model is an abstract object between a model object

    and a view controller, which performs any data transformation necssary for presentation, as well as sometimes networking and database access, might do a little form validation and any other tasks you feel like throwing in there, and acts as a jack-of-all-trades object originally intended to transfer weight away from your controllers but ultimately creates a new kitchen sink for you to dump responsibilities into. Soroush Khanlou @SzklarekMateusz RSConf 2019 31
  13. class SavedListViewModel { var title: String // MARK: Child view

    models var itemViewModels: [SavedListItemViewModel] {} var loadMoreViewModel: FetchMoreViewModel? {} // MARK: Loading var isLoading: Bool {} var isPerformingAction: Bool {} // MARK: Actions func present(details: PropertyDTO) {...} func itemLeftButtonAction(itemViewModel: SavedListItemViewModel) {...} func itemRightButtonAction(itemViewModel: SavedListItemViewModel) {...} // MARK: Networking func reloadItems() {} func loadMoreItems() {} } @SzklarekMateusz RSConf 2019 32
  14. class SavedListViewModel { var title: String // MARK: Child view

    models var itemViewModels: [SavedListItemViewModel] {} var loadMoreViewModel: FetchMoreViewModel? {} // MARK: Loading var isLoading: Bool {} var isPerformingAction: Bool {} // MARK: Actions func present(details: PropertyDTO) {...} func itemLeftButtonAction(itemViewModel: SavedListItemViewModel) {...} func itemRightButtonAction(itemViewModel: SavedListItemViewModel) {...} // MARK: Networking func reloadItems() {} func loadMoreItems() {} } @SzklarekMateusz RSConf 2019 32
  15. class EventsViewModel { var lastEvent: Observable<Event?> var buttonsState: Observable<ButtonsState> var

    tableViewState: Observable<AppContentState> var isReminderAllowed: Bool var isEventOutdated: Bool var attendanceState: Observable<AttendanceState> var notificationState: Observable<NotificationState> var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var eventsListRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var previousEvents: Observable<[Event?]?> var eventDetailsRefresh: Observable<Void>() var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var notificationPermissionsNotGranted: Observable<Void>() weak var delegate: EventsControllerDelegate? } @SzklarekMateusz RSConf 2019 33
  16. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  17. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  18. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  19. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  20. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  21. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  22. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  23. class EventsViewModel { var buttonsState: Observable<ButtonsState> var tableViewState: Observable<AppContentState> var

    attendanceState: Observable<AttendanceState> var isReminderAllowed: Bool var isEventOutdated: Bool var facebookAlert: Observable<String?> var loginScreen: Observable<Void> var summaryCellDidTap: Observable<Void> var locationCellDidTap: Observable<Void> var lectureCellDidTap: Observable<Int> var speakerCellDidTap: Observable<Int> var eventsListRefresh: Observable<Void> var eventDetailsRefresh: Observable<Void> var previousEventsCellDidSet: Observable<Void> var previousEvents: Observable<[Event?]?> var previousEventsViewModel: Observable<PreviousEventsListTableViewCellViewModel?> var carouselEventPhotosViewModel: Observable<CarouselEventPhotosTableViewCellViewModel?> var lastEvent: Observable<Event?> weak var delegate: EventsControllerDelegate? var notificationState: Observable<NotificationState> var notificationPermissionsNotGranted: Observable<Void> } @SzklarekMateusz RSConf 2019 35
  24. class SavedListViewModel { var title: String // MARK: Child view

    models var itemViewModels: [SavedListItemViewModel] {} var loadMoreViewModel: FetchMoreViewModel? {} // MARK: Loading var isLoading: Bool {} var isPerformingAction: Bool {} // MARK: Actions func present(details: PropertyDTO) {...} func itemLeftButtonAction(itemViewModel: SavedListItemViewModel) {...} func itemRightButtonAction(itemViewModel: SavedListItemViewModel) {...} // MARK: Networking func reloadItems() {} func loadMoreItems() {} } @SzklarekMateusz RSConf 2019 39
  25. Implementation (numbers) 4 ViewController - 115 lines 4 bindings 190

    lines 4 ViewModel 280 lines @SzklarekMateusz RSConf 2019 40
  26. "It's NOT a mistake to make a mistake but it

    is a mistake to repeat a mistake." @SzklarekMateusz RSConf 2019 41
  27. View model is a model for a view, isn't it?

    @SzklarekMateusz RSConf 2019 43
  28. View model is a “model for the view”. This is

    a dumb object (usually a struct in Swift) that is passed to a view to populate its subviews. Soroush Khanlou @SzklarekMateusz RSConf 2019 45
  29. struct SavedListViewModel { let title: String let itemViewModels: [SavedListItemViewModel] let

    loadMoreViewModel: FetchMoreViewModel? let isLoading: Bool {} let isPerformingAction: Bool {} } @SzklarekMateusz RSConf 2019 46
  30. struct ActivityViewModel { let id: Int let time: String let

    title: NSAttributedString let icon: UIImage let details: [ActivityDetailViewModel] let isSeparatorVisible: Bool } @SzklarekMateusz RSConf 2019 47
  31. struct SavedListViewModel { let title: String let itemViewModels: [SavedListItemViewModel] let

    loadMoreViewModel: FetchMoreViewModel? let isLoading: Bool {} let isPerformingAction: Bool {} } @SzklarekMateusz RSConf 2019 48
  32. // MARK: Actions func present(details: PropertyDTO) {...} func itemLeftButtonAction(itemViewModel: SavedListItemViewModel)

    {...} func itemRightButtonAction(itemViewModel: SavedListItemViewModel) {...} func reloadItems() {} func loadMoreItems() {} @SzklarekMateusz RSConf 2019 50
  33. Real life example app 4 235k lines of code (85k

    production code & 150k tests) @SzklarekMateusz RSConf 2019 57
  34. Real life example app 4 235k lines of code (85k

    production code & 150k tests) 4 6092 swift files @SzklarekMateusz RSConf 2019 57
  35. Real life example app 4 235k lines of code (85k

    production code & 150k tests) 4 6092 swift files 4 8200 unit tests @SzklarekMateusz RSConf 2019 57
  36. Real life example app 4 235k lines of code (85k

    production code & 150k tests) 4 6092 swift files 4 8200 unit tests 4 204 UIViewControllers @SzklarekMateusz RSConf 2019 57
  37. Real life example app 4 235k lines of code (85k

    production code & 150k tests) 4 6092 swift files 4 8200 unit tests 4 204 UIViewControllers 4 average file length 37.23 lines @SzklarekMateusz RSConf 2019 57
  38. Real life example app 4 235k lines of code (85k

    production code & 150k tests) 4 6092 swift files 4 8200 unit tests 4 204 UIViewControllers 4 average file length 37.23 lines 4 code coverage 99.2% @SzklarekMateusz RSConf 2019 57
  39. Real life example app 4 235k lines of code (85k

    production code & 150k tests) 4 6092 swift files 4 8200 unit tests 4 204 UIViewControllers 4 average file length 37.23 lines 4 code coverage 99.2% 4 file > 150 lines does not compile (only 88 files > 100 lines) @SzklarekMateusz RSConf 2019 57
  40. Split a view model 4 Services for network calls 4

    Data providers @SzklarekMateusz RSConf 2019 60
  41. Split a view model 4 Services for network calls 4

    Data providers 4 Notification handlers @SzklarekMateusz RSConf 2019 60
  42. Split a view model 4 Services for network calls 4

    Data providers 4 Notification handlers 4 Database repositories @SzklarekMateusz RSConf 2019 60
  43. Split a view model 4 Services for network calls 4

    Data providers 4 Notification handlers 4 Database repositories 4 and many child controllers @SzklarekMateusz RSConf 2019 60
  44. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let

    item = items[indexPath.row] let controller = DetailViewController(item: item) navigationController?.pushViewController(controller, animated: true) } @SzklarekMateusz RSConf 2019 63
  45. public class ItemViewModel { let id: Int let title: String

    var buttonTapped: (() -> Void)? func presentAlert(on controller: UIViewController) { let alert = UIAlertController(title: "Confirmation", message: "Do you want to buy \(title)?", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) alert.addAction(UIAlertAction(title: "YES", style: .default, handler: { [weak self] in self?.buyProduct(id: id) })) controller.present(alert, animated: true, completion: nil) } func buyProduct(id: Int) { // TODO: Network request } } @SzklarekMateusz RSConf 2019 64
  46. public class DashboardController: UIViewController { public init(childrenFactory: DashboardChildrenFactoryProtocol) { self.childrenFactory

    = childrenFactory super.init(nibName: nil, bundle: nil) } ... // MARK: - Child controllers private lazy var headerController = childrenFactory.makeHeaderController() private lazy var detailsController = childrenFactory.makedetailsController() private lazy var availableItemsController = childrenFactory.makeAvailableItemsController() private lazy var orderController = childrenFactory.makeOrderController() private lazy var showMoreButtonController = childrenFactory.makeReferralButtonController() ... } @SzklarekMateusz RSConf 2019 71
  47. public class DashboardController: UIViewController { ... private var childControllers: [DashboardChildControllerType]

    { return [headerController, detailsController, availableItemsController, orderController, showMoreButtonController] } // MARK: - Lifecycle public override func viewDidLoad() { super.viewDidLoad() embedChildControllers() } // MARK: - Private private let childrenFactory: DashboardChildrenFactoryProtocol private func embedChildControllers() { childControllers.forEach { controller in embed(childViewController: controller, using: dashboardView.stackView.addArrangedSubview) } } } @SzklarekMateusz RSConf 2019 72
  48. "Extracting logic from VC to VM is nothing else than

    moving the Junk to a Different Drawer" @SzklarekMateusz RSConf 2019 77
  49. class ListController: UIViewController, UITableViewDataSource { ... // MARK: UITableViewDataSource override

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = viewModel[indexPath.row] let cell = tableView.dequeueReusableCell(SomeCell.self, forIndexPath: indexPath) cell.model = model return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = viewModel[indexPath.row] openDetails(model: model) } ... } @SzklarekMateusz RSConf 2019 78
  50. extension ListController: UITableViewDataSource { override func tableView(_ tableView: UITableView, cellForRowAt

    indexPath: IndexPath) -> UITableViewCell { let model = viewModel[indexPath.row] let cell = tableView.dequeueReusableCell(SomeCell.self, forIndexPath: indexPath) cell.model = model return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = viewModel[indexPath.row] openDetails(model: model) } } @SzklarekMateusz RSConf 2019 79
  51. 4 John: "We do MVVM because it's really easy to

    test our view models" @SzklarekMateusz RSConf 2019 81
  52. 4 John: "We do MVVM because it's really easy to

    test our view models" 4 Me: Ok, so please show me the tests @SzklarekMateusz RSConf 2019 81
  53. 4 John: "We do MVVM because it's really easy to

    test our view models" 4 Me: Ok, so please show me the tests 4 John: We don't have them :D @SzklarekMateusz RSConf 2019 81
  54. 4 John: "We do MVVM because it's really easy to

    test our view models" 4 Me: Ok, so please show me the tests 4 John: We don't have them :D 4 Me: Why? @SzklarekMateusz RSConf 2019 81
  55. 4 John: "We do MVVM because it's really easy to

    test our view models" 4 Me: Ok, so please show me the tests 4 John: We don't have them :D 4 Me: Why? 4 John: Because logic is quite simple @SzklarekMateusz RSConf 2019 81
  56. 4 John: "We do MVVM because it's really easy to

    test our view models" 4 Me: Ok, so please show me the tests 4 John: We don't have them :D 4 Me: Why? 4 John: Because logic is quite simple 4 Me: But you said that you made view models to extract business logic from your view controllers because it was too complex, am I right? @SzklarekMateusz RSConf 2019 81
  57. 4 John: "We do MVVM because it's really easy to

    test our view models" 4 Me: Ok, so please show me the tests 4 John: We don't have them :D 4 Me: Why? 4 John: Because logic is quite simple 4 Me: But you said that you made view models to extract business logic from your view controllers because it was too complex, am I right? 4 John: Yes, it's true and it is easy to test @SzklarekMateusz RSConf 2019 81
  58. 3. "Because it's so popular and it's a perfect solution

    with RxSwift" @SzklarekMateusz RSConf 2019 84
  59. 4 nice way to keep the state of the view

    (simple struct) @SzklarekMateusz RSConf 2019 87
  60. struct BookViewModel { let id: String let name: String let

    author: String let coverImage: UIImage } @SzklarekMateusz RSConf 2019 88
  61. Break SRP 4 Loading data from multiple APIs / endpoints

    4 Filtering data @SzklarekMateusz RSConf 2019 90
  62. Break SRP 4 Loading data from multiple APIs / endpoints

    4 Filtering data 4 logic for formatting strings (composed from several other values) @SzklarekMateusz RSConf 2019 90
  63. Break SRP 4 Loading data from multiple APIs / endpoints

    4 Filtering data 4 logic for formatting strings (composed from several other values) 4 Being data sources and delegates for UITableViews / UICollectionViews @SzklarekMateusz RSConf 2019 90
  64. Break SRP 4 Loading data from multiple APIs / endpoints

    4 Filtering data 4 logic for formatting strings (composed from several other values) 4 Being data sources and delegates for UITableViews / UICollectionViews 4 Saving or persisting data @SzklarekMateusz RSConf 2019 90
  65. 4 Break SRP 4 Poor reusability 4 Poor testability 4

    without bindings it's still more code to write @SzklarekMateusz RSConf 2019 93
  66. 3 things to remember 4 architecture & design patterns are

    to solve problem not to create them @SzklarekMateusz RSConf 2019 94
  67. 3 things to remember 4 architecture & design patterns are

    to solve problem not to create them 4 don't put network calls, logic and different actions to your view model - use your view model as a simple struct @SzklarekMateusz RSConf 2019 94
  68. 3 things to remember 4 architecture & design patterns are

    to solve problem not to create them 4 don't put network calls, logic and different actions to your view model - use your view model as a simple struct 4 don't repeat mistakes from your previous projects @SzklarekMateusz RSConf 2019 94