GWT-Platform основы работы с презентерами

от автора

Всем хабражителям доброго времени суток!

Я начинающий Java-программист и так уж получилось, что свою карьеру я начинаю с разработки серьезного приложения на GWT. На хабре довольно много статей на тему GWT, однако почему-то совсем нет информации о замечательном фреймворке GWT-Platform. Подробно познакомиться с данным фреймворком можно тут, а я расскажу вкратце об основах работы на примере простого приложения.

Наше приложение будет содержать в себе навигационную панель, на которой расположены кнопки для переключения текущего вида. А также две колонки, в которые мы будем вставлять нужный контент в зависимости от ситуации. Две колонки я сделал для наглядности. В реальной жизни понадобилась бы одна конечно.

Если нажать на кнопку в навбаре, то откроется либо левая часть приложения, либо правая с бессмысленным текстом.

По умолчанию открывается левая колонка, на которой находится кнопка вызова диалогового окна для ввода имени и фамилии. После нажатия кнопки подтверждения эти данные передаются в правую колонку и отображаются.




Итак, для начала нам нужно создать GWT проект в IDE. Для работы с GWTP нам потребуется добавить в проект библиотеки: guice-2.0.jar, guice-3.0.jar, gwtp-all-1.0.jar, aopalliance.jar, guice-assistedinject-3.0.jar. Также я добавил gwt-bootstrap-2.2.2.0-SNAPSHOT.jar чтобы добавить “красоты” приложению.

Можно установить в Eclipse gwt-platform плагин. Он сильно облегчает жизнь. С его помощью можно создавать как новые проекты, так и связки презентер-вью. Качается по этой ссылке: plugin.gwt-platform.googlecode.com/hg/update

Приступим:
Нужно создать клиентский модуль и Ginjector. Если приложение создавать с помощью плагина, то они будут созданы автоматически:
В методе configure () мы будем биндить наши презентеры с интерфейсами и имплементацией их вью.

public class ClientModule extends AbstractPresenterModule {  	@Override 	protected void configure() { 		install(new DefaultModule(ClientPlaceManager.class));                            bindPresenter(MainPagePresenter.class, MainPagePresenter.MyView.class, 				MainPageView.class, MainPagePresenter.MyProxy.class);  		bindConstant().annotatedWith(DefaultPlace.class).to(NameTokens.main); 	} } 
         @GinModules({ DispatchAsyncModule.class, ClientModule.class })          public interface ClientGinjector extends Ginjector {  	EventBus getEventBus();  	PlaceManager getPlaceManager();  	Provider<MainPagePresenter> getMainPagePresenter(); } 

Далее наша точка входа: Тут мы говорим нашему placemanager перейти на текущую страницу (place). То есть если у нас в адресной строке браузера был введен какой-то токен, определяющий необходимый place, то мы попадем туда. Конечно при условии что мы имеем доступ. (За это может отвечать например GateKeeper).

public class HabraTest implements EntryPoint {  	private final ClientGinjector ginjector = GWT.create(ClientGinjector.class);  	@Override 	public void onModuleLoad() { 		DelayedBindRegistry.bind(ginjector);	 		ginjector.getPlaceManager().revealCurrentPlace(); 	} } 

Не буду заострять внимание на работе с place. На хабре уже было много замечательных статей по GWT. Например эта.

Я же покажу как можно создавать небольшие GWT приложения без использования place (точнее с одним place).

Для начала создадим главный презентер нашего приложения:

