Injeção de dependências no Android com RoboGuice

Injeção de dependências no Android com RoboGuice
toshi
toshi

Compartilhe

"É quase impossível escrever uma app Android que não se pareça com uma app Android". Com essas palavras Michael Burton apresentou o RoboGuice (lê-se "robojuice"), uma ferramenta de injeção de dependências no Android, aos desenvolvedores brasileiros na AndroidConf Brasil 2011. A estrutura do código de uma app Android é fortemente baseada na classe [Context](http://developer.android.com/reference/android/content/Context.html), usada, entre outras coisas, para:

  • Criar componentes de tela (Views);
  • Acessar recursos pré-definidos pelo desenvolvedor (imagens, arquivos de áudio, xml's de configuração, etc - disponíveis no diretório res);
  • Acessar o sistema de arquivos;
  • Criar bancos de dados;
  • Enviar mensagens para telas, serviços rodando em background e receptores de bradcast (Intents);
  • Obter objetos responsáveis (managers) por algumas funcionalidades do aparelho (enviar SMS, verificar sensores, alarme, entre outros).

Por esse motivo, em muitos pontos do código dependemos de uma instância de Context. Acabamos tendo algo parecido como uma variável global, pior ainda, muitas vezes vamos passá-la por referência para todos os lados.

As telas de um aplicativo Android estão sempre acompanhadas de uma classe filha de Activity (que por sua vez também é um Context), responsável pela lógica associada. Essa classe define o comportamento em cada etapa do ciclo de vida da tela. Para isso é possível implementar alguns métodos que serão chamados pelo sistema em cada etapa:

 public class MinhaTela extends Activity { protected void onCreate(Bundle savedInstanceState) {        // Chamado assim que a Activity é criada } protected void onStart() { // Chamado logo após o onCreate } protected void onResume() { // Chamado após a tela ser criada depois do onStart } protected void onPause() { // Chamado antes da tela não ser mais visível para o usuário } protected void onStop() {        // Chamado depois que a tela não é mais visível    }    protected void onDestroy() {        // Chamado antes da Activity ser destruída    } } 

Muitas vezes precisamos utilizar algumas das funcionalidades disponíveis em Context (chamar outra Activity, por exemplo). É comum fazermos isso diretamente no código da Activity atual, já que ela herda de Context. Assim não é raro encontrar Activities grandes e com muitas responsabilidades. Tome como exemplo a seguinte Activity de uma tela de cotações. Ela busca o valor das cotações atualizadas na internet:

 public class TelaCotacoes extends Activity {    private TextView textoCotacaoDolar;    private TextView textoCotacaoEuro;    private TextView textoCotacaoPeso;

protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.tela\_cotacoes);

       textoCotacaoDolar = (TextView) findViewById(R.id.cotacaoDolar);        textoCotacaoEuro = (TextView) findViewById(R.id.cotacaoEuro);        textoCotacaoPeso = (TextView) findViewById(R.id.cotacaoPesoArgentino);         String urlDolar = getString(R.string.url\_dolar); String urlEuro = getString(R.string.url\_euro); String urlPeso = getString(R.string.url\_peso);        String cotacaoDolar = buscaCotacao(urlDolar);        String cotacaoEuro = buscaCotacao(urlEuro);        String cotacaoPeso = buscaCotacao(urlPeso);

textoCotacaoDolar.setText(cotacaoDolar); textoCotacaoEuro.setText(cotacaoEuro); textoCotacaoPeso.setText(cotacaoPeso); } private String buscaCotacao(String urlDaCotacao) { HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(urlDaCotacao); String cotacao = null; try { HttpResponse response = client.execute(get); cotacao = EntityUtils.toString(response.getEntity()); } catch (ClientProtocolException e) { // Trata exception } catch (IOException e) { // Trata exception } if(cotacao == null) { cotacao = "Cotação indisponível"; } return cotacao; } protected void onStart() { // Chamado logo após o onCreate } protected void onResume() { // Chamado após a tela ser criada depois do onStart } protected void onPause() { // Chamado antes da tela não ser mais visível para o usuário } protected void onStop() {        // Chamado depois que a tela não é mais visível    }    protected void onDestroy() {        // Chamado antes da Activity ser destruída    } } 

Acabamos com um código muitas vezes pouco orientado a objetos, com muitas responsabilidades concentradas na Activity. A estrutura do código de uma app Android nos induz a esse tipo de design. Repare que uma parte considerável do código é responsável pela busca das cotações atualizadas na internet. Podemos então extrair esse comportamento para outra classe:

 public class ValoresDeCotacao { public String buscaDolar() { String urlDolar = // Preciso chamar o método getString(R.string.url\_dolar) a partir de um Context return buscaCotacao(urlDolar); }

public String buscaEuro() { String urlEuro = // Preciso chamar o método getString(R.string.url\_euro) a partir de um Context return buscaCotacao(urlEuro); }

public String buscaPesoArgentino() { String urlPeso = // Preciso chamar o método getString(R.string.url\_peso) a partir de um Context return buscaCotacao(urlPeso); }

private String buscaCotacao(String urlDaCotacao) { HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(urlDaCotacao); String cotacao = null; try { HttpResponse response = client.execute(get); cotacao = EntityUtils.toString(response.getEntity()); } catch (ClientProtocolException e) { // Trata exception } catch (IOException e) { // Trata exception } if(cotacao == null) { cotacao = "Cotação indisponível"; } return cotacao; } } 

Aqui temos um problema para conseguir as Strings das URLs que queremos acessar (definidas em strings.xml), pois precisamos de uma instância de Context para chamar o método getString. Podemos então receber um Context no construtor de nossa classe:

 public class ValoresDeCotacao { private final Context context;

public ValoresDeCotacao(Context context) { this.context = context; }

public String buscaDolar() { String urlDolar = context.getString(R.string.url\_dolar); return buscaCotacao(urlDolar); }

public String buscaEuro() { String urlEuro = context.getString(R.string.url\_euro); return buscaCotacao(urlEuro); }

public String buscaPesoArgentino() { String urlPeso = context.getString(R.string.url\_peso); return buscaCotacao(urlPeso); }

private String buscaCotacao(String urlDaCotacao) { HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(urlDaCotacao); String cotacao = null; try { HttpResponse response = client.execute(get); cotacao = EntityUtils.toString(response.getEntity()); } catch (ClientProtocolException e) { // Trata exception } catch (IOException e) { // Trata exception } if(cotacao == null) { cotacao = "Cotação indisponível"; } return cotacao; } } 

Nosso problema foi resolvido, entretanto existe o inconveniente de se ter que passar uma instância de Context para o nosso ValoresDeCotacao. Pior ainda, todas as classes que precisarem realizar qualquer interação com o sistema precisarão receber um Context.

Acontece que em muitos casos não precisamos do Context em si, mas de algum recurso fornecido por ele, como Strings, Managers, ou Views. Ou seja, o Context acaba agindo como uma grande Factory global.

No nosso caso o que realmente precisamos são as Strings das URLs guardadas em strings.xml. Seria interessante já recebermos diretamente essas Strings ao invés de receber todo o Context. O RoboGuice é capaz de injetar essas dependências da seguinte maneira:

 public class ValoresDeCotacao { @InjectResource(R.string.url\_dolar) private String urlDolar; @InjectResource(R.string.url\_euro) private String urlEuro; @InjectResource(R.string.url\_peso) private String urlPeso; public String buscaDolar() { return buscaCotacao(urlDolar); }

public String buscaEuro() { return buscaCotacao(urlEuro); }

public String buscaPesoArgentino() { return buscaCotacao(urlPeso); }

private String buscaCotacao(String urlDaCotacao) { HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(urlDaCotacao); String cotacao = null; try { HttpResponse response = client.execute(get); cotacao = EntityUtils.toString(response.getEntity()); } catch (ClientProtocolException e) { // Trata exception } catch (IOException e) { // Trata exception } if(cotacao == null) { cotacao = "Cotação indisponível"; } return cotacao; } } 

A TelaCotacoes agora depende de uma instância de ValoresDeCotacao. Para isso devemos herdar de RoboActivity ao invés de Activity e pedir para o RoboGuice injetar essa dependência:

 public class TelaCotacoes extends RoboActivity { @Inject private ValoresDeCotacao cotacoes;

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);

TextView textoCotacaoDolar = (TextView) findViewById(R.id.cotacaoDolar); TextView textoCotacaoEuro = (TextView) findViewById(R.id.cotacaoEuro); TextView textoCotacaoPeso = (TextView) findViewById(R.id.cotacaoPesoArgentino); textoCotacaoDolar.setText(cotacoes.buscaDolar()); textoCotacaoEuro.setText(cotacoes.buscaEuro()); textoCotacaoPeso.setText(cotacoes.buscaPesoArgentino()); } } 

Repare que o código da TelaCotacoes ficou mais enxuto, mas ainda temos código que não está associado à lógica da tela (busca dos TextViews e a configuração do layout). As chamadas a findViewById e setContentView são uma infraestrutura necessária para a lógica. O problema é que se tivermos muitas Views as chamadas a findViewById podem dificultar o entendimento da nossa lógica de negócios, pois poluirá o código. Podemos mais uma vez recorrer ao RoboGuice para injetar essas dependências no nosso código:

 @ContentView(R.layout.tela\_cotacoes) public class TelaCotacoes extends RoboActivity { @Inject private ValoresDeCotacao cotacoes; @InjectView(R.id.cotacaoDolar) private TextView textoCotacaoDolar; @InjectView(R.id.cotacaoEuro) private TextView textoCotacaoEuro; @InjectView(R.id.cotacaoPesoArgentino) private TextView textoCotacaoPeso; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); textoCotacaoDolar.setText(cotacoes.buscaDolar()); textoCotacaoEuro.setText(cotacoes.buscaEuro()); textoCotacaoPeso.setText(cotacoes.buscaPesoArgentino()); } } 

A extração de classes e métodos para aumentar a divisão de responsabilidades é possível (e recomendável) mesmo sem o RoboGuice, mas o simples fato de muitas vezes precisarmos de uma instância de Context pode nos desestimular a fazer isso. Com a injeção de dependências não temos mais essa preocupação, já que ele se responsabiliza por guardar essa instância, permitindo que foquemos mais em boas práticas de orientação a objetos.

Além da divisão de responsabilidades, utilizando injeção de dependências facilitamos a criação de testes de unidade, já que podemos testar cada uma das dependências e depois injetar mocks em seu lugar, sem ficar sempre mockando a infraestrutura do Android, como a classe Context.

Veja outros artigos sobre Mobile