Conhecendo padrões da SDK Android

Veja nesse artigo alguns exemplos práticos envolvendo os padrões Intent, Broadcast, AsyncTask, Loader e Handler.

Fique por dentro

Este artigo traz alguns dos padrões que a SDK do Android traz para ajudar no desenvolvimento. Estes padrões acabam algumas vezes sendo pouco ou subutilizados principalmente pelos desenvolvedores com pouca experiência na plataforma. Os padrões que serão apresentados são o intent, broadcast, asyntasx, loader e handler, service e adapter.

A SDK do Android traz com ela uma série de soluções para ajudar no desenvolvimento de aplicações e na otimização das aplicações que já sofrem bastante com as limitações de desempenho dos smartphones atuais. Neste artigo você vai conhecer um pouco sobre alguns dos principais padrões utilizados na plataforma que na sua maioria não são comuns de ver em outras plataformas como web ou desktop, mas que têm muita importância para a plataforma móvel principalmente por causa de suas limitações de recursos.


Guia do artigo:

Não poderia começar de outra forma se não falando sobre a classe Intent. Com certeza, a classe que gera mais dúvidas aos recém-chegados no desenvolvimento Android, justamente porque está presente já nas operações mais básicas do sistema.

Esta classe facilita a interação entre os diversos objetos que temos evitando acoplamento, questão que é de extrema importância no desenvolvimento móvel por conta da limitação de memória. Acoplamentos indesejados podem facilmente causar leaks de memória.

Uma Intent é uma descrição de uma operação a ser realizada que será executada pelo Android. Existem duas maneiras de escolher o componente a ser executado, pode-se explicitar utilizar o nome completo de sua classe ou utilizar uma ação genérica a ser realizada, com isso o sistema pode tomar a decisão de quem executará a operação, ou mesmo solicitar ao usuário que decida. Esta ação genérica é definida através de um filtro de intent declarado no manifesto.

Um Intent pode também carregar uma série de parâmetros, porém, estes parâmetros devem ser serializáveis para que caso necessário o Android os guarde de uma forma otimizada para posterior utilização.

Pode-se também usar flags para indicar comportamentos desejados durante transições de atividades. Desta forma podemos modelar o fluxo da aplicação para se adequar a suas necessidades. Existem uma série de flags disponíveis com variados efeitos e pode-se utilizar várias delas simultaneamente. Ao final do artigo há um link para a documentação atualizada delas.

A utilização de Intents é essencial para a comunicação entre atividades e recursos do Android. É possível fazer a requisição de dados em Content Providers, fazer a chamada de outras aplicações de forma específica ou genérica e até mesmo requisitar outra aplicação para que abra ou execute um recurso que sua aplicação não possui suporte, algo que é muito útil para reutilizarmos funcionalidades de outros aplicativos e ainda deixar que o usuário utilize suas ferramentas favoritas para o serviço.

O exemplo da Listagem 1 serve para realizar uma chamada de algum aplicativo de captura de fotos para tirar uma foto, salvar no arquivo desejado e retornar para nossa aplicação. Esta integração evita a escrita de um serviço para tirar fotos dentro de nossa aplicação.

Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File f = createImageFile(smu.getId()); mCurrentPhotoPath = f.getAbsolutePath(); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f)); ctxActivity.startActivityForResult(takePictureIntent, actionCode);
Listagem 1. Chamar aplicativo de câmera

Para fazer com que suas activities também possam ser chamadas de outras aplicações, basta declarar ela como exported e adicionar filtros para facilitar sua utilização.

Nota: Nunca utilize filtros e exporte seus serviços. Isto abre uma brecha em sua aplicação de forma que outro aplicativo possa interferir nos seus serviços. Além disso, justamente por não possuírem uma interface, será difícil para que o usuário identifique este comportamento indesejado.

Broadcast

Este padrão é amplamente utilizado pela SDK do Android e algumas funcionalidades dependem exclusivamente dele para que possam ser acessados. É muito semelhante ao padrão Observer, onde você registra um observador para que receba eventos relacionados ao objeto ouvido.

Você pode utilizar este padrão internamente para comunicar eventos que interessem à sua aplicação sem a necessidade de verificações constantes, ou para ouvir eventos lançados por outras aplicações ou pelo próprio sistema Android.

