domingo, 31 de outubro de 2010

Desenvolvimento GWT usando MVP com Activities e Places

Esta semana, foi anunciado o esperadíssimo lançamento do GWT 2.1. Neste release, o GWT finalmente incluiu um framework MVP baseado nas best practices recomendadas no Google I/O 2009.

Este post é a tradução que fiz para a documentação deste novo framework. Particularmente achei a documentação pouco didática e relaxada com os novos usuários.

Para os usuários já acostumados com o estilo GWT de desenvolver (baseado em MVP/EventBus para promover baixo acoplamento e facilitar o teste unitário, e PlaceManagers atuando como controllers que gerenciam o estado da aplicação e a URL do browser), a solução proposta é interessante e elegante, padroniza um estilo de programação que antes ficaria a cargo do desenvolvedor arquitetar e implementar.

Para os novos usuários, a documentação é deveras extensa, e ficaria bem melhor documentada se fosse recheada com imagens e diagramas ao invés de apenas descrições e código. Deixo como promessa aos colegas entusiastas do GWT que, nas próximas semanas, envidarei esforços para didatizar e criar diagramas que facilitem este aprendizado.

Boa Leitura!


Tradução da Documentação Oficial publicada em http://code.google.com/intl/pt-BR/webtoolkit/doc/latest/DevGuideMvpActivitiesAndPlaces.html


O GWT 2.1 traz inclusa uma framework para desenvolvimento MVP. Este artigo demonstra as Activities e Places no GWT 2.1, relacionadas aos aspectos View e Presenter do MVP. Num artigo futuro, olharemos mais a fundo as novas funcionalidades para o Model (RequestFactory, EntityProxy e Data Binding). As funcionalidades relacionadas à Model podem ser usadas independentemente das Activities e Places, ou podem ser usadas em conjunto, usando um ProxyPlace e classes relacionadas.

Se o MVP é um conceito novo para você, dê uma olhada nestes artigos:

Definições

Uma Activity no GWT 2.1 é análoga ao presenter, na terminologia MVP. Ela não contém widgets ou código da UI. Atividades são iniciadas e encerradas por um ActivityManager associado a um Widget. Um nova e poderosa feature no GWT 2.1 é que uma atividade pode automaticamente exibir um alerta de confirmação, quando a atividade está prestes a ser encerrada - por exemplo quando o usuário navega para um novo Place. Além disso, o ActivityManager alerta o usuário quando a janela está prestes a ser fechada.

Um Place no GWT 2.1 é um objeto Java representando um estado particular da UI. Um Place pode ser converdido em um token de histórico de navegação (veja o objeto GWT History), e vice-versa, ou seja, uma URL com token de navegação pode ser traduzida em um Place. Para tal, define-se um PlaceTokenizer para cada Place, e o PlaceHistoryHandler automaticamente atualiza o navegador com a URL correspondente a cada Place da aplicação.

O código referenciado neste artigo pode ser baixado nesta aplicação de exemplo, que consiste em um simples "Hello World!" com uma view adicional para ilustrar a nevegação.

Vamos dar uma olhada em cada uma das partes de uma aplicação GWT 2.1 usando Places e Activities.

Views

Um conceito chave do desenvolvimento MVP é que uma View é definida por uma interface. Isto permite que múltiplas implementações da View, baseadas em características do cliente (por exemplo, se ele é mobile ou desktop), e torna mais leve o teste unitário, pois avita o uso do GWTTestCase, cuja execução é mais demorada.

O GWT não obriga que as interfaces View da aplicação extendam ou implementem alguma classe ou interface. O GWT 2.1 introduz uma interface IsWidget, implementada pela maioria dos Widgets GWT. Quando uma view provê um widget, é recomendável que ela estenda IsWidget, como no exemplo abaixo:

A implementação correspondente extende Composite, evitando que sejam expostas dependências a Widgets específicos da implementação.

Abaixo, uma view um pouco mais complicada, que declara uma interface para o Presenter (Activity) correspondente:

A interface Presenter e o método Presenter permite uma comunicação Bidirecional entre View e Presenter, que simplifica as interações envolvendo widgets repedidos, além de permitir implementações a usarem UiBinder com métodos @UiHandler que delegam chamadas diretamente para o Presenter, como no exemplo abaixo.

Segue o template uiBinder desta implementação:

Devido ao fato que a criação de widgets envolve operações DOM, views têm um custo alto de criação. Uma boa prática, portanto, é torná-las reusáveis, e uma forma relativamente fácil de fazer isto é através de uma View Factory, a qual pode fazer parte de uma ClientFactory.

ClientFactory

Apesar de não ser estritamente necessário, é muito útil fazer uso de fábricas, ou frameworks de Injeção de Dependências com o Google GIN, para obtenção de referência aos objetos necessários à aplicação - por exemplo o Event Bus. Este exemplo faz uso de uma ClientFactory para prover implementações para um EventBus, um PlaceController e para as Views.

Usando ClientFactories em conjunto com o mecanismo de deferred binding do GWT, é possível obter diferentes implementações de objetos, baseadas em propriedades como o user.agent. Por exemplo, a aplicação pode usar uma MobileClientFactoryImpl ou uma DesktopClientFactory.

Para tal, a ClientFactory deve ser instanciada usando GWT.create:

As classes de implementação devem ser especificadas no descritor gwt:

A tag pode ser usada para especificar diferentes implementações de acordo com o user.agent, com o locale, ou por propriedades próprias da aplicação. Segue um exemplo de implementação de uma ClientFactory:

Activities

Classes Activity devem implementar a interface com.google.gwt.app.place.Activity. Por conveniência, pode-se também extender a classe AbstractActivity, que provê implementações default vazias para cada um dos métodos requeridos. segue o código de Helloactivity, que simplesmente diz olá para um usuário:

A primeira coisa a se notar é que HelloActivity tem uma referencia HelloView. Uma forma de codificar MVP é definir a interface View no próprio Presenter. Apesar de ser um estilo perfeitamente legítimo, não há nenhuma razão fundamental para uma Atividade e sua interface View correspondente serem altamente acoplamas uma com a outra. Note que a atividade implementa a interface Presenter que foi declarada na View. Isto permite que a View possa chamar métodos da Atividade, o que fcilita o uso dos métodos @UiHandler do UiBinder.

O construtor de HelloActivity recebe dois argumentos: HelloPlace e ClientFactory. Nenhum é estritamente obrigatório para uma atividade. HelloPlace simplesmente facilita para HelloActivity a ontenção de propriedades d estado representado pelo HelloPlace (neste caso, o nome do usuário que estamos cumprimentando). A passagem de HelloPlace no construtor implica que uma nova HelloActivity deverá ser criada para cada HelloPlace.

No GWt 2.1, Activities são desenhadas para serem descartadas, enquanto as views, cujo custo de criação é mais caro devido às chamadas DOM envolvidas, devem ser reusáveis. Em concordância com esta idéia, ClientFactory é usada pela HelloActivity para obter as referências para HelloView, bem como ao eventBus e ao PlaceController, papel que pode também ser desempenhdo usando Injeção de Dependências com o Gin.

O método start é invocado pelo ActivityManager e coloca as coisas em ação. Ele atualiza a view e então a insere dentro do widget de conteúdo [da página de host], através da chamada

Ao implementar mayStop(), HelloView provê um alerta a ser mostrado para o usuário quando a atividade estiver prestes a ser encerrada devido ao fechamento da janela ou à navegação para outro Place. Caso retorne null, nenhum alerta será exibido.

Finalmente, o método goTo invoca o PlaceController para navegar para um novo Place. PlaceController, por sua vez, notifica o ActivityManager para encerrar a Activity atual, e encontrar / iniciar a Activity associada ao novo Place, além de atualizar a URL no PlaceHistoryHandler.

Places

Além de ser acessíveis via URL, uma Activity deve corresponder a um determinado Place, que deve estender com.google.gwt.app.place.Place, e ter um PlaceTockenizer que saiba como serializar seu estado para um token URL. Por padrão, a URML consiste no nome não-qualificado (sem prefixo referente ao nome pacote) do Place, seguido por dois pontos (:) e o token retornado pelo PlaceTokenizer.

É conveniente - apesar de não obrigatório - declarar o PlaceTokenizer como uma classe interna dentro do próprio Place. Porém, não é necessário ter um PlaceTokenizer para cada Place. Muitos Places da aplicação podem não precisar salvar estado na URL, então eles podem simplesmente estender BasicPlace, que declara um PlaceTokenizer que simplesmente retorna null.

PlaceHistoryMapper

É uma interface que serve para mapear todos os Places disponíveis na aplicação. Deve-se criar uma interface que estende PlaceHistoryMapper, e usa a anotação @WithTokenizers para listar cada uma das classes Tokenizer, como no exemplo abaixo:

Em tempo de compilação, o GWT gera uma classe baseada nesta interface, que é o elo entre os PlaceTokenizers da aplicação e os PlaceHistoryHandler do GWT que sincronizam a URL do navegador com o estado do Place.

Para se ter maior controle sobre o PlaceHistoryMapper, é possível usar uma anotação @Prefix para alterar a primeira parte da URL associada a um Place. Para um controle ainda maior, pode-se implementar um PlaceHistoryMapperWithFactory na aplicação para prover uma TokenizerFactory, que por sua vez provê os PlaceTokenizers.

ActivityMapper

Finalmente, a aplicação deve possuir um ActivityMapper para mapear Places em Activities. Segue um exemplo:

Uma implementação mais elegante do que esta cadeia de if/else's é realizar as configurações correspondentes em módulos do GIn.

Juntando as peças

O EntryPoint da aplicação ficará parecido com o exemplo abaixo:

Como isso tudo funciona

O ActivityMapper gerencia as Activities rodando no contexto de um determinado container widget [da página de host]. Ele escuta por PlaceChangeRequestEvents e notifica a atividade atual quando um novo Place é solicitado. Caso a Activity vigente permita a mudança do Place (quando Activity.mayStop() retorna null), ou quanto o usuário permite que o Place mude (ao clicar OK na caixa de diálogo), o ActivityManager descarta a Activity atual e inicia uma nova. Para determinar a nova Activity, o ActivityManager usa o ActivityMapper para obter a Activity associada ao novo Place solicitado.

Justamente com o ActivityManager, duas outras classes trabalham para gerenciar os Places da aplicação. PlaceController inicia a navegação para um novo Place, sendo responsável por alertar ao usuário sobre as transições entre Places. PlaceHistoryHandler provê um mapeamento bidirecional entre Places e a URL. Toda vez que a aplicação navega para um novo Place, a URL será atualizada com um novo token, representando o Place, de forma que ele pode ser salvo na lista de favoritos do navegador, bem como a URL seja adicionada ao histórico de navegação - permitindo que os botões voltar e avançar sejam corretamente interpretados pela aplicação. Inversamente, quando uma determinada URL é solicitada pelo navegador - seja ela digitada, clicada na lista de favoritos, ou respondendo a ao clique nos botões avançar ou voltar do navegador, o PlaceHistoryHandler assegura que a aplicação carregue o Place correspondente.

Como navegar

Para navegar para um novo Place na aplicação, basta chamar o método goTo() no PlaceController, fato que é ilustrado na chamada HelloActivity.goTo(). Através de um PlaceRequestEvent, PlaceController alerta avisa à Activity atual que ele deve estar parando, e uma vez que isto seja autorizado, dispara um PlaceChangedEvent, com o novo Place. O PlaceHistoryHandler escuta por PlaceChangeEvents e atualiza o token de histórico da URL da forma apropriada. O ActivityManager também escuta por PlaceChangeEvents, usando o ActivityMapper da aplicação para iniciar a Activity associada ao novo Place.

Ao invés de usar PlaceController.goTo(), pode=se também criar um hiperlink contendo o token para o novo Place. Quando o usuário navegar para a nova URL (seja via hiperlink, botões voltar/avançar, lista de favoritos, etc), PlaceHistoryHandler captura ValueChangeEvent do objeto History e chama o PlaceHistoryMapper para transformar o token no Place correspondente, e então invocar laceController.goTo() com o novo Place.

E caso aplicações que tenham múltiplos painéis na mesma janela, cujos estados devem ser todos salvos na mesma URL? GWT 2.1 não tenta prover uma implementação genérica de um Composite Plac; Porém, sua aplicação pode criar um CompositePlace, uma CompositeActivity, e um CompositePlaceTokenizer, delegando a eles esta responsabilidade. Neste caso, apenas os objetos composite deverão ser necessários para registro no ActivityMapper e PlaceHistoryMapper da aplicação.