GWTでMVPアーキテクチャ その3 GINと愉快な仲間たち
明けましておめでとうございます。相変わらず読みにくい文章ですがよろしくお願い致します。
今年もあまり記事を書かなそうですが、頑張りたいと思います。
さてここ最近はひたすらappengine + gwt しかやっていないわけですが、
年末辺りにやったGWT のMVPアーキテクチャについて書きたいと思います。
毎回のとおり、翻訳能力が低いため、間違っている部分があるかと思いますが、見守っていただけたら幸いです。
ということで前回GWTのActivity + Place について記載しましたが、今回はそれにGWT用のDIコンテナGIN *1 を含めてより簡単にActivity + Placeを実現したいと思います。
GINとは
GINはGWT用に作成されたDIコンテナ*2でGoogle製のDIコンテナ Guiceをもとに作成されています。
Guice自体は設定ファイルでなくJavaのコードで設定を書くDIコンテナとして、appengineでも利用できるなど*3
個人的にはかなり好きなDIコンテナです。
利用方法もほぼほぼGuiceと同じですが、AOPが非サポートなどGWT特有の癖があったりします。
細かな使い方などなどは公式サイトやググッてください。
テンション的に各元気があったらいつか多分書きます。。。
ってことで実装!!
今回は上記のGINを利用して、Activity + Placeを楽にしてみます。 ちなみに参考にしたのはGWTのMLです(ここ)。
なお今回の記事はGWT2.1のMVPが分かっている前提で書くのであしからず。
わからない方は前回の記事を読むことをおすすめします。
まずはDIの設定 Moduleクラス
まずはDIの設定を行います。Guiceでは前述のとおり、Javaで設定を書きます(Moduleクラスといいます)。GINでも同様にAbstractGinModuleを継承した、クラスに設定を記載してきます。
すべての設定を1クラスに記述してしまうと管理がめんどくさいので
Guice(GIN)ではこのModuleクラスを分けることができます。
なのでMVPの設定モジュールを作成します。
package stk.client.inject; ※import public class MvpModule extends AbstractGinModule { @Override protected void configure() { } @Provides @Singleton public EventBus eventBus() { return new SimpleEventBus(); } @Provides @Singleton @Named("contents") public SimplePanel contentsPanel() { SimplePanel simplePanel = new SimplePanel(); simplePanel.addStyleName(Resources.INSTANCE.site().contentsPanel()); return simplePanel; } @Provides @Singleton public PlaceController placeController(EventBus eventBus) { return new PlaceController(eventBus); } @Provides @Singleton public ActivityMapper activityMapper() { return new AppActivityMapper(); } @Provides @Singleton public ActivityManager activityManager( ActivityMapper mapper,EventBus eventBus,@Named("contents") SimplePanel contentsPanel) { ActivityManager activityManager = new ActivityManager(mapper, eventBus); activityManager.setDisplay(contentsPanel); return activityManager; } @Provides @Singleton public PlaceHistoryHandler placeHistoryHandler( AppPlaceHistoryMapper mapper,PlaceController placeController,EventBus eventBus,TopPlace defaultPlace,MyInjector injector) { mapper.setFactory(injector); PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(mapper); historyHandler.register(placeController,eventBus,defaultPlace); return historyHandler; } }
今回のMvpModuleクラスでは@Providesアノテーションを利用した方法で設定を行っています。
この方法はあるDIの設定を行いたいクラスに初期化処理を入れたい場合などに非常に便利です。
それでは各メソッドについてみていきます。
configureメソッド
configureメソッドはDIの設定を各メソッドです。通常はここにどのインターフェースにどのクラスをDIさせるかなどを記載していきます。
また後記しますが、他のModule読み込みもconfigureメソッド内で記載します。
たとえばこのクラス内のeventBusメソッドをconfigureメソッド内で同じ設定で記載すると以下のようになります。
@Override protected void configure() { bind(EventBus.class).to(SimpleEventBus.class).in(Singleton.class); }
このようにModuleクラスではいろいろな設定の書き方があります。
ちなみに今回わざわざeventBusメソッドに切り出しているのはなんとなく(というより横並び感)なので気にしないでください。
またGuiceを利用したことがある方向けですが、GINではbind().toInstance()がないので、toInstanceを利用するようなケースも@Providedで記載する必要があります。
eventBusメソッド
eventBusメソッドは、Activity + Placeで利用するEventBusインターフェースの設定を行っています。
@Singletonアノテーションを記述することで、EventBusをSingletonとして設定できます。
また@ProvidesメソッドをModule内で記述することにより他のクラスでDIできるようになります。
contentsPanelメソッド
contentsPanelメソッドではViewを挿入するためのSimplePanelの設定を行っています。
@Namedアノテーションを指定することにより"名前付"を行い、後記のactivityManagerのように
DIを行う際、同様のアノテーションを利用することで、
この名前の付いたSimplePanel(つまりここで作成されているインスタンス)を利用出来るようになります。
placeControllerメソッド、activityMapperメソッド
eventBusと同じです。placeControllerではEventBusをModule内でメソッドインジェクションすることにより、
eventBusメソッドで作成したEventBusを利用しています。
activityManagerメソッド
activityManagerメソッドでは、引数に@Named("contents")を設定することで、contentsPanelメソッドで作成したSimplePanelをDIさせ、acitivityMangerに設定しています。
これによりPlaceが変更されActivityが呼び出される際にこのcontentsPanelが利用されるようになります。
placeHistoryHandlerメソッド
placeHistoryHandlerでは、ついでにAppPlaceHistoryMapperの設定を行っています。
AppPlaceHistoryMapperに関しては後ほど細かく書きますが、PlaceのFactoryとしてInjectorを指定しています。
またデフォルト(初期画面表示など)用のTopPlaceなどの設定もここで行っています。
MvpModuleの読み込み MyModule
次に上記で作成した、MvpModuleを読み込みする親のModuleを作成します。
package stk.client.inject; ~ import public class MyModule extends AbstractGinModule { @Override protected void configure() { install(new MvpModule()); //bind displays bind(TopDisplay.class).to(TopView.class).in(Singleton.class); } @Provides @Singleton public LoginInfo loginInfo(){ return new LoginInfo(); } } <|| ><p>configureメソッドにてinstallメソッドを利用し、MvpModuleを読み込んでいます。<br> またMyModuleではログイン情報を保持するためのLoginInfoの設定も行っています。<br> ログイン情報を@Singletonで宣言していると不思議に感じるかもしれませんが、<br> GWTでいうSingletonは各クライアントごとのSingletonの為問題ありません。</p>< ** InjectorとModuleの紐付け ><p>InjectorはGINで設定した各インスタンスを取得するためのインターフェースです。<br> 私の感覚ですが、Seasarで言うところのS2SingletonContainerみたいなものですが、<br> 通常のDIコンテナと異なり、Injectorからインスタンスを取得するためには<br> Injectorにメソッドを定義していく必要があります。<br> GinではこのInjectorを作成するには、GinInjectorを継承したインターフェースを作成する必要があります。</p>< >|java| package stk.client.inject; ~ import @GinModules(MyModule.class) public interface MyInjector extends Ginjector { LoginInfo getLoginInfo(); PlaceController getPlaceController(); ActivityManager getActivityManager(); EventBus getEventBus(); GwtAuthServiceAsync getAuthService(); TopPlace getTopPlace(); TopPlace.Tokenizer getTopTokenizer(); PlaceHistoryHandler getHistoryHandler(); MainView getMainView(); @Named("contents") SimplePanel getContentsPanel(); TopDisplay getTopDisplay(); }
@GinModulesアノテーションを利用して、上記のMyModuleとInjectorを紐付けています。
Injector内では、各利用するクラスのGetterを記述し、あとあと利用出来るようにしています。
これでGIN側の設定は完了です。こんどはMVPフレームワークをより簡単にするために、Placeクラスなどを作成します。
ActivityとPlaceを紐付けするActivityPlaceの作成
前回書いたMVPフレームワークでは、AppActivityMapperにてinstanceofを利用することにより、
PlaceとActivityの関連付けを行っていました。
今回はPlaceとActivityを事前にひもづけることによりinstanceofをほとんど利用せず、
AppActivityMapperを記述できるようにします。
そのためにはPlaceクラスを拡張した抽象クラス、ActivityPlaceクラスを作成します。
package stk.client.framework; ~ import public abstract class ActivityPlace<T extends Activity> extends Place { @Inject Provider<T> provider; protected String token = ""; public void init(String token){ this.token = token; } public String getToken(){ return this.token; } public T getActivity(){ return provider.get(); } abstract public static class Tokenizer<E extends ActivityPlace<?>> implements PlaceTokenizer<E>{ @Inject Provider<E> provider; @Override public E getPlace(String token) { E place = provider.get(); place.init(token); return place; } @Override public String getToken(E place) { return place.getToken(); } } }
まずActivityPlaceクラスでは、Genericsを利用しPlaceとActivityを紐付けています。
providerフィールドに@Inject指定し、Providerクラスをフィールドインジェクションしています。
Providerクラスはクラスを生成するためのクラスで、provider.get()メソッドを呼ぶことにより、
Genericsにより指定されたクラスを生成することができます。
今回はActivityを継承したT(TはActivityPlaceを継承したクラスで任意に設定します。)クラスを生成することができます。
getActivity()メソッドではこのproviderを利用し、Tを生成・返却することで、PlaceからActivityの生成を可能にしています。
ActivityPlace内で宣言された、TokenizerではこのActivityPlaceの生成を行います。
前述のMyInjector内にて呼び出しメソッド(例えばgetTopPlaceTokenizer)を作成し、
AppPlaceHistoryMapperにてMyInjector内のTokenizer作成メソッドを呼ぶことで、
Tokenizerが作成、さらにはTokenizerからこのActivityPlaceが作成されます。
例えば下記のようなActivityPlaceを継承した、クラスを作成します。
package stk.client.place; ~ import public class TopPlace extends ActivityPlace<TopActivity> { @Override public String getToken() { return ""; } @Override public void init(String token) { } @Prefix("!top") public static class Tokenizer extends ActivityPlace.Tokenizer<TopPlace> implements PlaceTokenizer<TopPlace> {} }
PlaceからActivityの作成を行う
今度はPlaceからActivityの作成を行うActivityMapperの作成をします。
package stk.client.framework; ~ import public class AppActivityMapper implements ActivityMapper { @Inject public AppActivityMapper() { super(); } @Override public Activity getActivity(Place place) { if (!(place instanceof ActivityPlace<?>)) { return null; } ActivityPlace<?> activityPlace = (ActivityPlace<?>)place; return activityPlace.getActivity(); } }
どおでしょうか、前回のAppActivityMapperにくらべinstanceofの記述がほぼなくなり、
非常にすっきりしたのではないでしょうか?
前回のAppActivityMapperではPlaceが増えるたびにAppActivityMapperを書き換える必要がありましたが、
GINを利用したことにより、その依存が消えAppActivityMapperを変更する必要がな くなりました。
次にHistory Token と Placeの紐付けを行う、AppPlaceHistoryMapper
前回のAppPlaceHistoryMapperでは@WithTokenizersを利用することにより、History TokenとTokenizerの紐付けを指定していました。
今回はTokenizerがGINにより作成される必要があるため、WithTokenizersアノテーションを利用できません。
そのため、前回AppPlaceHistoryMapperを作成するために継承した、PlaceHistoryMapperを利用するのではなく、
TokenizerのFactoryを指定できる、PlaceHistoryMapperWithFactoryを継承します。
package stk.client.framework; ~ import public interface AppPlaceHistoryMapper extends PlaceHistoryMapperWithFactory<MyInjector> { }
PlaceHistoryMapperWithFacotryのgenericsとして、MyInjectorを指定しています。
これによりGWT 2.1のMVPフレーワークがFactoryとして指定されたMyInjectorより、
PlaceTokenizerを継承したクラスを返却するメソッドを探し出し、Tokenizerを生成するようになります。
余談ですが、このgenericsにしてするFacotryは特に指定されたクラスを継承する必要がありません。
PlaceHistoryMapperWithFactoryにはこのFactoryのGetterもないため、ものすごく不思議な感じがします。。。
細かな仕組みはここでは書きませんが、このへんがGWTの離れ業なんでしょうね。
あとは適当にActivityとPlaceを作成
あとは実際に利用する、ActivityとPlaceを作成します。
package stk.client.presenter; ~ import public class TopActivity extends AbstractActivity { private final MyInjector injector; @Inject public TopActivity(MyInjector injector){ this.injector = injector; } @Override public void start(AcceptsOneWidget panel, EventBus eventBus) { TopDisplay topDisplay = injector.getTopDisplay(); panel.setWidget(topDisplay); } }
package stk.client.place; ~ import public class TopPlace extends ActivityPlace<TopActivity> { @Override public String getToken() { return ""; } @Override public void init(String token) { } @Prefix("!top") public static class Tokenizer extends ActivityPlace.Tokenizer<TopPlace> implements PlaceTokenizer<TopPlace> {} }
で最後に呼び出し
さいごにこれらのクラスの呼び出しを行います。
まずgwt.xmlにてGINの呼び出しを行います。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.0.3//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.0.3/distro-source/core/src/gwt-module.dtd"> <module> <inherits name="com.google.gwt.user.User" /> <inherits name='com.google.gwt.user.theme.standard.Standard' /> <inherits name='org.slim3.gwt.emul.S3Emulation' /> <inherits name="com.google.gwt.activity.Activity"/> <inherits name="com.google.gwt.inject.Inject"/> <inherits name="com.google.gwt.logging.Logging" /> <inherits name="com.google.common.collect.Collect"/> <set-property name="gwt.logging.popupHandler" value="DISABLED"/> <script src="/js/jquery-1.4.2.min.js"/> <script src="/js/jquery-ui-1.8.1.custom.min.js"/> <source path="client"> <exclude name="**/*Test.java"/> </source> <source path="shared"> <exclude name="**/*Test.java"/> </source> <entry-point class="stk.client.Index"></entry-point> </module>
ちなみにcom.google.gwt.inject.InjectがGINの設定部分です。
次にGWTのモジュールクラスを記述します。
package stk.client; ~ import public class Index implements EntryPoint { Logger logger = Logger.getLogger(Index.class.getName()); @Override public void onModuleLoad() { final MyInjector injector = GWT.create(MyInjector.class); injector.getActivityManager(); Resources.INSTANCE.site().ensureInjected(); injector.getAuthService().loginAuth(GWT.getHostPageBaseURL(), new AsyncCallback<LoginInfo>() { @Override public void onSuccess(LoginInfo result) { LoginInfo loginInfo = injector.getLoginInfo(); loginInfo.email = result.email; loginInfo.loggedIn = result.loggedIn; loginInfo.logoutUrl = result.logoutUrl; loginInfo.nickName = result.nickName; loginInfo.loginUrl = result.loginUrl; RootPanel.get("msg").setVisible(false); RootPanel.get("contents").add(injector.getContentsPanel()); injector.getHistoryHandler().handleCurrentHistory(); } @Override public void onFailure(Throwable caught) { } }); } }
今回の例ではログイン処理とかも入っていますが、注目していただきたいのは、
最初にGWT.createでMyInjectorを生成している部分です。
これによりMyInjectorが初期化されMyModuleなどが呼び出され、すべてのDI設定が行われます。
ちなみにinjector.getActivityManager();を次に呼び出しているのは、@Singletonアノテーションでは、
初期作成ではなく呼び出し時に初めてSingletonインスタンスが作成されるため、一度呼び出しを行っています。
多分もっとスマートにできる気がしますが。。。
またRootPanel.get("contents").add(injector.getContentsPanel());でRootPanelにフレームワークで利用する
ContentsPanel(@Named("contents")を指定したSimplePanel)を設定しています。
まとめ
非常にクラス数が多いため、めんどくさく感じるかもしれませんが、一度作ってしまえば、
前回の記事に比べほとんど変更は要らなくなります。
画面を増やす場合はPlaceとActivityを作成しinjectorにTokenizerメソッドを増やすだけでよくなります。
実際にMVP + GINを使っているサイトとして、
私がコミッターをやらさせていただいている、id:coolstyle さん(@yusuke_kokubo)のskillmapsがあります。
ソースコードもgithubで公開されていますので*4、是非ご覧になってみてください。
次回予告
次回は、MVPの話でなくて、GWT 2.1で追加されたCellViewあたりの話を書こうかと思います。たぶんいつか
あとは、MVP + GWTでのGWT.runAsyncのうまいやりかたを書こうかと たぶんいつか
追伸
文章がヘタクソで申し訳ございません。。。GWTでMVPアーキテクチャ + EventBus + History その2(HistoryもAppControllerもさようなら)
ちょうど昨日頑張ってHisotryとEventBus、AppControllerの話を書いたのに
今日気がついたらGWT2.1.0-RC1が来ていて、AppControllerもHistoryもEventBusもいらなくなったよ
オワタ \(^o^)/オワタ \(^o^)/オワタ \(^o^)/オワタ \(^o^)/オワタ \(^o^)/
はい、意味ナッシングです。
でもいらなくなったのは必要なくなったのではなく、作らなくてよくなっただけなので、
昨日出て速攻でGWT2.1.0-RC1版MVPフレームワーク対応を行ってみました。
だって男の子だもん
てことで昨日と同じ感じで前提から
私は英語力も日本語力もないので、
わかりにくい、解釈が間違っている個所は多分にあるかと思います。
そういった場合はやさしく見守ってください。
今回MVP Frameworkを作る上で参考にしたのはGWTの公式サイトです。
Googleさん今回は非常に分かりやすかったです。
サンプルのソースが間違っている個所が少々ありましたが。。。
で中身はほぼ写経しました(めんどいところはコピペしました)。
コードはこちらです。
全体像などなど
昨日の記事を見て下さい。
もう一回書くと↓のような画面遷移です。
https://cacoo.com/diagrams/UmEpII6zMLeG4bRm
よくある
「一覧」→「新作成入力」→「一覧」
「一覧」→「修正入力」→「一覧」
のような遷移のモジュールを作成しています。
今回はこれをGWTでMVP化するとどうなるかを書きます。
登場人物
Activity
ActivityはMVPアーキテクチャで言うところのPresenterの役割を持ちます。
つまり責務は
- Eventの処理
- データバインディング
です。
GWT MVPフレームワークでは
Activity(MVPのPresenter)となるクラスはAbstractActivityを継承します。
そしてstart()メソッドで、担当するViewに対して、データバインドを行います。
コンストラクタで出てくるPlaceは後で解説します。
ClientFactoryは単純なFactoryです。
各getHogeにてインスタンスを作成し、返却します。
ListPresenter(Activity)
package com.google.code.stk.client.ui.presenter; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.code.stk.client.ClientFactory; import com.google.code.stk.client.service.TwitterServiceAsync; import com.google.code.stk.client.ui.display.ListDisplay; import com.google.code.stk.client.ui.place.EditPlace; import com.google.code.stk.client.ui.place.ListPlace; import com.google.code.stk.client.ui.place.NewPlace; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.activity.shared.AbstractActivity; import com.google.gwt.event.shared.EventBus; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.AcceptsOneWidget; public class ListPresenter extends AbstractActivity implements ListDisplay.Presenter{ private final TwitterServiceAsync service; private final ClientFactory clientFactory; private final ListPlace place; private ListDisplay display; public ListPresenter(ListPlace place , ClientFactory clientFactory) { super(); this.place = place; this.clientFactory = clientFactory; this.service = this.clientFactory.getTwitterService(); } @Override public void clickNewButton() { clientFactory.getPlaceController().goTo(new NewPlace()); } @Override public void clickDeleteAnchor(final Key key) { service.delete(key, new AsyncCallback<Void>() { @Override public void onSuccess(Void arg0) { clientFactory.getPlaceController().goTo(new ListPlace("delete" , key.getId())); } @Override public void onFailure(Throwable throwable) { } }); } @Override public void clickEditAnchor(Key key) { clientFactory.getPlaceController().goTo(new EditPlace(key.getId())); } @Override public void savePinCode(String value) { service.registAccessToken(value, new AsyncCallback<Void>() { @Override public void onFailure(Throwable arg0) { } @Override public void onSuccess(Void arg0) { display.getPinCodeInputPanel().setVisible(false); display.getSavePinCodeButton().setEnabled(true); Window.alert("ピンコードを保存しました。"); } }); } @Override public void start(final AcceptsOneWidget panel, EventBus eventBus) { display = clientFactory.getListDisplay(); display.setPresenter(this); panel.setWidget(display); service.findAll(new AsyncCallback<List<AutoTweet>>() { @Override public void onSuccess(List<AutoTweet> arg0) { display.drowTable(arg0); } @Override public void onFailure(Throwable arg0) { Window.alert(arg0.getMessage()); } }); } }
ClientFactory(Factory)
ClientFactoryはDeferred Bindingを利用しGWT.createにて作成します。
これによりごっそりViewなどをモックに変更することが出来ます。Factory中のEventBusは昨日説明したEventBusとほぼ同じっぽいです。
つまり独自Eventを処理します。PlaceControllerは後ほど説明します。
package com.google.code.stk.client; import com.google.code.stk.client.service.TwitterService; import com.google.code.stk.client.service.TwitterServiceAsync; import com.google.code.stk.client.ui.display.AutoTweetDisplay; import com.google.code.stk.client.ui.display.AutoTweetView; import com.google.code.stk.client.ui.display.ListDisplay; import com.google.code.stk.client.ui.display.ListView; import com.google.gwt.core.client.GWT; import com.google.gwt.event.shared.EventBus; import com.google.gwt.event.shared.SimpleEventBus; import com.google.gwt.place.shared.PlaceController; public class ClientFactoryImpl implements ClientFactory { private ListDisplay listDisplay = null; private PlaceController placeController = null; private SimpleEventBus eventBus; private TwitterServiceAsync twitterService; @Override public AutoTweetDisplay getAutoTweetDisplay() { return new AutoTweetView(); } @Override public EventBus getEventBus() { if(eventBus == null){ eventBus = new SimpleEventBus(); } return eventBus; } @Override public ListDisplay getListDisplay() { if(listDisplay == null){ listDisplay = new ListView(); } return listDisplay; } @Override public PlaceController getPlaceController() { if(placeController == null){ placeController = new PlaceController(getEventBus()); } return placeController; } @Override public TwitterServiceAsync getTwitterService() { if(twitterService == null){ twitterService = GWT.create(TwitterService.class); } return twitterService; } }
Display(IsWidget)
MVPのDisplayは単純なViewクラスなのでWidgetやUiBinderあたりで作れば問題ありませんが、
Activityで利用する場合はIsWidgetインターフェースを継承しておくと、よいです。
Displayインターフェースの中にPresenterインターフェースを持っています。
これはビジネスロジックをPresenterに委譲させるために、
Displayにとって処理してほしいことをPresenterに切り出し、
そのPresenterを継承しているクラスをsetPresenterにて登録させ、
すべてのビジネスロジックをPresenterにもたせます。
これにより、ビジネスロジックと、画面が切り離され、
デスクトップアプリでありがちな、BigViewを防ぎます。
またそれぞれインターフェースとして、宣言させ、
それぞれのインターフェースを継承させることによって、
PresenterとDisplayの依存度が下がりテストのしやすさがまします。今回のListViewにとってのPresenterはListPresetnerです。
なのでListPresenterはListDislay.Presenterを継承し、
startメソッド内で、display.setPresenter(this)を行っています。そしてListView内の各@UiHanderのついたメソッドでビジネスロジック(データ登録など)を
Presenterに委譲しています。
ちなみにMVPの場合は画面遷移もPresenter側の仕事なのでPresenterに任せています。
Viewが行うのは自身のもつWidgetの管理のみです。
ListDisplay
package com.google.code.stk.client.ui.display; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.IsWidget; public interface ListDisplay extends IsWidget{ void drowTable(List<AutoTweet> tweetList); public interface Presenter{ void clickNewButton(); void clickDeleteAnchor(Key key); void clickEditAnchor(Key key); void savePinCode(String value); } public void setPresenter(Presenter presenter); FlowPanel getPinCodeInputPanel(); Button getSavePinCodeButton(); }
ListView
package com.google.code.stk.client.ui.display; import java.util.List; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlexTable; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; public class ListView extends Composite implements ListDisplay { @UiField Button newButton; @UiField InlineLabel sizeLabel; @UiField FlexTable table; @UiField Anchor pinCodeLink; @UiField FlowPanel pinCodeInputPanel; @UiField TextBox pinCode; @UiField Button savePinCode; private static ListViewUiBinder uiBinder = GWT .create(ListViewUiBinder.class); private Presenter presenter; interface ListViewUiBinder extends UiBinder<Widget, ListView>{ } @Override public void drowTable(List<AutoTweet> tweetList) { table.removeAllRows(); if (tweetList == null || tweetList.isEmpty()) { table.setText(0, 0, "tweetなし"); return; } table.setText(0, 0, "ID"); table.setText(0, 1, "対象"); table.setText(0, 2, "内容"); table.setText(0, 3, "時間"); table.setText(0, 4, "間隔"); table.setText(0, 5, "ブレ"); table.setText(0, 6, "期間"); table.setText(0, 7, "最終Tweet日"); table.setText(0, 8, "修正"); table.setText(0, 9, "削除"); int row = 1; for (final AutoTweet autoTweet : tweetList) { table.getColumnFormatter().setWidth(1, "140em"); table.getColumnFormatter().setWidth(3, "6em"); table.getColumnFormatter().setWidth(4, "8em"); table.setCellPadding(10); table.setCellSpacing(10); table.setText(row, 0, String.valueOf(autoTweet.getKey().getId())); table.setText(row, 1, autoTweet.getScreenName()); table.setText(row, 2, autoTweet.getTweet()); table.setText(row, 3, autoTweet.getTweetHour() + "時"); table.setText(row, 4, autoTweet.getCycle().name()); table.setText(row, 5, autoTweet.getBure().name()); table.setText(row, 6, autoTweet.getStartMMdd() + " 〜 " + autoTweet.getEndMMdd()); table.setText(row, 7, autoTweet.getLastTweetAt()); Anchor syusei = new Anchor("修正"); syusei.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent arg0) { presenter.clickEditAnchor(autoTweet.getKey()); } }); table.setWidget(row, 8, syusei); Anchor sakujo = new Anchor("削除"); sakujo.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent arg0) { presenter.clickDeleteAnchor(autoTweet.getKey()); } }); table.setWidget(row, 9, sakujo); row++; } } @Override public Widget asWidget() { return this; } @Override public void setPresenter(Presenter presenter) { this.presenter = presenter; } @Override public FlowPanel getPinCodeInputPanel(){ return pinCodeInputPanel; } @UiHandler("newButton") void onClickNewButton(ClickEvent e){ presenter.clickNewButton(); } @UiHandler("pinCodeLink") public void onClickPinCodeLink(ClickEvent e){ pinCodeInputPanel.setVisible(true); } @UiHandler("savePinCode") public void onClickSavePinCode(ClickEvent e){ savePinCode.setEnabled(false); presenter.savePinCode(pinCode.getValue()); } public ListView() { initWidget(uiBinder.createAndBindUi(this)); } @Override public Button getSavePinCodeButton() { return savePinCode; } }
Place
Placeは画面や状態を表します。
昨日AppControllerとEventBus、Historyにてヒストリー中心の画面遷移を説明しました。
AppControllerとEventBus、Historyのやることをより、わかりやすく、そして責務を画面遷移、状態保持に
特化させたのがPlaceです。(タブン)
GWTでのPlaceフレームワークは画面や状態を表すPlaceクラスと、
それらを利用してHistoryイベントを発火するPlaceController、
PlaceとHistoryをマッピングさせるPlaceHistoryMapper、
そしてPlaceとActivityをひもづける、ActivityMapperを利用して実現します。
まず画面や状態を表す、Placeクラスの子クラスから
ListPlace
ListPlaceは一覧画面を表すPlaceクラスの子クラスです。
idやstateを持っているのは削除処理からもどった、
修正処理から戻った、
新規作成から戻ったなどを管理するためです。
ListPlaceにはListPlaceの情報からTokenを導き出す、
Tokenizerを持っています。
TokenizerはListPlaceの作成と、ListPlaceからtokenの作成を行います。
package com.google.code.stk.client.ui.place; import com.google.gwt.place.shared.Place; import com.google.gwt.place.shared.PlaceTokenizer; public class ListPlace extends Place { private final String state; private final Long id; public ListPlace(){ this(null , null); } public ListPlace(String state, Long id) { this.state = state; this.id = id; } public ListPlace(String state) { this(state, null); } public Long getId() { return id; } public String getStringId(){ return id == null?"":id.toString(); } public String getState() { return state == null ? "" :state; } public static class Tokenizer implements PlaceTokenizer<ListPlace>{ @Override public ListPlace getPlace(String token) { if(token.contains("delete")){ return new ListPlace("delete" , Long.parseLong(token.replace("delete/", ""))); } if(token.contains("edit")){ return new ListPlace("edit" , Long.parseLong(token.replace("edit/", ""))); } if(token.contains("new")){ return new ListPlace("new"); } return new ListPlace(); } @Override public String getToken(ListPlace place) { return place.getState() + "/" + place.getStringId(); } } }
PlaceController
PlaceControllerはPlaceを利用して、PlaceHistoryChangeEventを発火し遷移を実現します。
たとえばListPresenterの下記メソッドは修正アンカー押下時に、
修正画面へ遷移させます。
ListPresenter
ListPresetner.java @Override public void clickEditAnchor(Key key) { clientFactory.getPlaceController().goTo(new EditPlace(key.getId())); }
PlaceHistoryMapper
PlaceHistoryMapperは各PlaceのTokenizerを利用して、HistoryTokenとPlaceのマッピングを行います。
@WithTokenizersアノテーションにTokenizerを指定するだけでマッピングしてくれます。
ちなみに発行されるHistoryTokenは {Place短縮クラス名}:{Tokenizer.getToken()}の形になります。。。
Place短縮クラス名がダサいので、治す方法が知りたい限りです。。。
package com.google.code.stk.client; import com.google.code.stk.client.ui.place.EditPlace; import com.google.code.stk.client.ui.place.ListPlace; import com.google.code.stk.client.ui.place.NewPlace; import com.google.gwt.place.shared.PlaceHistoryMapper; import com.google.gwt.place.shared.WithTokenizers; @WithTokenizers({ListPlace.Tokenizer.class , EditPlace.Tokenizer.class , NewPlace.Tokenizer.class}) public interface AppPlaceHistoryMapper extends PlaceHistoryMapper { }
ActivityMapper
ActivityMapperはPlaceとActivityをひもづけます。
ひもづけは頑張ります。。。
instanceofが微妙なのですが、、、
どうにかしたいですね。。。
package com.google.code.stk.client; import com.google.code.stk.client.ui.place.EditPlace; import com.google.code.stk.client.ui.place.ListPlace; import com.google.code.stk.client.ui.place.NewPlace; import com.google.code.stk.client.ui.presenter.EditPresenter; import com.google.code.stk.client.ui.presenter.ListPresenter; import com.google.code.stk.client.ui.presenter.NewPresenter; import com.google.gwt.activity.shared.Activity; import com.google.gwt.activity.shared.ActivityMapper; import com.google.gwt.place.shared.Place; public class AppActivityMapper implements ActivityMapper { private ClientFactory clientFactory; public AppActivityMapper(ClientFactory clientFactory) { super(); this.clientFactory = clientFactory; } @Override public Activity getActivity(Place place) { if(place instanceof ListPlace){ return new ListPresenter((ListPlace)place, clientFactory); } if(place instanceof NewPlace){ return new NewPresenter((NewPlace)place , clientFactory); } if(place instanceof EditPlace){ return new EditPresenter((EditPlace)place, clientFactory); } return null; } }
Activity、Placeまとめ
以上がActivity、Placeの関連クラスとその実装です。
画面遷移をHistory中心で行うことが出来、ブラウザBack、Ajaxにつよりアプリケーションが作成できます。
昨日書いた、AppControllerや、Historyはフレームワークの中に隠れた状態になっています。
(または各MapperやPlaceに分散されている)
上記の各MapperクラスはEntryPointあたりで各Manegerクラスに登録しておきます
ClientFactory clientFactory = GWT.create(ClientFactory.class); EventBus eventBus = clientFactory.getEventBus(); PlaceController placeController = clientFactory.getPlaceController(); // Start ActivityManager for the main widget with our ActivityMapper ActivityMapper activityMapper = new AppActivityMapper(clientFactory); ActivityManager activityManager = new ActivityManager(activityMapper,eventBus); activityManager.setDisplay(appWidget); // Start PlaceHistoryHandler with our PlaceHistoryMapper AppPlaceHistoryMapper historyMapper = GWT .create(AppPlaceHistoryMapper.class); PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper); historyHandler.register(placeController, eventBus, defaultPlace);
もうチョイ細かい動きはまた今度追います。
あとがき
今回のも前回のも全くMVPフレームワークのデザインパターンを説明してない。。。
なんでHogeDisplayがPresetner持ってるのとか説明しないとですね。。。
でもAcitivityとPlaceが出ちゃったんだもん。。。
うん、良かったと思ってるよ。。。
Data Presenterも書きたいですね。
GWTでMVPアーキテクチャ + EventBus + History その1(HistoryとAppController)
久しぶりに書きます。
最近はもっぱらGWTを扱ってます。
半年ぐらい触ってみて、やはりGWTではMVPアーキテクチャが大事になってくるなと思ったりしました。
僕も最初はまったく分かっていなかったのですが、id:k2juniorさんのこちらの日記をみて結構理解ができました。
そのあとGWTの大規模開発の説明文(?)においてある、
サンプルプロジェクトをみて何となく理解して、
自分なりに書けるようになってきたのでMVP + EventBusパターンについて書いてみます。
なお英語は読めない、聞けないなので、
Googlge IO 2009のGoogle Web Toolkit Architecture: Best Practices for Architecting Your GWT Appや
GWTの大規模開発の説明文(?)はなんとな〜くしかわかっていません。
なので解釈や、使い方が間違っている場所は多々あると思いますので、
ご指摘をお願いいたします。
ちなみにサンプルとしてはGAE上に乗っけているTwitterのBot(のデータ入力画面)を使います。
コード:http://goo.gl/7xPh (Google Codeです。)
作っているものの全体像(画面遷移)
今回つくっている画面遷移は以下のような感じです。
https://cacoo.com/diagrams/UmEpII6zMLeG4bRm
よくある
「一覧」→「新作成入力」→「一覧」
「一覧」→「修正入力」→「一覧」
のような遷移のモジュールを作成しています。
今回はこれをGWTでMVP化するとどうなるかを書きます。
登場人物
MVP + EventBusパターンでは以下のような登場人物がいます。
Model パッケージ:com.google.code.stk.share.model
MVCのModel
(モデルの解釈がいろいろあるので詳細は除きます。)
私の場合はappengineのEntityがそのままModelとしています。
package com.google.code.stk.shared.model; import java.io.Serializable; import com.google.appengine.api.datastore.Key; import com.google.code.stk.shared.Enums; import org.slim3.datastore.Attribute; import org.slim3.datastore.Model; @Model(schemaVersion = 1) public class AutoTweet implements Serializable { private static final long serialVersionUID = 1L; @Attribute(primaryKey = true) private Key key; @Attribute(version = true) private Long version; /** tweet */ @Attribute(unindexed = true) private String tweet; /** bure */ @Attribute(unindexed = true) private Enums.Bure bure; /** cycle */ @Attribute(unindexed = true) private Enums.Cycle cycle; /** startMMdd */ private String startMMdd; /** endMMdd */ private String endMMdd; /** tweetTime(hh) */ @Attribute private String tweetHour; @Attribute(unindexed = true) private String lastTweetAt; private String screenName; /** * keyを取得します。 * @return key */ public Key getKey() { return key; } /** * keyを設定します。 * @param key key */ public void setKey(Key key) { this.key = key; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((key == null) ? 0 : key.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } AutoTweet other = (AutoTweet) obj; if (key == null) { if (other.key != null) { return false; } } else if (!key.equals(other.key)) { return false; } return true; } /** * versionを取得します。 * @return version */ public Long getVersion() { return version; } /** * versionを設定します。 * @param version version */ public void setVersion(Long version) { this.version = version; } /** * tweetを取得します。 * @return tweet */ public String getTweet() { return tweet; } /** * tweetを設定します。 * @param tweet tweet */ public void setTweet(String tweet) { this.tweet = tweet; } /** * cycleを取得します。 * @return cycle */ public Enums.Cycle getCycle() { return cycle; } /** * cycleを設定します。 * @param cycle cycle */ public void setCycle(Enums.Cycle cycle) { this.cycle = cycle; } /** * startMMddを取得します。 * @return startMMdd */ public String getStartMMdd() { return startMMdd; } /** * startMMddを設定します。 * @param startMMdd startMMdd */ public void setStartMMdd(String startMMdd) { this.startMMdd = startMMdd; } /** * endMMddを取得します。 * @return endMMdd */ public String getEndMMdd() { return endMMdd; } /** * endMMddを設定します。 * @param endMMdd endMMdd */ public void setEndMMdd(String endMMdd) { this.endMMdd = endMMdd; } /** * tweetTime(hhmm)を取得します。 * @return tweetTime(hhmm) */ public String getTweetHour() { return tweetHour; } /** * tweetTime(hhmm)を設定します。 * @param tweetTime tweetTime(hhmm) */ public void setTweetHour(String tweetTime) { this.tweetHour = tweetTime; } public void setBure(Enums.Bure bure) { this.bure = bure; } public Enums.Bure getBure() { return bure; } public String getLastTweetAt() { return lastTweetAt; } public void setLastTweetAt(String lastTweetAt) { this.lastTweetAt = lastTweetAt; } public String getScreenName() { return screenName; } public void setScreenName(String value) { this.screenName = value; } }
Display パッケージ:com.google.code.stk.client.ui.display
MVPのViewにあたるインターフェース。
実際のViewクラス(WidgetsやUiBinder)の各項目をメソッドに持ちます。
package com.google.code.stk.client.ui.display; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.HasValue; public interface AutoTweetDisplay extends Display { HasValue<String> getKeyId(); HasValue<String> getTweet(); HasValue<String> getBure(); HasValue<String> getCycle(); HasValue<String> getEndMMdd(); HasValue<String> getStartMMdd(); HasValue<String> getTweetHour(); void setPresenter(Presenter presenter); public interface Presenter{ public void regist(); } Button getRegistButton(); void setScreenNames(List<Key> arg0); HasValue<String> getScreenName(); }
DisplayはPresenterとViewをそ結合にし、
再利用性とテストをしやすくしています。
ほとんどのメソッドがHas〜インターフェースで宣言されているのは、
テスト時などにモックを入れやすくするためです。
View パッケージ:com.google.code.stk.client.ui.display
ViewクラスはUiBinderやWidgetsで作成された、「画面」を持つクラスです。
ViewはDisplayを実装し、画面を作成します。
package com.google.code.stk.client.ui.display; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.code.stk.client.ui.base.ValueListBox; import com.google.code.stk.shared.Enums.Bure; import com.google.code.stk.shared.Enums.Cycle; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasValue; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; public class AutoTweetView extends Composite implements AutoTweetDisplay{ @UiField TextBox tweet; @UiField TextBox startMMdd; @UiField TextBox endMMdd; @UiField ValueListBox cycle; @UiField ValueListBox tweetHour; @UiField ValueListBox bure; @UiField TextBox keyId; @UiField Button registButton; @UiField ValueListBox screenName; private static AutoTweetViewUiBinder uiBinder = GWT .create(AutoTweetViewUiBinder.class); private Presenter presenter; private List<Key> arg0; interface AutoTweetViewUiBinder extends UiBinder<Widget, AutoTweetView> { } public AutoTweetView() { initWidget(uiBinder.createAndBindUi(this)); initDisplay(); } private void initDisplay() { for (Bure b : Bure.values()) { bure.addItem(b.name()); } for(Cycle c : Cycle.values()){ cycle.addItem(c.name()); } for(int i = 0; i < 24 ; i++){ String hour = String.valueOf(i); if(hour.length() < 2){ hour = 0 + hour; } tweetHour.addItem(hour +"時" , hour); } } @UiHandler("registButton") public void onRegistButtonClick(ClickEvent e){ registButton.setEnabled(false); presenter.regist(); } @Override public Button getRegistButton(){ return registButton; } @Override public HasValue<String> getBure() { return bure; } @Override public HasValue<String> getCycle() { return cycle; } @Override public HasValue<String> getEndMMdd() { return endMMdd; } @Override public HasValue<String> getKeyId() { return keyId; } @Override public HasValue<String> getStartMMdd() { return startMMdd; } @Override public HasValue<String> getTweetHour() { return tweetHour; } @Override public Widget asWidget() { return this; } @Override public HasValue<String> getTweet() { return tweet; } @Override public void setPresenter(Presenter presenter) { this.presenter = presenter; } @Override public void setScreenNames(List<Key> arg0) { this.arg0 = arg0; for (Key key : this.arg0) { screenName.addItem(key.getName()); } } @Override public HasValue<String> getScreenName() { return screenName; } }
Presenter
MVPのPで「機能提供者」の役割を持ちます。
やることは
- イベントの処理
- データバインド(Viewへ渡す)
です。
package com.google.code.stk.client.ui.presenter; import java.util.List; import com.google.appengine.api.datastore.Key; import com.google.code.stk.client.event.NewTweetCreatedEvent; import com.google.code.stk.client.service.TwitterServiceAsync; import com.google.code.stk.client.ui.display.AutoTweetDisplay; import com.google.code.stk.client.ui.display.AutoTweetDisplay.Presenter; import com.google.code.stk.shared.Enums.Bure; import com.google.code.stk.shared.Enums.Cycle; import com.google.code.stk.shared.model.AutoTweet; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.user.client.rpc.AsyncCallback; public class NewPresenter extends AbstractPresenter<AutoTweetDisplay> implements Presenter { private final TwitterServiceAsync service; public NewPresenter(AutoTweetDisplay display, HandlerManager eventBus , TwitterServiceAsync service) { super(display, eventBus); this.service = service; display.setPresenter(this); } @Override public void regist() { display.getRegistButton().setEnabled(false); AutoTweet data = new AutoTweet(); data.setBure(Bure.valueOf(display.getBure().getValue())); data.setCycle(Cycle.valueOf(display.getCycle().getValue())); data.setStartMMdd(display.getStartMMdd().getValue()); data.setEndMMdd(display.getEndMMdd().getValue()); data.setTweet(display.getTweet().getValue()); data.setTweetHour(display.getTweetHour().getValue()); data.setScreenName(display.getScreenName().getValue()); service.regist(data, new AsyncCallback<Void>() { @Override public void onSuccess(Void arg0) { display.getRegistButton().setEnabled(true); eventBus.fireEvent(new NewTweetCreatedEvent()); } @Override public void onFailure(Throwable throwable) { } }); } @Override protected void initView() { container.clear(); service.findAllAccessToeknOnlyKey(new AsyncCallback<List<Key>>() { @Override public void onFailure(Throwable arg0) { } @Override public void onSuccess(List<Key> arg0) { display.setScreenNames(arg0); container.add(display.asWidget()); } }); } }
AppController
AppControllerは各PresenterとDisplayの管理、
独自Eventの処理を行います。
またValueChangeHandlerを実装することで、
PresenterとHistoryを管理することにより、
画面遷移をコントロールしています。
package com.google.code.stk.client; import com.google.code.stk.client.event.DeleteTweetClickEvent; import com.google.code.stk.client.event.DeleteTweetClickHandler; import com.google.code.stk.client.event.EditTweetClickEvent; import com.google.code.stk.client.event.EditTweetClickHandler; import com.google.code.stk.client.event.NewTweetClickEvent; import com.google.code.stk.client.event.NewTweetClickHandler; import com.google.code.stk.client.event.NewTweetCreatedEvent; import com.google.code.stk.client.event.NewTweetCreatedHandler; import com.google.code.stk.client.service.TwitterService; import com.google.code.stk.client.service.TwitterServiceAsync; import com.google.code.stk.client.ui.display.AutoTweetDisplay; import com.google.code.stk.client.ui.display.AutoTweetView; import com.google.code.stk.client.ui.display.ListDisplay; import com.google.code.stk.client.ui.display.ListView; import com.google.code.stk.client.ui.presenter.AbstractPresenter; import com.google.code.stk.client.ui.presenter.EditPresenter; import com.google.code.stk.client.ui.presenter.ListPresenter; import com.google.code.stk.client.ui.presenter.NewPresenter; import com.google.gwt.core.client.GWT; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.user.client.History; import com.google.gwt.user.client.ui.HasWidgets; public class AppController implements ValueChangeHandler<String> { private final HandlerManager eventBus; private HasWidgets container; private TwitterServiceAsync service = GWT.create(TwitterService.class); public AppController(HandlerManager eventBus) { super(); this.eventBus = eventBus; bind(); } public void go(HasWidgets container){ this.container = container; if(History.getToken() == null || History.getToken().equals("")){ History.newItem("list"); }else{ History.fireCurrentHistoryState(); } } private void bind() { History.addValueChangeHandler(this); eventBus.addHandler(NewTweetClickEvent.TYPE, new NewTweetClickHandler() { @Override public void onClick(NewTweetClickEvent e) { History.newItem("new"); } }); eventBus.addHandler(EditTweetClickEvent.TYPE, new EditTweetClickHandler() { @Override public void onClick(EditTweetClickEvent e) { History.newItem("edit/" + String.valueOf(e.getKey())); } }); eventBus.addHandler(DeleteTweetClickEvent.TYPE, new DeleteTweetClickHandler() { @Override public void onClick(DeleteTweetClickEvent e) { History.newItem("list/deleteComplete/" + String.valueOf(e.getKey())); } }); eventBus.addHandler(NewTweetCreatedEvent.TYPE, new NewTweetCreatedHandler() { @Override public void onCreated(NewTweetCreatedEvent e) { History.newItem("list/newComplete"); } }); } @Override public void onValueChange(ValueChangeEvent<String> arg0) { String token = History.getToken(); if(token == null || token.startsWith("list") || token.equals("")){ ListDisplay display = new ListView(); AbstractPresenter<ListDisplay> presenter = new ListPresenter(display , eventBus , service); presenter.go(container); return; } if(token.equals("new")){ AutoTweetView display = new AutoTweetView(); AbstractPresenter<AutoTweetDisplay> presenter = new NewPresenter(display, eventBus, service); presenter.go(container); return; } if(token.startsWith("edit/")){ long id = Long.parseLong(token.replace("edit/", "")); AutoTweetView display = new AutoTweetView(); AbstractPresenter<AutoTweetDisplay> presenter = new EditPresenter(display, eventBus, service, id); presenter.go(container); } } }
EventBus (HandlerManager)
EventBus(HandlerManager)は独自作成Eventの管理を行います。
このEventBusに独自Eventを登録し、Handlerの処理を定義することができます。
動きの流れ
① 始まり(EntryPoint → AppController)
まずEntryPointではEventBusの作成と、AppControllerの作成をおこないます。
またAppControllerに表示の起点(container)を通知します。
package com.google.code.stk.client; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.user.client.ui.RootPanel; public class Index implements EntryPoint { @Override public void onModuleLoad() { HandlerManager eventBus = new HandlerManager(null); AppController appController = new AppController(eventBus); appController.go(RootPanel.get("contents")); RootPanel.get("msg").setVisible(false); } }
AppControllerがnewされるタイミングでコンストラクタにて、EventBusを受け取り、
独自イベントの登録を行います。(bind()メソッド)
独自Eventの作成pointは画面遷移が行われる個所です。
また独自Eventの処理も画面遷移が行われたことを記録するため
Historyの登録のみおこないます。
またこの時、AppController自信をHistoryのValueChangeEventHandlerとして登録します。
@AppControllerのコンストラクタ
public AppController(HandlerManager eventBus) { super(); this.eventBus = eventBus; bind(); } private void bind() { History.addValueChangeHandler(this); eventBus.addHandler(NewTweetClickEvent.TYPE, new NewTweetClickHandler() { @Override public void onClick(NewTweetClickEvent e) { History.newItem("new"); } }); eventBus.addHandler(EditTweetClickEvent.TYPE, new EditTweetClickHandler() { @Override public void onClick(EditTweetClickEvent e) { History.newItem("edit/" + String.valueOf(e.getKey())); } }); eventBus.addHandler(DeleteTweetClickEvent.TYPE, new DeleteTweetClickHandler() { @Override public void onClick(DeleteTweetClickEvent e) { History.newItem("list/deleteComplete/" + String.valueOf(e.getKey())); } }); eventBus.addHandler(NewTweetCreatedEvent.TYPE, new NewTweetCreatedHandler() { @Override public void onCreated(NewTweetCreatedEvent e) { History.newItem("list/newComplete"); } }); }
EntryPointでAppControllerの作成が完了したら、AppController#goを呼び出し、
初期画面を表示します。
goメソッドでは、初期表示を行う親Widgetsを受け取り、保存します。
そして、Historyになにも積まれてなければ、初期画面を表示するためHistoryにtokenを保存します。
既に、Historyに何かあった場合(通常はF5などで画面を更新したなど)は現行のTokenの状態でValueChangeEventを発生させます。
public void go(HasWidgets container){ this.container = container; if(History.getToken() == null || History.getToken().equals("")){ History.newItem("list"); }else{ History.fireCurrentHistoryState(); } }
goメソッドにて、発行されたHistoryTokenにより、ValueChangeEventが発生します。
AppControllerのコンストラクタにて、AppController自信がHistoryのValueChangeHandlerとして
登録されているため、AppControllerのonValueChangeメソッドが呼び出されます。
onValueChangeメソッドではHistoryTokenをもとにどの画面を表示するか(DisplayとPresenterを使うか)を
判定します。
初期処理の場合は、tokenが"list"となっているので、ListDisplayとListPresenterが利用されます。
@Override public void onValueChange(ValueChangeEvent<String> arg0) { String token = History.getToken(); if(token == null || token.startsWith("list") || token.equals("")){ ListDisplay display = new ListView(); AbstractPresenter<ListDisplay> presenter = new ListPresenter(display , eventBus , service); presenter.go(container); return; } if(token.equals("new")){ AutoTweetView display = new AutoTweetView(); AbstractPresenter<AutoTweetDisplay> presenter = new NewPresenter(display, eventBus, service); presenter.go(container); return; } if(token.startsWith("edit/")){ long id = Long.parseLong(token.replace("edit/", "")); AutoTweetView display = new AutoTweetView(); AbstractPresenter<AutoTweetDisplay> presenter = new EditPresenter(display, eventBus, service, id); presenter.go(container); } }
以上にて、EntryPointからAppControllerまでの処理は完了です。
AppControllerがHistoryにより画面遷移を制御することで、
画面遷移=Historyへの登録という状態が出来ます。
これによりF5やブラウザバックに強いアプリケーションが作成できます。
たとえば上記にて一覧画面が表示された後、新規作成画面を表示する場合は、
ListPresenterにてNewTweetClickEventをEventBus経由で発火することにより、
再びAppControllerへ処理が戻り、Historyが"new"で積まれ、
AppControllerのonValueChangeメソッドが呼ばれ
NewPresetnerとNewDisplayが選ばれて画面が表示されます。
大規模な開発の場合はこのAppControllerを複数作成し、Event処理を分割することも可能です。
またonValueChangeごとで、コード分割(GWT.runAsync)を行って画面ごとにコードを分割することも可能です。
Historyを使った画面遷移は管理もしやすく、値画面遷移にパラメータを必要とする場合も、#hoge/valueなどとしてうまく取得することもできます。
※タブンGmailとかもそんな感じ?
AppControllerを利用することで、見やすい画面遷移の制御を作ることが出きます。
あとがき
文才がない。。。。
次回はMVPアーキテクチャを細かく書きます。
タブン、ソノウチ、イツカカキマス
T2でSlim3のバリデーションを使いたい
現在作っているサイトはWeb系の部分はt2、永続化周りはslim3-datastoreを使っています。
ただt2にはValidationはないので(t2にはなくていいと思います その薄さがとても素敵なので。)どうしようかなと思っていたのですが、
あるじゃないですかSlim3に!validatorsが!
ってことでslim3のValidatorsをT2で使えないかやってみました。
とりあえずなんも考えないでつかってみた。。。
とりあえず何も考えないで、使ってみました。
/** * イメージデータを保存します。 * @param context context * @param request request * @param file アップロードファイル * @param title タイトル * @param updaterComment 投稿者コメント * @return 成功時完了画面 */ @POST @ActionParam("store") public Navigation store( WebContext context ,HttpServletRequest request ,UploadFile file ,@RequestParam("title") String title ,@RequestParam("updaterComment") String updaterComment ){ //ここからValidation Validators v = new Validators(request); v.add(ImageMeta.get().title, v.required()); v.add("img", v.byteType()); v.add(ImageMeta.get().updaterComment , v.maxlength(500)); if(v.validate() == false){ //v.validate()がfalseなら終了 return index(context); } if(file.get().length > StkConst.MAX_IMAGE_BYTES){ return index(context); } if(!ArrayUtil.contains(StkConst.getSupportedExtensions() , FilenameUtils.getExtension(file.getName()).toLowerCase())){ return index(context); } Image image = new Image(); GlobalTransaction tx =Datastore.beginGlobalTransaction(); try{ image.setKey(Datastore.allocateId(Image.class)); image.getUpdaterRef().setModel((Member)context.getSession().getAttribute(StkConst.SESSION_KEY_MEMBER)); image.setTitle(title); image.setUpdaterComment(updaterComment); image.setCreateDate(new Date()); image.setFileName(file.getName()); List<ImgUnit> imgUnits = image.setImage(file.get()); tx.put(imgUnits); tx.put(image); tx.commit(); }catch (Exception e){ tx.rollback(); v.getErrors().put("put", e.getMessage()); } return Redirect.to("/top"); }
で実行すると、下記のようなException
Caused by: java.lang.NullPointerException
at org.slim3.jsp.Functions.errors(Functions.java:403)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.apache.commons.el.FunctionInvocation.evaluate(FunctionInvocation.java:172)
at org.apache.commons.el.ExpressionEvaluatorImpl.evaluate(ExpressionEvaluatorImpl.java:263)
at org.apache.commons.el.ExpressionEvaluatorImpl.evaluate(ExpressionEvaluatorImpl.java:190)
at org.apache.jasper.runtime.PageContextImpl$13.run(PageContextImpl.java:884)
at java.security.AccessController.doPrivileged(Native Method)
at org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(PageContextImpl.java:880)
at org.apache.jsp.upload.index_jsp._jspx_meth_c_forEach_0(index_jsp.java:153)
at org.apache.jsp.upload.index_jsp._jspService(index_jsp.java:86)
... 66 more
なんか必要らしい。。。
で該当の場所をみると
@slim3 Functions
/** * Returns errors iterator. * * @return errors iterator */ @SuppressWarnings("unchecked") public static Iterator<String> errors() { HttpServletRequest request = RequestLocator.get();←ここでNullが帰る。 Map<String, String> errors = ←ここでNullPointer (Map<String, String>) request .getAttribute(ControllerConstants.ERRORS_KEY); if (errors != null) { return errors.values().iterator(); } return null; }
こんな感じらしく、RequestLocatorにrequestが入っていないといけないらしい。。。
あと雰囲気的にRequestのなかにControllerConstrants.ERRORS_KEYでなんかはいってるらしい。
でいろいろ見てってPluginを作ってみた。
でいろいろ見てってPluginを作ってみた。
こんな感じです。
package com.google.code.stk.t2gae.commons.plugin; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slim3.controller.ControllerConstants; import org.slim3.controller.validator.Errors; import org.slim3.util.ApplicationMessage; import org.slim3.util.RequestLocator; import org.slim3.util.ResponseLocator; import org.t2framework.t2.plugin.AbstractPlugin; /** * slim3の各種機能を利用するためのPlugin * @author keisuke.oohashi * */ public class Slim3Plugin extends AbstractPlugin { //TODO 2010/03/21 16:37:53 keisuke.oohashi:ThreadSafeじゃなさそう。。。 /** previousRequest */ private HttpServletRequest previousRequest; /** previousResponse */ private HttpServletResponse previousResponse; /** * slim3のコントローラクラスの代わりにRequest、Response、Errorsの設定を行います。 * * @param request Request * @param response Response * * @see org.slim3.controller.FrontController * @see org.t2framework.t2.plugin.AbstractPlugin#beginRequestProcessing(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) * */ @Override public void beginRequestProcessing(HttpServletRequest request, HttpServletResponse response) { previousRequest = RequestLocator.get(); RequestLocator.set(request); previousResponse = ResponseLocator.get(); ResponseLocator.set(response); ApplicationMessage.setBundle(ControllerConstants.DEFAULT_LOCALIZATION_CONTEXT, request.getLocale()); Errors errors = (Errors) request.getAttribute(ControllerConstants.ERRORS_KEY); if (errors == null) { errors = new Errors(); request.setAttribute(ControllerConstants.ERRORS_KEY, errors); } super.beginRequestProcessing(request, response); } /** * slim3のコントローラクラスの代わりにRequest、Response、Errorsのクリアを行います。 * * @param request Request * @param response Response * * @see org.slim3.controller.FrontController * @see org.t2framework.t2.plugin.AbstractPlugin#endRequestProcessing(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public void endRequestProcessing(HttpServletRequest request, HttpServletResponse response) { ApplicationMessage.clearBundle(); ResponseLocator.set(previousResponse); RequestLocator.set(previousRequest); super.endRequestProcessing(request, response); } }
こんな感じで作ってみたのですが、TODOにも書いてる通り、ThreadSafeじゃなさそう。。。
PluginクラスはGuiceのRequestScopedで保存できないっぽいのでとりあえずSINGLETONにしてます。
かなり怪しい。
であとはこのクラスをGuiceモジュールで登録しておきます。
bind(Plugin.class).annotatedWith(Names.named("slim3Plugin")).to(Slim3Plugin.class).in(Scopes.SINGLETON);
これで最初のメソッドを呼べばValidatorが実施されます。
Navigationを拡張してみる。
T2Frameworkの非常に使いやすいところの一つに拡張性の高さがあると思います。ということで今回はNavigationを拡張してみます。
ことの発端
現在T2 + Guice + GAE + mobylet + Slim3を使って画像uploadサイトを作ってます。
でそのなかでGAEはfileSystemを扱えないため、DatastoreにFileデータを入れて置かなければならない状態でした。
つまり画像をUpしたら、それを見るにはDatastoreから一回引っ張り出してResponseボディーに書き出す的なことをしなければなりません。
T2Frameworkの場合
T2Frameworkの場合直接Responseに書き出すには、Directナビゲーションを利用します。
ただこのクラスを利用する場合はHTTPのContentsTypeはPage側で指定しなければなりません。
つまりこんな感じ
res.setContentType("image/jpeg"); return Direct.from(img.getData());
で拡張しよう
contentTypeぐらい自前で頑張れば?と言われそうですが、まぁやってみたいんです。Navigationの拡張
だって男の子だもん。。。
ということでNavigationを拡張しました。
基本的にはDirectを継承して、ContentsTypeを判定できるようにしようと考えてこんな感じで作ってみました。
package com.google.code.stk.t2gae.commons.navigation; import java.io.File; import java.io.InputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import org.t2framework.commons.util.Assertion; import org.t2framework.commons.util.FileUtil; import org.t2framework.commons.util.StreamUtil; import org.t2framework.t2.contexts.WebContext; import org.t2framework.t2.navigation.StreamNavigation; import org.t2framework.t2.spi.Navigation; public class Img extends StreamNavigation { private String contentType; public Img(byte[] bytes) { super(bytes); ImgCodec codec = ImgCodec.getInstance(bytes); Assertion.notNull(codec); contentType = codec.contentType; } public Img(File file) { super(file); ImgCodec codec = ImgCodec.getInstance(FileUtil.getBytes(file)); Assertion.notNull(codec); contentType = codec.contentType; } public Img(InputStream is) { super(is); ImgCodec codec = ImgCodec.getInstance(StreamUtil.getBytes(is)); contentType = codec.contentType; } public Img(byte[] bytes , ImgCodec codec) { super(bytes); Assertion.notNull(codec); contentType = codec.contentType; } public Img(File file , ImgCodec codec) { super(file); Assertion.notNull(codec); contentType = codec.contentType; } public Img(InputStream is , ImgCodec codec) { super(is); Assertion.notNull(codec); contentType = codec.contentType; } public static Navigation jpg(byte[] bytes){ return new Img(bytes , ImgCodec.JPG); } public static Navigation gif(byte[] bytes){ return new Img(bytes , ImgCodec.GIF); } public static Navigation png(byte[] bytes){ return new Img(bytes , ImgCodec.PNG); } public static Navigation jpg(File file){ return new Img(file , ImgCodec.JPG); } public static Navigation gif(File file){ return new Img(file , ImgCodec.GIF); } public static Navigation png(File file){ return new Img(file , ImgCodec.PNG); } public static Navigation jpg(InputStream is){ return new Img(is , ImgCodec.JPG); } public static Navigation gif(InputStream is){ return new Img(is , ImgCodec.GIF); } public static Navigation png(InputStream is){ return new Img(is , ImgCodec.PNG); } public static Navigation from(byte[] bytes){ return new Img(bytes); } public static Navigation from(File file){ return new Img(file); } public static Navigation from(InputStream is){ return new Img(is); } @Override public void execute(WebContext context) throws Exception { final HttpServletResponse res = context.getResponse().getNativeResource(); res.setContentType(contentType); ServletOutputStream sos = res.getOutputStream(); super.writeTo(res, sos); sos.flush(); } public enum ImgCodec{ GIF("image/gif"), PNG("image/gif"), JPG("image/jpg"); private String contentType; private ImgCodec(String contentType){ this.contentType = contentType; } public String getContentType(){ return this.contentType; } public static ImgCodec getInstance(byte[] bytes){ if (bytes != null && bytes.length >= 2) { int b1 = bytes[0] & 0xFF; int b2 = bytes[1] & 0xFF; if (b1 == 0xFF && b2 == 0xD8) { return ImgCodec.JPG; } else if (b1 == 0x47 && b2 == 0x49) { return ImgCodec.GIF; } else if (b1 == 0x89 && b2 == 0x50) { return ImgCodec.PNG; } } return null; } } }
ちなみにImageの判定処理はmobyletを参考にさせていただきました。
であとはImageデータをFile or Stream or byte で取ってこのImg.from()とか呼べばOKです。
byte[]の書き出しのところ、BufferedOutputStreamとかつかったほうがいいのかな?
ちなみにgoogle code に公開したので、ご利用の方はどうぞ!
JavaDocはまぁそのうち。。。
T2+GAE+guice のPageクラス用GuiceModuleをAnnotation Processing Toolで作成します。
T2 Guice連携だとPageクラスをModuleで一つずつ読み込む必要があります。
意外とこれが面倒な作業で、Page作ったけどModuleに書き忘れてもっかいApplicationServer再起動なんてことはよくあります。
てことでめんどくさい作業はToolを作ってしまえと思いました。そこで最近よく見るようになってきたAnnotation Processing Tool(以下 APT)で
Pageクラス用Moduleを作成してみます。
まずAnnotationを処理するProcessorクラス
package jp.stk.tools.apt.processor; import java.util.List; import java.util.Map; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedOptions; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import jp.stk.tools.apt.AptConst; import jp.stk.tools.apt.generator.T2GuiceModuleGenerator; import org.t2framework.t2.tool.apt.PageMirror; import org.t2framework.t2.tool.apt.PageSettingsGenerator; import org.t2framework.t2.tool.apt.PageSettingsProcessor; /** * t2 page module processor * <pre> * t2のPageクラス用Moduleクラスを作成するProcessorクラスです。 * </pre> * @author soundTricker * */ @SupportedSourceVersion(SourceVersion.RELEASE_6) @SupportedAnnotationTypes("org.t2framework.t2.annotation.core.Page") @SupportedOptions({AptConst.OPTIONS_GENERATE_MODULE_NAME , AptConst.OPTIONS_OUTPUT_PACKAGE}) public class T2GuiceModuleProcessor extends PageSettingsProcessor { /** * GuiceModuleを作成します。 * <pre> * Pageクラスのメタデータより、GuiceModuleを作成します。 * </pre> * @see org.t2framework.t2.tool.apt.PageSettingsProcessor#generate(java.util.Map<java.lang.String,java.util.List<org.t2framework.t2.tool.apt.PageMirror>>, javax.annotation.processing.ProcessingEnvironment) */ @Override protected void generate(Map<String, List<PageMirror>> pageMetaMap,ProcessingEnvironment processingEnv) { PageSettingsGenerator generator = new T2GuiceModuleGenerator(pageMetaMap, processingEnv); generator.generate(); } }
PageSettingsProcessor はT2framework 0.6.3-cr1に入っていたpage.propertiesを作るAPTのProcessorクラスです。
ほとんどの処理はこれがやってくれちゃってますね。。。
でもバージョンがcrなのでなくなっちゃったらどうしよう。
次に実際にGuiceModuleを作成するGeneratorクラス
package jp.stk.tools.apt.generator; import java.io.IOException; /** * Pageクラス用のGuiceModuleを作成するGeneratorクラスです。 * @author soundTricker * */ public class T2GuiceModuleGenerator implements PageSettingsGenerator { //====================================================================== // プロパティ //====================================================================== /** * Pageクラスのメタデータマップ * <pre> * key:package名 * value:keyのパッケージ内Pageクラスメタデータのリスト */ protected Map<String, List<PageMirror>> pageMetaMap; /** APTの実行情報 */ protected ProcessingEnvironment processingEnv; /** writer */ protected PrintWriter writer; /** GuiceModuleの出力先パッケージ */ protected String outputPackage = ""; /** 作成するGuiceModule名 */ protected String generateModuleName = AptConst.DEFAULT_MODULE_NAME; //====================================================================== // コンストラクタ //====================================================================== /** * デフォルトコンストラクタ * <pre> * writerなどの設定を行います。 * </pre> * @param pageMetaMap Pageクラスのメタデータマップ * @param processingEnv APTの実行情報 */ public T2GuiceModuleGenerator(Map<String, List<PageMirror>> pageMetaMap,ProcessingEnvironment processingEnv) { this.pageMetaMap = pageMetaMap; this.processingEnv = processingEnv; Filer filer = this.processingEnv.getFiler(); Map<String , String> options = this.processingEnv.getOptions(); if(options.containsKey(AptConst.OPTIONS_OUTPUT_PACKAGE)){ outputPackage = options.get(AptConst.OPTIONS_OUTPUT_PACKAGE); } if(options.containsKey(AptConst.OPTIONS_GENERATE_MODULE_NAME)){ generateModuleName = options.get(AptConst.OPTIONS_GENERATE_MODULE_NAME); } FileObject fileObject = null; try { fileObject = filer.createSourceFile(outputPackage + AptConst.PACKAGE_SEPARATOR + generateModuleName); final Writer filerWriter = FilerUtil.openWriter(fileObject); this.writer = WriterUtil.createPrintWriter(filerWriter); } catch (IOException e) { CloseableUtil.close(this.writer); throw new IORuntimeException(e); } } //====================================================================== // メイン //====================================================================== /** * ソースを作成します。 * @see org.t2framework.t2.tool.apt.PageSettingsGenerator#generate() */ @Override public void generate() { try{ makeClassDefinition(); makeConfigureMethodDefinition(); for (Entry<String, List<PageMirror>> pageMetaEntry : pageMetaMap.entrySet()) { for (PageMirror pageMirror : pageMetaEntry.getValue()) { makeBindLogic(pageMirror); } } makeConfigureMethodFutter(); makeClassFutter(); FlushableUtil.flush(writer); }finally{ CloseableUtil.close(writer); } } //====================================================================== // 下請け //====================================================================== /** * クラス定義を作成します。 */ private void makeClassDefinition() { writer.format("package %s;\n" , outputPackage); writer.format("public class %s extends %s {\n", generateModuleName , AbstractModule.class.getName()); } /** * メソッド定義を作成します。 */ private void makeConfigureMethodDefinition(){ writer.format(" @Override\n"); writer.format(" protected void configure() {\n"); } /** * Pageクラスのメタデータよりbindロジックを作成します。 * @param pageMirror */ private void makeBindLogic(PageMirror pageMirror) { writer.format(" bind(%s.%s.class).in(com.google.inject.servlet.ServletScopes.REQUEST);\n" , pageMirror.getPackageName() , pageMirror.getPageClassName()); } /** * メソッドのフッターを作成します。 */ private void makeConfigureMethodFutter() { writer.format(" }\n"); } /** * クラスのフッターを作成します。 */ private void makeClassFutter() { writer.format("}\n"); } }
あと定数定義クラス。
package jp.stk.tools.apt; public class AptConst { public static final String OPTIONS_GENERATE_MODULE_NAME = "generateModuleName"; public static final String OPTIONS_OUTPUT_PACKAGE = "outputPackage"; public static final String DEFAULT_MODULE_NAME = "PageModule"; public static final String PACKAGE_SEPARATOR = "."; }
ただテストの仕方がわからなくて、
調べていたら、SeasarのsandboxにaptinaというAPT用のプロジェクトがありましたので、
こちらのhttp://aptina.sandbox.seasar.org/aptina-unit/index.html:Aptina Unitを使ってみました。
でこれがTestクラス
package jp.stk.tools.apt.processor; import java.io.IOException; import javax.tools.ToolProvider; import jp.stk.tools.apt.AptConst; import jp.stk.tools.apt.testpage.TestPage; import org.seasar.aptina.unit.AptinaTestCase; public class T2GuiceModuleProcessorTest extends AptinaTestCase { private static final String OPTION_FORMAT = "-A%s=%s"; protected void setUp() throws Exception { super.setUp(); addSourcePath("src/test/java"); } public void testProcess() throws IOException { ToolProvider.getSystemJavaCompiler().getClass(); T2GuiceModuleProcessor processor = new T2GuiceModuleProcessor(); addProcessor(processor); setCharset("UTF-8"); addOption(String.format(OPTION_FORMAT, AptConst.OPTIONS_OUTPUT_PACKAGE , "jp.stk.gae.module")); addCompilationUnit(TestPage.class); compile(); //TODO めんどくさいので表示だけ確認。。。ちゃんと作らなくちゃ。。。 System.out.println(getGeneratedSource("jp.stk.gae.module." + AptConst.DEFAULT_MODULE_NAME)); } }
これでテストしました。
今回はAPTをやってみました。
私適当なSVNを持っていないのでjarを公開できませんが、
質問がありましたらご連絡ください。
ちなみに最初Velocityでソースをつくるの(テンプレートの変更も可能)も書いていたのですが、
わざわざAPTのためにライブラリを落とすのも嫌かなと思ってべた書き版を載せました。
要望があったら載せます。
追加:
このソースだとAPTに引数を与えないとこけます。。。
時間があったら治します。
明けまして、おめでとうございます。
明けましておめでとうございます。
今年もゆるゆると書いていきたいと思います。
よろしくお願いいたします。
年末年始は、私結婚式があったため、全く更新できていませんでした。
前回の記事もサッサと書きたいですね。。。
で結婚式に向けて実はサイトを作っていました。(ぶっちゃけ間に合わなかったのですが。。。)
どんなサイトかというと、
「画像投稿サイト」
GAE上に結婚式の時に撮っていただいた携帯とかの画像を投稿していただいて、
投票できるようにして、優秀作品の人にはなんかプレゼント!みたいなことをしたかったのですが、
間に合いませんでした。。。
結婚式の画像とかってじぶんらは取れないので、
人にとってもらったのって結構大事なんですよね。
本当はQRコードを席札に乗っけてログインしてね!みたいにしたかったのですが。。。
もったいないので、友人の結婚式があったら提案してみます。。。
ちなみにFWは
T2 + Guice + GAE + mobylet + slim3 datastore
でした。
基本的な機能は全部できてたのですが、結婚式だとデザインにこだわりすぎて間に合いませんでした。
(他の準備で死にそうでしたし。。。)
T2 + Guiceは相変わらず使いやすいですね!
mobyletは非常に扱いやすい薄いFWで、携帯フレームワークはまだまだ少ないのでぜひ使っていきたいです。
ただ今後はi-PhoneやAndoroidなどの端末のことも考えていかなければならないので、
もっと研究しなきゃですね!
slim3 datastoreもJDOに比べて扱いやすかったです。
まだまだDDを勉強していかなきゃですね。。。
今回アップロード周りをかなり扱ったので、
時間が出来たらソースも上げていきたいと思います。