Existe uma variedade muito grande de Broadcasts que constantemente são lançados pelo sistema, cobrindo uma variedade de eventos que acontecem no dispositivo. Entre eles estão eventos de conexão com rede, captura de coordenada, SMS recebida entre muitas outras. A lista completa pode ser encontrada dentro do código-fonte da SDK e pode variar (geralmente de modo incremental) entre versões da SDK.

Como mencionado, temos dois lados para a utilização deste padrão, a parte ativa, que lança o broadcast, e a parte que recebe o broadcast. Para receber um broadcast, inicialmente é necessário uma classe que estenda da classe BroadcastReceiver e associe ela ao broadcast que se queira receber. Observe que isto é dinâmico, no momento que não mais for necessário o recebimento destes broadcasts deve-se desatrelar este objeto (enquanto atrelado, o objeto deverá receber todos os eventos relacionados).

Junto com o broadcast enviado é comum se enviar uma série de dados associados a ele, sejam eles relacionados ao evento ocorrido ou ao estado atual que ele possa ter causado. Desta forma, facilita-se o acesso a informações relacionadas e cria-se um evento consistente e independente. Os dados chegam como parte de um Intent. As Listagens 2 e 3 trazem um trecho com a implementação de recebimento do broadcast para avisar quando se está iniciando o sistema.

public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //Realiza a operação desejada ao receber o broadcast. } }
Listagem 2. Broadcast Receiver
<receiver android:name=".receiver.BootReceiver" android:enabled="false" android:permission="android.permission.RECEIVE_BOOT_COMPLETED" > <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" > </action> </intent-filter> </receiver>
Listagem 3. Android XML

Mas como já mencionado, também podemos enviar nossos próprios broadcasts para interagir com nossa própria aplicação ou até mesmo com outras aplicações que possam estar interessadas com algo que nossa aplicação pode fornecer.

O envio do broadcast é ainda mais simples, deve-se utilizar um nome único para seu broadcast, junto com o nome do pacote de sua aplicação e a partir daí apenas uma chamada a sendBroadcast() passando o Intent com a definição do broadcast. O recebimento do broadcast segue exatamente o mesmo exemplo já mencionado (veja a Listagem 4).

Intent i = new Intent("your.package.YOUR_ACTION"); sendBroadcast(i);
Listagem 4. Envio de Broadcast

AsyncTask

Sua finalidade é facilitar a interação entre threads com a manipulação de layout e feedback para usuário. Projetada de forma simplista, ela não prevê eventos externos como o ciclo de vida da Activity.

Ela não possui um limite relacionado a tempo de execução, porém como tem a finalidade de trazer um retorno para a thread principal (de layout), seu ciclo de vida passa a estar fortemente ligado à Activity que a criou, por isso é muito importante realizar verificações no momento de realizar as operações no layout (durante o método onPostExecute()), pois este pode não estar mais disponível. O mesmo vale para o feedback de progresso onProgressUpdate().

Pode-se facilmente reproduzir sua funcionalidade utilizando outros elementos, mas sua grande vantagem está justamente na simplicidade de sua implementação. Possuindo uma interface simples e vários callbacks relevantes, fica fácil criar uma solução para uma operação com tempo de execução incerto que seria inviável de se rodar na thread principal para que a capacidade de resposta e interações não sejam comprometidas. Sua interface é também genérica, o que deixa passível adaptação para quase qualquer solução através da adequação de parâmetros.

A partir da versão 3.0 do Android, todas as AsyncTasks passam a fazer uso de uma mesma thread em background, conhecida como worker. Desta forma, todas as AsyncTask irão rodar seu método doInBackground() serialmente, o que deixa ainda mais claro sua finalidade de rodar serviços curtos em segundo plano e disponibilizar o mais brevemente possível ao usuário. Veja o código da Listagem 5.

