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 (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.

12 Comentários

  1. Douglas 15/03/2012 at 15:08 #

    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?

  2. Toshi 16/03/2012 at 00:21 #

    Opa Douglas! Valeu!
    Dê uma olhada aqui. É o que você quer?

  3. Douglas 16/03/2012 at 16:41 #

    Isso mesmo Toshi vlw!

  4. jose 16/03/2012 at 23:49 #

    e quando a desempenho, consumo de bateria, performance? alguma análise desse tipo?

  5. jose 16/03/2012 at 23:49 #

    e quanto a desempenho, consumo de bateria, performance? alguma análise desse tipo?

  6. Diogo Souza 17/03/2012 at 10:08 #

    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! 🙂

  7. Toshi 17/03/2012 at 11:28 #

    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.

  8. Toshi 17/03/2012 at 11:31 #

    Legal Diogo!
    Valeu! Se der depois posta um feedback aqui sobre o que achou dele 🙂

  9. Rayssa 19/03/2012 at 16:39 #

    Graaande, Toshi!
    Muito bom meeesmo!
    Valeu por compartilhar 😀

  10. Toshi 21/03/2012 at 00:37 #

    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

  11. Jônatas 08/05/2012 at 13:37 #

    Excelente post e excelente ferramenta! Vou utilizar com certeza.
    Obrigado pela informação.

Deixe uma resposta