public class MainPagePresenter extends 	Presenter<MainPagePresenter.MyView, MainPagePresenter.MyProxy> implements MainPageUiHandlers, FirstPageEvent.Handler{  	public interface MyView extends View, HasUiHandlers<MainPageUiHandlers> { 	} 	 	// идентификаторы слотов для вставки соответствующего презентера 	public final static Object SLOT_FIRST_PAGE =  new Object(); 	public final static Object SLOT_SECOND_PAGE =  new Object(); 	 	//вложенные  презентеры 	private FirstPagePresenter firstPagePresenter; 	private SecondPagePresenter secondPagePresenter;	  	@ProxyStandard 	@NameToken(NameTokens.main) 	public interface MyProxy extends ProxyPlace<MainPagePresenter> { 	}  	private EventBus eventBus; 	private final PlaceManager placeManager;  	// инжектим вложенные презентеры 	@Inject 	public MainPagePresenter(final EventBus eventBus, final MyView view,  			FirstPagePresenter firstPagePresenter, 			SecondPagePresenter secondPagePresenter, 			final MyProxy proxy, final PlaceManager placeManager) { 		super(eventBus, view, proxy); 		this.placeManager = placeManager; 		this.firstPagePresenter =  firstPagePresenter; 		this.secondPagePresenter =  secondPagePresenter; 		this.eventBus =  eventBus; 		// назначаем себя обработчиком событий  вью 		getView().setUiHandlers(this); 		eventBus.addHandler(FirstPageEvent.getType(), this); 	}  	// внедряем себя в главный презентер приложения 	@Override 	protected void revealInParent() { 		RevealRootContentEvent.fire(this, this); 	}  	@Override 	protected void onBind() { 		super.onBind(); 		// по умолчанию  будет загружена первая страница 		getView().setInSlot(SLOT_FIRST_PAGE, firstPagePresenter);	 	}  	// вызывается при нажатии левой кнопки в MainPageView 	@Override 	public void onRightBtnClicked() { 		showRightContent(); 		MainPageEvent mainPageEvent =  new MainPageEvent( MainPageEvent.Action.SHOW_LOREM_IPSUM); 		eventBus.fireEvent(mainPageEvent);	 	} 	// аналогично при нажатии правой 	@Override 	public void onLeftBtnClicked() { 		showLeftContent(); 		 	}	 	 	public void showLeftContent() { 		removeFromSlot(SLOT_SECOND_PAGE, secondPagePresenter); 		getView().setInSlot(SLOT_FIRST_PAGE, firstPagePresenter);		 	}  	public void showRightContent() { 		removeFromSlot(SLOT_FIRST_PAGE, firstPagePresenter); 		getView().setInSlot(SLOT_SECOND_PAGE, secondPagePresenter);	 	}  	@Override 	public void onFirstPageEvent(FirstPageEvent event) { 		// закрываем левый контент и открываем правый, в который через эвент передаем имя и фамилию 		showRightContent(); 		MainPageEvent mainPageEvent =  new MainPageEvent( MainPageEvent.Action.SHOW_FORM_RESULT, event.getFirstName(),                          event.getLastName());     		eventBus.fireEvent(mainPageEvent); 	 	}	 } 

Обратите внимание на то что мы заинжектили в конструкторе FirstPagePresenter firstPagePresenter, SecondPagePresenter secondPagePresenter.
Это будут presenter — виджеты представляющие левую и правую часть приложения (то есть в теории отдельные страницы);

В GWTP есть три основных типа презентеров:

  • Перезентеры, которые являются еще и place
  • Перезентеры-виджеты(PresenterWidget)
  • Презентеры-виджеты, представляющие собой Popup окно

Presenter-place испульзуются для создания отдельных страниц приложения, для навигации по которым можно использовать так называемые токены, добавленые в адрессной строке браузера.
Каждый такой презентер должен содержать аннотацию, которая указывает какой токен привязан к презентеру.
В нашем случае мы используем только один такой презентер.

Для смены «страниц» мы будем использовать систему слотов и презентер-виджеты помещенные в слоты.
Презентер-виджет это презентер который не обязательно является синглтоном. Он может имет множество независимых instance.
Благодаря системе слотов мы можем бесконечно вкладывать презентеры внутри других презентеров. Чтобы поместить презентер-виджет в другой презентер, нам необходимо определить слоты в родительском презентере и переопределить метод setInSlot() во вью родительского презентера.

В классе MainPagePresenter видно что слот это просто Object:

