#mbltdev: Опыт использования mvvm в реальных проектах
DESCRIPTION
#MBLTdev: Конференция мобильных разработчиков Спикер: Юрий Буянов Разработчик мобильных приложений, Одноклассники http://mbltdev.ru/TRANSCRIPT
MVVM в реальных проектах
Юрий Буянов “Одноклассники”
MVVM
MVC
“Massive View Controller”
• Занимается вообще всем
• Сильная связанность с View layer
• Трудно тестировать
• Трудно переиспользовать
Model-View-ViewModel
ViewModel
Model
View(+ViewController) Lightweight View
UI logic
Business logic
Model-View-ViewModel
ViewModel
Model
View(+ViewController) Lightweight View
UI logic
Business logic
Bindings!
ViewModel• Не знает ничего про ViewController
• Не использует UIKit
• Отображает (?) состояние UI с помощью KVO-совместимых свойств или RAC-сигналов
• Действия представлены в виде методов или RAC-команд
• Легко тестируема (и должна тестироваться)
View (ViewController)
• Декларативно привязывает состояние UI к состоянию VM
• Обрабатывает чистую логику UI (вёрстка, анимации, ориентирование)
• Может зависеть от нескольких ViewModel
Биндинги
- (void)viewDidLoad { [super viewDidLoad];
RAC(self.trackTitleLabel, text) = RACObserve(self.viewModel, trackTitle);
RAC(self.playBtn, enabled) = RACObserve(self.viewModel, loading).not; }
- (IBAction)nextBtnTap:(id)sender { [self.viewModel nextBtnPressed]; }
View
UI customization layout
animations orientation changes
ViewModel
Model interaction (network, storage)
Model-related UI state
???
Navigation Image Loading
Action Confirmations UI-only actions
…
Навигация
Ad hocUINavigationController
Sections Controller
Talks Controller
ctrl = [[TalksController alloc] init]
push2
1 инициализация
@implementation SectionsController
- (IBAction)mobileBtnTap:(id)sender { UIViewController *ctrl = [[TalksController alloc] init]; [self.navigationController pushViewController:ctrl animated:YES]; }
@end
@implementation SectionsController
- (IBAction)mobileBtnTap:(id)sender { UIViewController *ctrl = [[TalksController alloc] init]; [self.navigationController pushViewController:ctrl animated:YES]; }
@end
Ненужная связанность (coupling)
UINavigationController
Sections Controller Talks Controller
push4
2
создаёт
Sections ViewModel
Talks ViewModel
действие1
Ad Hoc + MVVM
создаёт
3
@implementation SectionsViewModel
- (void)mobileBtnPressed { MobileTalksViewModel *vm = [[MobileTalksViewModel alloc] init];
UIViewController *ctrl = [[TalksController alloc] initWithVM:vm];
[self.navigationController pushViewController:ctrl animated:YES]; }
@end
@implementation SectionsViewModel
- (void)mobileBtnPressed { MobileTalksViewModel *vm = [[MobileTalksViewModel alloc] init];
UIViewController *ctrl = [[TalksController alloc] initWithVM:vm];
[self.navigationController pushViewController:ctrl animated:YES]; }
@end
Ненужная связанность
UI-код внутри ViewModel! :(
UINavigationController
Sections Controller
2 создаётSections ViewModel
Talks ViewModel
действие1
Решение “В лоб”
UINavigationController
Sections Controller
2 создаётSections ViewModel
Talks ViewModel
действие1
Решение “В лоб”
3 сигнал
UINavigationController
Sections Controller Talks Controller
2 создаётSections ViewModel
Talks ViewModel
действие1
Решение “В лоб”
3 сигнал
4 создаёт и связывает
UINavigationController
Sections Controller Talks Controller
push5
2 создаётSections ViewModel
Talks ViewModel
действие1
Решение “В лоб”
3 сигнал
4 создаёт и связывает
@implementation SectionsViewModel
- (void)mobileBtnPressed { MobileTalksViewModel *vm = [[MobileTalksViewModel alloc] init]; [_talksViewModelsSubject sendNext:vm]; }
@end
@implementation SectionsController
- (IBAction)mobileBtnTap:(id)sender { [self.viewModel mobileBtnPressed]; }
- (void)viewDidLoad {
[self.viewModel.talksViewModelsSignal subscribeNext:^(id viewModel) {
UIViewController* ctrl = [[TalksController alloc] initWithVM:viewModel]; [self.navigationController pushViewController:ctrl animated:YES];
}]; }
@end
Решение “В лоб”
• Большая степень связанности в VM и контроллере
• Негибкость
• Повторение кода
• Отдельный сигнал для каждого “маршрута”
Роутер (Navigation Object)
UINavigationController
Sections Controller
Sections ViewModel
action showTalks1 2
MVVM + Роутер
UINavigationController
Sections Controller
showDst
MVC + Роутер
Роутер
• Имеет доступ к навигационным контроллерам (navigation controller, tab controller, drawer controller, menu controller)
• Создаёт и конфигурирует экземпляры VM и контроллеров
• (сам или используя DI-контейнер)
• Осуществляет переходы между экранами
• Mockable
• Отлично работает в рамках MVVM и MVC
Роутер@interface Router : NSObject
- (void)showMobileTalks;
- (void)showWebTalks;
- (void)showGamesTalks;
- (void)showTalk:(Talk*)talk;
@end
Роутер
@implementation Router
- (void)showMobileTalks { MobileTalksViewModel *vm = [[MobileTalksViewModel alloc] init]; UIViewController *ctrl = [[TalksController alloc] initWithVM:vm]; [self.mainNavigationController pushViewController:ctrl animated:YES]; }
@end
@implementation SectionsController
- (IBAction)mobileBtnTap:(id)sender { [self.viewModel mobileBtnPressed]; }
- (void)viewDidLoad {
[self.viewModel.talksViewModelsSignal subscribeNext:^(id viewModel) {
UIViewController* ctrl = [[TalksController alloc] initWithVM:viewModel]; [self.navigationController pushViewController:ctrl animated:YES];
}]; }
@end
@implementation SectionsViewModel
- (void)mobileBtnPressed { [self.router showMobileTalks]; }
@end
Вложенные контроллеры
UINavigationController
Controller
Child controller
Some other controller
ViewModel
Child ViewModel
Some other ViewModel
Вложенные контроллеры
UINavigationController
Controller
Child controller
Some other controller
ViewModel
Child ViewModel
Some other ViewModel
Универсальные приложения
[self.router showMobileTalks];
[self.router open:@"/sections/mobile" modal:NO];
Частный случай: URL-based роутер
URL-based роутер• Легко настраивается
• Упрощает поддержку URL-схем
• Упрощает переход от веб-приложений к нативным
• Логика навигации может “утекать” в VM/контроллер
• Сложности с передачей сложных объектов
• Подходит не для всех приложений
Списки (UICollectionView, UITableView)
Модель ячейкиViewController ViewModel
- (void)setViewModel:(MYCellViewModel *)viewModel
UICollectionView/UITableView
Cell
CellViewModel
CellViewModel
CellViewModel
CellViewModel
- (void)viewDidLoad { [super viewDidLoad]; [self.viewModel.updatedContentSignal subscribeNext:^(id x) { [self.collectionView reloadData]; }]; }
Cell
Cell
@property RACSubject *updatedContentSignal;
NSArray *cellViewModels
Статическая модель ячейки
Cell
Cell ViewModel
- (void)setViewModel:(OKPTrackCellViewModel *)viewModel { NSParameterAssert(viewModel); self.titleLabel.text = viewModel.title; self.subTitleLabel.text = viewModel.subtitle; self.someIcon.hidden = !viewModel.someFlag; }
@property NSString* title;
@property NSString* subtitle;
@property BOOL someFlag;
Динамическая модель ячейки
Cell
Cell ViewModel
- (void)setViewModel:(OKPTrackCellViewModel *)viewModel { NSParameterAssert(viewModel); self.titleLabel.text = viewModel.title; self.subTitleLabel.text = viewModel.subtitle; self.someIcon.hidden = !viewModel.someFlag; }
@property NSString* title;
@property NSString* subtitle;
@property BOOL someFlag; //Может изменяться!
Динамическая модель ячейки
Cell
RAC(self.someIcon, hidden) = RACObserve(viewModel.someFlag).not;
Warning: cell reuseCell
RAC(self.someIcon, hidden) = RACObserve(viewModel.someFlag).not;
/// A given key on an object should only have one active signal bound to it at any given time. Binding more than one signal to the same property is considered undefined behavior.
Warning: cell reuseCell
RAC(self.someIcon, hidden) =[RACObserve(viewModel.someFlag).not takeUntil:self.rac_prepareForReuseSignal];
/// Solution: unsubscribe from RACObserve signal on cell reuse
Ощущения (на данном этапе)
• Технология “на вырост”
• Сделает возможным написание тестов, но не напишет их за вас
• Хинт: используйте Dependency Injection.
• Вам придётся полюбить ReactiveCocoa
Спасибо