AsyncTask<Void, Integer, String> task = new AsyncTask<Void, Integer, String>() { @Override protected void onPreExecute() { //Executado na thread principal antes de iniciar processo em background. } @Override protected String doInBackground(Void... params) { //Executado em background. setProgress(0); return ""; } @Override protected void onProgressUpdate(Integer... values) { //Executado na thread principal durante o processo em background, // para que possa retornar feedback para usuario // de como está o progresso. } @Override protected void onCancelled(String result) { //Executado na thread principal caso seja chamado AsyncTask.cancel(). } @Override protected void onPostExecute(String result) { //Executado na thread principal após doInBackground() completar. } }; task.execute();
Listagem 5. Implementação AsyncTask

Observe que a implementação faz uso de conceitos provenientes do Java com seus três parâmetros genéricos que são obrigatórios e devem ser explicitados na implementação.

Loader

Os Loaders e sua principal implementação CursorLoader foram introduzidos na plataforma para melhorar a utilização de operações relacionadas a banco de dados. Eles já vêm preparados para o ciclo de vida da Activity e para entregar dados sempre atualizados, porém, depende de uma implementação de um Content Provider sobre o banco de dados. Todas as interfaces que dão acesso direto a informações que o Android disponibiliza vêm com a implementação de Content Provider.

A não existência de um Content Provider não inviabiliza completamente a utilização de Loaders para se carregar dados de um banco. Para isso existe a implementação AsyncTaskLoader que estende da Loader e traz várias de suas vantagens para uma implementação mais próxima do AsyncTask, porém, exige novamente um pouco mais de atenção especialmente quanto a atualização dos dados.

Diferentemente de uma AsyncTask onde você implementa a classe e realiza a operação em seus métodos, em um Loader você atrela a um LoadManager que é disponibilizado em fragments ou activities através do método getLoaderManager() e implementa uma interface de callback chamada LoaderCallbacks, onde serão recebidos eventos relacionados para se atualizar os dados apenas nos momentos necessários. Para inicializar a loader você utiliza o método initLoader() do LoaderManager, normalmente no método onCreate() da Activity ou onActivityCreated() do Fragment.

Handler e Looper

Em uma abstração bem simples, podemos comparar esse padrão com uma fila onde você adiciona as tarefas que serão executadas, as tarefas são executadas uma a uma serialmente sem concorrência entre si. A fila é executada completamente em cada ciclo do processo, por isso uma tarefa maior, sendo executada desta forma interfere no momento de início das próximas tarefas.

Este padrão já vem pré-implementado no Android pelas classes Activity, Fragment e Service, sendo as tarefas executadas na thread principal (a mesma do layout), o que torna de grande utilidade para a manipulação de objetos que interferem no layout, mesmo sendo estas tarefas disparadas de threads diferentes. Entretanto, é muito importante tomar cuidado com tarefas pesadas ao utilizá-las na Activity, pois o ciclo faz parte de toda a construção do layout, uma tarefa mais pesada pode comprometer o tempo de resposta de sua interface.

A especialização de threads e serviços pode encontrar aqui uma importante vantagem: podemos ter threads especializadas para certo aspecto da aplicação. Desta forma, podemos controlar precisamente sua prioridade quando ela está rodando e como ela vai interferir no restante do sistema, sem gerar uma grande complexidade.

Para rodar um objeto Runnable através de um Handler é preciso apenas utilizar o método post() ou postDelayed() caso queria que seja chamado em um tempo futuro.

No exemplo da Listagem 6 preparamos a thread para que possa rodar o looper. Em seguida criamos um objeto Handler com a implementação do método para recebimento de mensagens e em seguida iniciamos de fato o loop.

Looper.prepare(); mHandler = new Handler() { public void handleMessage(Message msg) { System.out.println(msg.obj); } };
Listagem 6. Looper e Handler

Looper.loop();

O método loop() da classe Looper é um método locker, no momento que ele for chamado esta thread ficará nela por tempo indefinido e resolverá as chamadas de serviços Handler.

O método handleMessage() do Handler será chamado mesmo que o método loop() do Looper ainda não tenha sido executado, porém os objetos Runnable passados por post através de um Handler só serão executados enquanto a thread estiver executando o método loop().

Em qualquer momento você pode parar o loop através dos métodos quitSafely() e quit() do próprio objeto Looper e que também podem ser acessados por qualquer Handler atrelados a este loop através do método getLooper().

Com o código apresentado na Listagem 7 você pode criar uma classe que executa funções em segundo plano para alguma operação de sua aplicação. Com esta implementação temos uma vantagem muito interessante que é o fato de suas operações rodarem serialmente.

//1. Através de mensagens Message msg = Message.obtain(); msg.obj = //obj mHandler.dispatchMessage(msg); //2. Através de Runnable mHandler.post(new Runnable() { @Override public void run() { //operação } });
Listagem 7. Chamada Handler

Service

Service é uma classe que pode rodar operações por um longo período em background, não possui uma interface gráfica e pode realizar comunicação entre threads e processos através de bind. A maior diferença de um Service para um Thread criada em uma Activity é justamente o fato dela rodar em background, pois uma Thread iniciada em uma Activity deve sempre ser parada ao sair desta Activity, caso contrário podem ocorrer comportamentos inesperados.

Um serviço apenas será interrompido no momento em que uma Activity em foco precisar de mais memória do que a disponível no momento, dando-se prioridade para manter serviços atrelados a Activity em foco e serviços declarados como foreground. Porém, caso seja declarado como ‘sticky’, ele voltará a ser executado assim que houver memória disponível.

Por padrão, um Service vai rodar na mesma thread que é utilizada para as operações de layout. Desta forma, operações pesadas podem afetar suas activities. É importante que você utilize outras threads para rodar operações mais pesadas, mas coordene elas através de seu Service. Assim você utiliza o ciclo de vida do Service e não afeta diretamente suas activities.

Temos também uma implementação pronta da classe Service chamada IntentService que traz um comportamento um pouco diferente, ela automaticamente repassa todas as chamadas de startService() para um método chamado onHandleIntent() com a intent recebida e na thread worker. Desta forma, os serviços são executados serialmente, evitando problemas com paralelismo e em segundo plano, muito semelhante ao que acontece com a AsyncTask, só que desatrelado da Activity (veja a Listagem 8).

public class OlaIntentService extends IntentService { public OlaIntentService() { super("OlaIntentService"); } @Override protected void onHandleIntent(Intent intent) { // Serviço realizado em background pela thread worker. } }
Listagem 8. IntentService

Temos também como definir um serviço de ‘foreground’, o que significa que este serviço é de uma operação extremamente importante para o usuário neste momento. Este tipo de serviço requer obrigatoriamente uma notificação ativa na barra de status, de modo que se a notificação for cancelada, o serviço também será cancelado. Exemplos desse tipo de serviço são players de música e um download em andamento. Para iniciar um serviço em foreground, você deve iniciar o serviço com o método startForeground(). Para removê-lo de foreground utilize stopForeground() (continuará como um serviço normal). Observe o código da Listagem 9.

Notification notification = new Notification(R.drawable.icon, getText(R.string.texto), System.currentTimeMillis()); Intent notificationIntent = new Intent(this, ExampleActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 123); notification.setLatestEventInfo(this, getText(R.string.title), getText(R.string.message), pendingIntent); startForeground(NOTIFICATION_ID, notification);
Listagem 9. Foreground Service

Dentre os tipos de serviço que podem ser utilizados, destaco:

Adapter

Este é mais um padrão que serve para ajudar a contornar as limitações dos devices. A implementação dele trabalha para manter a baixa utilização de memória e evitar ao máximo possível a criação de objetos de tela.

A SDK já traz consigo uma série de classes que implementam este padrão de forma que sua utilização seja facilitada. Para situações mais simples e comuns você já encontrará classes totalmente implementadas e prontas para uso (SimpleAdapter, SimpleCursorAdapter, ArrayAdapter), já em casos um pouco mais complexos você encontra alguma implementação parcial como, por exemplo, para percorrer listas diretamente do banco (CursorAdapter), para casos não previstos você terá que fazer sua própria implementação partindo da BaseAdapter.

Adapter é um padrão que se encaixa muito bem com diversos outros padrões para possibilitar uma usabilidade ainda mais fluída. Por exemplo, ele pode ser utilizado em conjunto com Holders e Tags para facilitar no reaproveitamento das views. Já a utilização de LruCache para a manipulação de objetos mais pesados como imagens pode ser algo muito importante, principalmente em smartphones com um menor poder de processamento.

Esta série de padrões pode ajudar muito no desenvolvimento de seus aplicativos e na performance de sua aplicação. Vale lembrar que estes não são os únicos padrões prontos para utilização na plataforma, existem muitos outros que você deve explorar para se beneficiar ao máximo desta plataforma de desenvolvimento.

Confira também

Artigos relacionados