	public final static Object SLOT_FIRST_PAGE =  new Object(); 	public final static Object SLOT_SECOND_PAGE =  new Object(); 

В соответствующем вью определяем правила вставки презентеров в слот:

public class MainPageView extends ViewWithUiHandlers<MainPageUiHandlers> implements MainPagePresenter.MyView {  	// главная панель приложения 	@UiField HTMLPanel main; 	// навигационная панель 	@UiField ResponsiveNavbar navbar;  	// кнопки навигации 	@UiField Button firstPageBtn, secondPageBtn; 		 	private static MainPageViewUiBinder uiBinder = GWT 			.create(MainPageViewUiBinder.class);  	interface MainPageViewUiBinder extends UiBinder<Widget, MainPageView> { 	}  	// колонки для вставки контента  	@UiField Column leftColumn, rightColumn;  	@Inject 	public MainPageView() {	 		uiBinder.createAndBindUi(this); 		navbar.setInverse(true); 		//обработчики для кнопок  		firstPageBtn.addClickHandler(new  ClickHandler() {		 			@Override 			public void onClick(ClickEvent event) { 				getUiHandlers().onLeftBtnClicked();		 			} 		}); 		 		secondPageBtn.addClickHandler(new  ClickHandler() {		 			@Override 			public void onClick(ClickEvent event) { 				getUiHandlers().onRightBtnClicked();		 			} 		});			 	}  	@Override 	public Widget asWidget() { 		return main; 	} 	 	// переопределяем метод вставки презентеров в слот 	@Override 	public void setInSlot(Object slot, IsWidget content) { 		if(slot == MainPagePresenter.SLOT_FIRST_PAGE ) { 			leftColumn.add(content); 		} 		else if(slot == MainPagePresenter.SLOT_SECOND_PAGE ){ 			rightColumn.add(content); 		} 		else { 			super.setInSlot(slot, content); 		}		 	} 	 	// аналогично переопределяем метод удаления из слота 	@Override 	public void removeFromSlot(Object slot, IsWidget content) { 		if(slot == MainPagePresenter.SLOT_FIRST_PAGE ) { 			leftColumn.remove(content); 		} 		else if(slot == MainPagePresenter.SLOT_SECOND_PAGE ){ 			rightColumn.remove(content); 		} 		else { 			super.removeFromSlot(slot, content); 		}	 	} } 

Все довольно просто: setInSlot() принимает в себя презентер и соответствующий ему слот.
Мы просто указываем в какой виджет поместить этот презентер. В данном случае это две бутстраповские колонки leftColumn и rightColumn.
Хотя я повторюсь в данном случае уместней было бы помещать все в одну колонку, чтобы имитировать переход по страницам.

Далее наши презентер-виджеты и их вью:

public class FirstPagePresenter extends 		PresenterWidget<FirstPagePresenter.MyView> implements FirstPageUiHandlers, PopupEvent.Handler{  	public interface MyView extends View, HasUiHandlers<FirstPageUiHandlers> { 	}  	// попап с формой 	FirstPagePopupPresenter firstPagePopupPresenter; 	EventBus eventBus; 	 	@Inject 	public FirstPagePresenter(final EventBus eventBus, final MyView view, 			FirstPagePopupPresenter firstPagePopupPresenter ) { 		super(eventBus, view); 		this.firstPagePopupPresenter =  firstPagePopupPresenter; 		this.eventBus =  eventBus; 		getView().setUiHandlers(this); 		// назначаем себя хендлером PopupEvent 		eventBus.addHandler(PopupEvent.getType(), this); 	}  	@Override 	public void onShowFormBtnClicked() { 		// показываем всплывающее окно с формой 		showForm(true);		 	}  	private void showForm(boolean show) { 		if(show){ 		addToPopupSlot(firstPagePopupPresenter, true); 		firstPagePopupPresenter.getView().show(); 		} 		else { 			removeFromPopupSlot(firstPagePopupPresenter); 		}		 	}  	@Override 	public void onPopupEvent(PopupEvent event) { 		showForm(false); 		eventBus.fireEvent(new FirstPageEvent(event.getFirstName(), event.getLastName()));		 	} } 

Обратите внимание что я заинжектил некий FirstPagePopupPresenter firstPagePopupPresenter.(код будет ниже). Это наше всплывающее окно с формой. Аналогично можно инжектить любые презентер-виджеты в любом количестве и с любой вложенностью. Главное не нарушать иерархию.

public class FirstPageView extends ViewWithUiHandlers<FirstPageUiHandlers> implements 		FirstPagePresenter.MyView {  	private final Widget widget; 	@UiField Button showFormBtn;  	public interface Binder extends UiBinder<Widget, FirstPageView> { 	}  	@Inject 	public FirstPageView(final Binder binder) { 		widget = binder.createAndBindUi(this); 		 		showFormBtn.addClickHandler(new ClickHandler() {			 			@Override 			public void onClick(ClickEvent event) { 				getUiHandlers().onShowFormBtnClicked();				 			} 		}); 	}  	@Override 	public Widget asWidget() { 		return widget; 	} } 

Во вью особо ничего интересного, кроме того, что оно наследует типизированный класс ViewWithUiHandlers.
Так как мы не хотим нарушать принципов MVP, то и не можем обращаться к презентеру напрямую из вью( наоборот можем). Для этого мы используем интерфейсы. О нажатии кнопки мы сообщаем с помощью getUiHandlers().onShowFormBtnClicked();

public interface FirstPageUiHandlers extends UiHandlers{  	void onShowFormBtnClicked(); } 

getUiHandlers() возвращает нам интерфейс FirstPageUiHandlers, в котором мы указываем методы, которые должны быть реализованы в соответствующем презентере. Естественно что презентер должен имплементировать данный интерфейс, а вложенный в него интерфейс MyView должен наследовать типизированный интерфейс HasUiHandlers. И главное не забыть в презентере назначить себя обработчиком для событий вью — getView().setUiHandlers(this);

Далее презентер и соответствующий вью второй страницы:

public class SecondPagePresenter extends 		PresenterWidget<SecondPagePresenter.MyView> implements MainPageEvent.Handler {  	public interface MyView extends View { 		void showLoremIpsum(); 		void showFormInfo(String firstName, String lastName); 	} 	 	EventBus eventBus;  	@Inject 	public SecondPagePresenter(final EventBus eventBus, final MyView view) { 		super(eventBus, view); 		this.eventBus =  eventBus; 		eventBus.addHandler(MainPageEvent.getType(), this); 	} 	 	@Override 	public void onMainPageEvent(MainPageEvent event) { 		switch(event.getAction()) { 		case SHOW_FORM_RESULT: 			showFormInfoWidget(event.getFirstName(), event.getLastName()); 			break; 		case SHOW_LOREM_IPSUM: 			showLoremIpsumWidget(); 			break; 		default: 			break;		 		}		 	}  	private void showLoremIpsumWidget() { 		getView().showLoremIpsum();		 	}  	private void showFormInfoWidget(String firstName, String lastName) { 		getView().showFormInfo( firstName, lastName);		 	} 
public class SecondPageView extends ViewImpl implements 		SecondPagePresenter.MyView {  	private final Widget widget; 	 	@UiField FlowPanel contentPanel;  	public interface Binder extends UiBinder<Widget, SecondPageView> { 	}  	@Inject 	public SecondPageView(final Binder binder) { 		widget = binder.createAndBindUi(this); 	}  	@Override 	public Widget asWidget() { 		return widget; 	}  	@Override 	public void showLoremIpsum() { 		contentPanel.clear(); 		contentPanel.add(new LoremIpsumWidget());				 	}  	@Override 	public void showFormInfo(String firstName, String lastName) { 		contentPanel.clear(); 		contentPanel.add(new FormInfoWidget(firstName, lastName));	 	} } 

Тут особо ничего интересного и нового для разработчика на GWT. Общение между презентерами происходит посредством стандартных эвентов ( GwtEvent ).

И наконец попап с формой:

public class FirstPagePopupPresenter extends 		PresenterWidget<FirstPagePopupPresenter.MyView> implements PopupUiHandlers {  	public interface MyView extends PopupView , HasUiHandlers<PopupUiHandlers>{ 	}  	EventBus eventBus; 	 	@Inject 	public FirstPagePopupPresenter(final EventBus eventBus, final MyView view) { 		super(eventBus, view); 		this.eventBus =  eventBus; 		getView().setUiHandlers(this); 	}  	@Override 	public void onSubmitBtnClicked(String firstName, String lastName) { 		eventBus.fireEvent(new PopupEvent(firstName, lastName));		 	} } 
public class FirstPagePopupView extends PopupViewWithUiHandlers<PopupUiHandlers> implements 		FirstPagePopupPresenter.MyView {  	@UiField PopupPanel main; 	@UiField Button submitBtn; 	@UiField TextBox firstName, lastName;  	public interface Binder extends UiBinder<Widget, FirstPagePopupView> { 	}  	@Inject 	public FirstPagePopupView(final EventBus eventBus, final Binder binder) { 		super(eventBus); 		binder.createAndBindUi(this);                            main.setAnimationEnabled(true);                            main.setModal(true);                            main.setGlassEnabled(true);                            submitBtn.addClickHandler(new ClickHandler() { 			 			@Override 			public void onClick(ClickEvent event) { 				getUiHandlers().onSubmitBtnClicked(firstName.getValue(), lastName.getValue());				 			} 		}); 	}  	@Override 	public Widget asWidget() { 		return main; 	} } 

Как видно попап тоже является презентер-виджетом, но интерфейс его вью должен наследовать PopupView. И еще главная панель вью должна быть обязательно PopupPanel, ну или наследником данного класса. Еще одно отличие от обычных презентер-виджетов — чтобы показать попап на странице не нужен слот и панель для вставки. Достаточно использовать метод addToPopupSlot();

Также во всех связках презентер-вью использован uibinder. Соответствующие *ui.xml файлы я не выкладываю. Там в принципе ничего для GWT-разработчиков интересного нет.

Сам проект будет доступен некоторое время по этому адресу

И так а сейчас пробежимся по проекту что бы описать что происходит и как связаны презентеры между собой:

При загрузке MainPagePresenter в переопределенном методе onBind() мы сразу же ставим в слот презентер первой страницы:

