Nawigacja w Vaadin z użyciem Guice

I kolejny temat około pracowy mi wyszedł. Zaczęło się od tweeta:

I generalnie tu pojawił się pomysł na wpis.

Problem

Chcemy by nasze View w Vaadin mogły być tworzone w taki sposób by Guice mógł wstrzyknąć im np. serwisy. Najprościej jest to zrobić w następujący sposób:

Listing 1. Wykorzystanie zdarzeń

public class NavigationProvider implements Provider<Navigator> {

	@Inject
	private MyUI ui;

	@Inject
	private ViewRegister viewRegister;

	@Inject
	private Provider<Injector> injector;

	@Override
	public Navigator get() {
		Navigator nav = new Navigator(ui, ui);
		viewRegister.initNavigation(nav);
		nav.addViewChangeListener(new ViewChangeListener() {
			@Override
			public boolean beforeViewChange(ViewChangeEvent event) {
				injector.get().injectMembers(event.getNewView());
				return true;
			}

			@Override
			public void afterViewChange(ViewChangeEvent event) {

			}
		});
		return nav;
	}
}

Mamy tu trzy zasadnicze elementy. Po zerowe obiekty Navigation są dostarczane providerem do głównego UI. Po pierwsze wstrzykujemy UI po utworzeniu providera (i wstrzyknięciu go do UI). Zrobienie tego w konstruktorze jest OK, ale jakieś takie niekoszerne i wymaga kombinowania.
Po drugie potrzebujemy instancji injectora i wstrzykujemy go przez providera.
Po trzecie po utworzeniu Navigation dodajemy do niego listener, w którym mając już utworzony nowy widok wstrzykujemy w niego potrzebne składowe. ViewRegister to już ciekawostka, do budowania „zagłębień” w menu.

Wadą tego rozwiązania jest brak możliwości tworzenia widoków wykorzystujących wstrzykiwanie przez konstruktor. Wynika to z faktu, że w bebechach Vaadin wykorzystywany jest ClassBasedViewProvider, który poprzez refleksję tworzy nowe widoki. By to działało narzuca on ograniczenie z bezparametrowym konstruktorem.

To czy wykorzystywać wstrzykiwanie przez konstruktor czy też przez pola to problem na osobą dyskusję. W każdym razie ja trafiłem na sytuację gdy wstrzykiwanie przez konstruktor okazało się zdroworozsądkowe.

Rozwiązanie

Kto śledzi bloga ten zapewne kojarzy mechanizm Assisted Injection obecny w Guice. Kliknijcie poczytajcie. Do rozwiązania naszego problemu wykorzystam właśnie ten mechanizm.

Własny Navigation

W sumie nie własny, bo rozszerzający ten z Vaadin, ale zawsze:

Listing 2. Ta klasa ma straszną nazwę…

public class GuicefiedNavigator extends Navigator {

	private GuiceViewProviderFactory guiceViewProviderFactory;

	@Inject
	public GuicefiedNavigator(GuiceViewProviderFactory guiceViewProviderFactory,
	                          @Assisted UI ui, @Assisted SingleComponentContainer container) {
		super(ui, container);
		this.guiceViewProviderFactory = guiceViewProviderFactory;
	}

	@Override
	public void addView(String viewName, Class viewClass) {
		Preconditions.checkNotNull(viewName, "view and viewClass must be non-null");
		Preconditions.checkNotNull(viewClass, "view and viewClass must be non-null");
		removeView(viewName);
		addProvider(guiceViewProviderFactory.create(viewName, viewClass));
	}

}

Jeden konstruktor, który akurat potrzebuję (bonus AI – można mieć wiele konstruktorów oznaczonych @Inject) i który jest zmapowany w odpowiedniej fabryce:

Listing 3. Ta nazwa jest bardzo javowa

public interface GuicefiedNavigatorFactory {

	GuicefiedNavigator create(UI ui, SingleComponentContainer container);
}

nadpisujemy tu jedną metodę. Służy ona to dodawania widoków i ich providerów do mechanizmu nawigacji. W oryginale jest to wykorzystana jak wspomniałem klasa ClassBasedViewProvider. Ja zastąpiłem ją za pomocą GuiceViewProviderFactory

I jeszcze GuiceViewProvider

Skoro jest factory to mamy tu kanapkę z podwójnym mięskiem, czyli kolejny AI na pokładzie.

Listing 4. Pracuję nad nazwami

public class GuiceViewProvider implements ViewProvider {

	private Provider<Injector> injectorProvider;

	private String viewName;

	private Class viewClass;

	@Inject
	public GuiceViewProvider(Provider<Injector> injectorProvider,
	                         @Assisted String viewName, @Assisted Class viewClass) {
		this.injectorProvider = injectorProvider;
		this.viewName = checkNotNull(viewName);
		this.viewClass = checkNotNull(viewClass);
	}

	@Override
	public String getViewName(String navigationState) {
		if (null == navigationState) {
			return null;
		}
		if (viewName.equals(navigationState)
				|| navigationState.startsWith(viewName + "/")) {
			return viewName;
		}
		return null;
	}

	@Override
	public View getView(String viewName) {
		if (this.viewName.equals(viewName)) {
			return injectorProvider.get().getInstance(viewClass);
		}
		return null;
	}
}

Tu magii nie ma (jest za to factory). Wstrzykujemy injector, z którego później pobieramy nowe instancje widoków.

Konfiguracja

Ostatnim krokiem w tej zabawie jest skonfigurowanie tego wszystkiego by śmigało jak należy dodać odpowiedni wpis w konfiguracji modułów.

Listing 5. Guice 3.0 w akcji w środowisku 4.0

install(new FactoryModuleBuilder().
		implement(GuicefiedNavigator.class, GuicefiedNavigator.class)
		.build(GuicefiedNavigatorFactory.class));                    
install(new FactoryModuleBuilder().                                  
		implement(GuiceViewProvider.class, GuiceViewProvider.class)  
		.build(GuiceViewProviderFactory.class));

Podsumowanie

Są dwie pułapki w tym całym rozwiązaniu. Po pierwsze kwestia sprawdzenia nazw w GuiceViewProvider musi spełniać kontrakt z interfejsu. Jednak nie ma tam słowa o tym slashu. Po drugie należy pamiętać, że wszystkie widoki musimy bindować jako bez scopa, bo domyślny singleton zrobi nam sieczkę z aplikacji (nie wiem jak to działa, bo z obserwacji wynika duża losowość).

Jeżeli spodobał ci się ten wpis to podziel się nim ze znajomymi korzystając z przycisków poniżej. Do tego one służą.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax