quinta-feira, 11 de abril de 2013

Utilizando Injeção para DAOs


Para o meu projeto do CadernoVigilantes, tive novamente de alterar a tabela EntradaPontos, que armazena os pontos armazenados, um pedido recorrente entre os usuários do sistema. A alteração foi  mudar o tipo de dados do campo Quantidade, da tabela Entrada_Pontos do tipo NUMBER para o tipo REAL (que é como o sqlite representa o float).

Adicionando uma modificação anterior, tenho agora três versões da tabela:

- Versão 1: A tabela Inicial.
- Versão 2: Alterando o valor em pontos de NUMBER para REAL.
- Versão 3: Alterando o campo quantidade de NUMBER para REAL.

Isto me fez alterar o método de atualização de tabela do DAO, onUpgrade, para levar em consideração mais esta possibilidade. O onUpgrade fará as seguintes operações:

- Se a versão do usuário for 1, atualize a tabela da versão 1->2.
- Se a versão do usuário for menor que três, atualize a tabela da versão 2->3.

Testar se a modificação está sendo feita corretamente é um pouco complicado. Como o acesso a base sqlite é realizado através do Context, que só existe em tempo de execução, eu ainda não vi uma forma simples de utilizar o JUnit para a automatização. Além disto, eu possuía apenas um objeto EntradaPontosDAO, que sofre modificações a cada versão de tabela, no método onUpdate, no método onCreate, e na função que transforma os dados em um objeto EntradaPontos.

Ou seja, para testes de regressão com versões anteriores do DAO, eu teria que garantir que a criação e atualização da tabela estava ocorrendo corretamente com os usuários, não importa qual a versão anterior deles. O que implicaria, a grosso modo:

1)  alterar o código para a versão anterior, injetar a apk no celula
2) adicionar dados
3)  modificar novamente o DAO para uma versão posterior
4)  injetar o apk novamente
5) verificar se o comportamento da atualização estava ocorrendo corretamente, e se a funcionalidade não estava afetada.

Tudo manualmente, com um grande componente de risco nas etapas 1 e 3. Foi o que eu fiz da versão 1 para a 2, só que agora as etapas teriam de ser repetidas da versão 1->2, 1->3 e 2->3 e novo 3, com boas chances de trazer uma versão do DAO errada do CVS em cada ponto. Esta obviamente não era uma boa opção.

Foi hora de usar as boas práticas. Primeiramente, eu fiz um refactoring da versão atual da classe EntradaPontosDAO para EntradaPontosDAO3. Então, eu extraí uma interface que representasse o EntradaPontosDAO3, que eu chamei de EntradaPontosDAO, que possuía apenas os métodos que estão sendo utilizados por objetos externos. Então, coloquei a EntradaPontosDAO3 como implementação do EntradaPontosDAO.


Isto me permitiu apontar para a interface, e não para o objeto de acesso a dados, o que é sempre uma boa prática. Isto me permitiu adicionar três novas classes, cada uma representando uma versão da tabela:


Então, agora só precisarei definir como realizar a injeção de qual DAO específico eu quero utilizar na minha classe de negócios. Para isto, utilizarei o RoboGuice.

Primeiramente, vou adicionar a notação de injeção nos construtores das três implementações, como no exemplo abaixo:


    @Inject
    public EntradaPontosDAO1(Context context) {
        super(context, TABELA, null, VERSAO);
    }

Isto indicará que o objeto DAO irá receber automaticamente o contexto do ambiente através do RoboGuice, conforme definido no ContextProvider (entrarei mais em detalhes sobre isto em outro post).

Deixando indicado no construtor da minha classe controladora que irá receber a injeção do DAO:

    @Inject
    protected ControleCaderno(EntradaPontosDAO entradaPontosDAO){
        super(entradaPontosSQL);
    }


Desta forma, quando o objeto de controle for criado, ele receberá automaticamente o objeto de acesso a dados, representado pela sua interface.

Finalmente, é só uma questão de definir na classe de módulo qual será a instância da interface que será injetada na interface:


public class ModuloCaderno extends AbstractAndroidModule {
    @Override
    protected void configure() {
        bind(EntradaPontosDAO.class).to(EntradaPontosDAO3.class);
        bind(ControleCaderno.class).asEagerSingleton();
    }
}

Qual foi a vantagem? Bom, agora eu tenho as três versões do DAO mantidas e estáveis, e apenas um ponto de amarração para definir qual das três classes será utilizada, com mínimo acoplamento. Tudo o que eu preciso para gerar um apk com os DAOs históricos 1,2 ou 3 é alterar o bind que é realizado no código acima para a classe desejada e regerar o pacote. O meu teste foi simplificado em muito, o risco diminuiu consideravelmente, e seguindo isto para os outros DAOs da minha aplicação, eu tenho uma aplicação mais simples de manter e testar, de forma geral. 

Nova Versão a Caminho - Mais Updates

Estou trabalhando na versão 1.4 do Caderno Vigilantes. Fiz um refactoring maior do código, tirando algum lixo acumulado, das versões anteriores, antes do uso do RoboGuice.

Está no planejamento da versão 1.4:
- Permitir a entrada de quantidades fracionárias (feito).
- Exportação do histórico de pontos.

Outras modificações sugeridas pelos usuários são desafios para uma versões posteriores:
- Gráfico de consumo de pontos.
- Widget de consumo de pontos.
- Adição de temas.
- Login e senha para armazenamento em um repositório.

Finalmente consegui disponibilizar o código no GIT, fica aqui: https://github.com/pcontop/CadernoVigilantes.git. O Intellij, a IDE que eu estou utilizando para o projeto, não permite o uso do CVS e do GIT ao mesmo tempo. Confiante de que a versão atual está razoavelmente estável, abdiquei do CVS para o GIT (naturalmente, eu tenho um backup guardado). Agora vou ter de adquirir experiência no uso da ferramenta nova.

domingo, 27 de maio de 2012

Problemas e mais problemas...


Entreguei a versão 1.1.1 da aplicação e decidi iniciar um refactoring e uma limpeza de código para seguir para as próximas etapas. Adicionei também um comportamento de deslizar para baixo para trazer o calendário na página do dia, e uma animação da transição. Então, ao realizar um teste de integridade com o que eu havia feito, um problema sério apareceu.

O problema ocorreu quando eu mudei a versão o apk. O dialog que eu uso para definir as Metas de Pontos aparecia assim no APK 7:


e ficou desse jeito depois de trocar para o APK 13:



E isso já estava publicado em produção faz um dia. É claro que eu deveria ter testado isso, e é claro que esse comportamento não mudaria, não é mesmo? Nunca lance nada com pressa, é nesse momento que os grandes bugs nascem!

O que aconteceu não foi bem um bug da APK 13, muito pelo contrário. Eu já estava definindo a largura e altura do dialog usando o método dialog.getWindow().setLayout(200,300). O que ocorre é que estas dimensões simplesmente não eram respeitadas no APK 7. Qualquer valor que eu colocasse, a Dialog apareceria da mesma forma! Em algum momento entre a APK 7 e a 13, isso foi corrigido, e as dimensões começaram a ser respeitadas. E a lei de Murphy, por sua vez, fez com que as dimensões que eu estava utilizando fossem muito pequenas, truncando o Dialog. Isso só foi aparecer na versão corrigida da APK!

Depois de descobrir que o problema foi esse, tive de ajustar o tamnho do Dialog. Isso levou a outro problema, pois os botões de + e - ficaram descentralizados no Dialog mais largo. Aparentemente, as imagens estavam descentralizadas de forma imperceptível na imagem original, e com o aumento largura do botão, isso foi exacerbado. Tive no final de trocar as imagens por outras.

Lancei a versão 1.1.5 no market:
- Adição do gesto de deslizar para baixo para pular da listagem do dia para o calendário.
- Adição de animação ao se mudar da listagem do dia para o calendário.
- Refactoring das classes do projeto. Estou separando o comportamento dos gestos e dos intents em classes estanques, para depois ter comportamentos diferenciados quando adicionar os fragments.
- Correção do erro de layout dos Dialogs de definição de Metas de Pontos.
- Correção do erro de refresh das Metas de Pontos do dia.

Problemas de Compatibilidade e Versão

Já tenho uma base de mais de 100 pessoas que baixaram o aplicativo!

A atualização com as propagandas estava demorando demais a entrar. Resolvi checar no google play, e havia um problema de compatibilidade. A versão 1.1 estava desabilitada, e quando eu tentava habilitar, tinha esse retorno do Google Play:
Erro: os APKs compatíveis com níveis maiores de API devem ter códigos de versão superiores aos dos APKs compatíveis com níveis mais baixos de API.


O que aconteceu foi o seguinte: Quando eu coloquei as informações de versão mínima e máxima de APK no manifest, eu adicionei a seguinte entrada no xml, conforme indicado pelo AdMob:

    <uses-sdk android:minSdkVersion="8"
              android:maxSdkVersion="13"
              android:targetSdkVersion="13"/>