	@Override 	protected void onBind() { 		super.onBind(); 		getView().setInSlot(SLOT_FIRST_PAGE, firstPagePresenter);	 	} 

(Про жизненный цикл презентеров и методы onBind(), onUnbind, onReveal(), onReset(), onHide() я бы хотел рассказать в следующей статье.)

Соответственно в левой части экрана появляется вью FirstPagePresenter’a. При клике на кнопку мы вызываем имплементацию метода onShowFormBtnClicked() в FirstPagePresenter описанного в интерфейсе FirstPageUiHandlers

Вызов:

		showFormBtn.addClickHandler(new ClickHandler() {			 			@Override 			public void onClick(ClickEvent event) { 				getUiHandlers().onShowFormBtnClicked();				 			} 		}); 

в FirstPagePresenter’ e происходит следующее:

		addToPopupSlot(firstPagePopupPresenter, true); 

Мы сетим презентер попапа в слот. Как я уже упоминал, для попапов слот не нужно определять. Единственное условие, что презентер из которого вызывается попап должен сам находится в слоте родителя и так далее по цепочке. Второй параметр в методе addToPopupSlot() указывает центровать ли попап в окне приложения(метод имеет несколько перегрузок и данный параметр в общем-то необязателен).

После того как попап появляется в окне, мы можем ввести туда какие-то данные и нажать кнопку подтверждения. Далее по аналогичной схеме вью попапа через getUiHandlers() вызывает обработчик в своем презентере, а тот в свою очередь кидает через EventBus эвент, на который подписан FirstPagePresenter (если кому-то интересно, то про эвенты в GWT я бы мог рассказать в следующей заметке):

