Я начинающий 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/
Добавить комментарий