Mas eu não havia definido o maxSdkVersion na versão 1.1! O que resultou que a versão 1.0 tinha o APK máximo 16+ (ou seja, o máximo), e a 1.1 tinha o APK máximo 13. Isso não é aceitado pelo Google Play.

Resolvi alterar o apk, colocando o 4.0.3 (porque não?), e mudei o xml para:


    <uses-sdk android:minSdkVersion="8"
              android:targetSdkVersion="13"/>


E lancei uma versão 1.1.1, enviando o APK para o Google Play. Desta vez, funcionou sem problemas (tive de desativar a versão 1.1 - que já não estava funcionando mesmo).

Vamos ver agora o que acontece.

Semana que vem, começarei a trabalhar na versão 1.2. Vou melhorar o layout da aplicação e a usabilidade.

quarta-feira, 23 de maio de 2012

Consegui adicionar o admob no meu aplicativo. Foi relativamente simples.

- Mudei o meu sdk android para o 3.1 (estava compilando antes com o 2.1), ajustando o target de compilação para o SDK 13 (3.1). Demorei um pouco para me achar no IntelliJ (cada IDE tem as suas peculiaridades).

- Adicionei o jar do Admob, que me foi passado pelo site no classpath do projeto.

- Fiz as alterações indicadas no AndroidManifest, adicionando as permissões de acesso a INTERNET e ACCESS_NETWORK_STATE, e o mapeamento para a activity especial do AdMob.

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
        <activity android:name="com.google.ads.AdActivity"                  android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"/>
Alterei também as informações do uses-sdk:
    <uses-sdk android:minSdkVersion="8"
              android:maxSdkVersion="13"
              android:targetSdkVersion="13"/>

- Depois, foi uma questão de adicionar a View do Admob nas minhas telas, passando a informação do meu id de publisher do admob (espero ter pego o id correto no site, esta parte não está muito explicada). Encapsulei isto tudo em uma método estático em uma classe nova, AdicionaPropaganda, para evitar repetição de código (prática semelhante que eu tenho feito para os outros pop-ups que utilizo, e que facilitou a minha vida):

public class AdicionaPropaganda {
    public static String MY_AD_UNIT_ID="...";
    public static void adicioneAdView(Activity activity, LinearLayout paginaDestino){
        AdView adView = new AdView(activity, AdSize.BANNER, MY_AD_UNIT_ID);
        paginaDestino.addView(adView);
        adView.loadAd(new AdRequest());
    }
}

- Então, foi só adicionar a chamada logo depois do build do layout, nas duas activities do meu projeto, como no exemplo abaixo:

        setContentView(R.layout.pagina_dia);
        AdicionaPropaganda.adicioneAdView(this, paginaDia);

Quem já conhece um pouco de android pode achar que eu pulei alguma linha entre o setContentView e o uso de paginaDia. Na verdade, o conteúdo de paginaDia é injetado pelo RoboGuice implicitamente depois do setContentView, conforme indicado no  InjectView que eu declarei juntamente com as outras declarações de classe:

    @InjectView(R.id.pagina_layout_dia) LinearLayout paginaDia;

Falo mais sobre o RoboGuice depois, mas ele realmente diminui muito o boilerplate do código!

- Regerei o pacote apk assinado, e o enviei para o market. O próximo update adicionará a propaganda e fará os centavos jorrarem em minha conta! Ou assim espero...


Conclusões: Infelizmente, o admob não funciona no android sdk 7 (2.1), o que fez o sdk mínimo do projeto subir para 8 (2.2). Queria ter sido avisado disso antes! Em último caso, espero que o market seja esperto o suficiente para não forçar este update para aqueles que possuem APIs inferiores a minSDKVersion declarada no manifest.

terça-feira, 22 de maio de 2012

Caderno Vigilantes


Publiquei hoje a minha primeira aplicação no Android Market, o Caderno Vigilantes, uma ferramenta de anotação para pessoas que controlam pontos em dietas como a Vigilantes do peso. Estou oficialmente no mundo dos fornecedores! 

Colocarei novas mensagens detalhando o processo de criação da aplicação e as minhas experiências ao tentar colocar a propaganda e monetizar a aplicação (já me inscrevi na admob, mas aparentemente, eu preciso da página da aplicação antes de me registrar). Também manterei aqui o histórico das atualizações.

Até agora, a aplicação não apareceu no Google Play. Postarei mais quando as coisas acontecerem.