	@Override 	public void onPopupEvent(PopupEvent event) { 		showForm(false); 		eventBus.fireEvent(new FirstPageEvent(event.getFirstName(), event.getLastName()));		 	} 

Сначала в методе showForm() мы удаляем попап из слота:

removeFromPopupSlot(firstPagePopupPresenter); 

Затем кидаем новый эвент ( теперь это FirstPageEvent) дальше. На него подписан наш MainPagePresenter:

	@Override 	public void onFirstPageEvent(FirstPageEvent event) { 		// закрываем левый контент и открываем правый , в который  через эвент передаем имя и фамилию 		showRightContent(); 		MainPageEvent mainPageEvent =  new MainPageEvent( MainPageEvent.Action.SHOW_FORM_RESULT, event.getFirstName(),     event.getLastName()); 		eventBus.fireEvent(mainPageEvent); 	} 

Получив его MainPagePresenter удаляет из слота первую страницу и вставляет вторую:

	public void showRightContent() { 		removeFromSlot(SLOT_FIRST_PAGE, firstPagePresenter); 		getView().setInSlot(SLOT_SECOND_PAGE, secondPagePresenter);	 	} 

Далее он полылает уже MainPageEvent дальше. В него сетит не только имя и фамилию, но и Action.

Наш SecondPagePresenter получив эвент в методе onMainPageEvent() решает что показать на странице. В данном случае это обычные виджеты без презентеров.

	@Override 	public void onMainPageEvent(MainPageEvent event) { 		switch(event.getAction()) { 		case SHOW_FORM_RESULT: 			showFormInfoWidget(event.getFirstName(), event.getLastName()); 			break; 		case SHOW_LOREM_IPSUM: 			showLoremIpsumWidget(); 			break; 		default: 			break;		 		}		 	} 

Вот собственно и все. Наверное, некоторым может показаться что для таких простых действий слишком много кода, но:

  • Мы не нарушаем принципов MVP — вью ничего не должно знать о своем презентере
  • Разделив приложение на модули код становится reusable и более гибким

Также наверняка некоторые возмутятся — зачем такие длинные цепочки передачи эвентов? Однако можно заметить, что получив эвент, презентер, перед отправкой следующего, делает какие-либо операции. Например, удаляет ненужные более презентеры или обрабатывает как-то полученные данные.

В общем надеюсь данная заметка окажется кому-либо полезной и он обратит свой взор в сторону GWT-Platform.

PS: Прошу прощения за некую сумбурность повествования и возможные ошибки. Это мой первый пост на IT-тематику. Обоснованная критика и советы очень приветствуются.

ссылка на оригинал статьи http://habrahabr.ru/post/192262/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *