#MBLTdev: Опыт использования MVVM в реальных проектах

  • View
    2.343

  • Download
    1

  • Category

    Mobile

Preview:

DESCRIPTION

#MBLTdev: Конференция мобильных разработчиков Спикер: Юрий Буянов Разработчик мобильных приложений, Одноклассники http://mbltdev.ru/

Citation preview

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

Спасибо