Injeção de dependências no Android com RoboGuice
“É 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
, usada, entre outras coisas, para:
- Criar componentes de tela (
View
s); - 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 (
Intent
s); - 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 String
s 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 String
s, Manager
s, ou View
s. Ou seja, o Context
acaba agindo como uma grande Factory global.
No nosso caso o que realmente precisamos são as String
s das URLs guardadas em strings.xml
. Seria interessante já recebermos diretamente essas String
s 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 TextView
s 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 View
s 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
.
Programação, Mobile, Front-end, Design & UX, Infraestrutura e Business
Muito boa a iniciativa do RoboGuice! Parabéns :D. Mas caso eu precise de uma injeção de uma BaseApplicationContext() eu consigo obter com o RoboGuice?
Opa Douglas! Valeu!
Dê uma olhada aqui. É o que você quer?
Isso mesmo Toshi vlw!
e quando a desempenho, consumo de bateria, performance? alguma análise desse tipo?
e quanto a desempenho, consumo de bateria, performance? alguma análise desse tipo?
Sensacional!
Annotations são muito importantes para o processo de desenvolvimento ágil, injeção de dependência então nem se fala…
Parabéns Toshi, muito bom o post. Vou testar aqui! 🙂
Oi Jose,
Fiz alguns testes para tentar medir o impacto na performance. Existe uma perda, mas
mesmo rodando no emulador é quase imperceptível. Como ele exige um pequeno processamento
a mais vai acabar consumindo um pouco mais de memória, mas nada que se compare a usar os sensores, por exemplo.
Legal Diogo!
Valeu! Se der depois posta um feedback aqui sobre o que achou dele 🙂
Graaande, Toshi!
Muito bom meeesmo!
Valeu por compartilhar 😀
Valeu Rayssa!
Aproveitando o post, o Mike Burton acabou de divulgar na lista do RoboGuice o RoboGuice 2.0 RC1!
Quem quiser aproveite pra testar! E mandar um feedback pra ele!
Tem a lista de discussão do RoboGuice também: roboguice@googlegroups.com
Excelente post e excelente ferramenta! Vou utilizar com certeza.
Obrigado pela informação.