quinta-feira, 28 de junho de 2018

Rumo ao Certificado Android: Multithread com Executors

O que é Executors?
É uma forma segura de lidar com threads no Android sem ter as inconveniências de usar o Runnable padrão do Java.

Como implementar?
Aqui usaremos o padrão Singleton, para termos apenas uma referência ao Executor (podemos ter mais de um se desejar, mas é bom manter apenas uma referência para cada executor). Criamos um novo executor implementando uma outra classe que herda Executor, mas também podemos usar a classe Executors para outras formas de instanciar, como usando o método newSingleThreadExecutor ou newFixedThreadPool.

01. public class AppExecutor{
02.    private static final Object LOCK = new Object();
03.    private static AppExecutors instance;
04.    private final Executor executor;


05.    private AppExecutor(Executor executor){
06.       this.executor = executor;
07.    }


08.    public static AppExecutors getInstance(){
09.       if(instance==null){
10.          synchronized(LOCK){
11.             instance = new AppExecutor(ThreadExecutor()));
12.          }
13.       }
14.       return instance;
15.    }

16.    public Executor getExecutor(){
17.       return executor;
18.    }

19.    private static class ThreadExecutor implements Executor{
20.       private Handler threadHandler = new Handler(Looper.getMainLooper());

21.       @Override
22.       public void execute(@NonNull Runnable command){
23.         threadHandler.post(command);
24.       }
25.    }
26. }


Rumo ao Certificado Android: Banco de Dados SQLite com Room

O que é Room?
Room é uma biblioteca do Android para mapeamento das tabelas de um banco SQLite para Objetos, em outras palavras um ORS (Object-relational mapping - Mapeamento objeto-relacional).

Ajustando dependências
Como qualquer outra biblioteca facultativa do Android, o Room deve ser importado no Gradle para poder ser usado. Para isso, vamos criar uma dependência:

dependencies{
    implementation "android.arch.persistence.room:runtime:1.0.0"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
}


Criando uma Entity
Entities são objetos que representam uma tabela no banco de dados. 
No código abaixo, nas linhas 1 e 2, vemos que Room utiliza classes com a anotação @Entity para criar as tabela antes da declaração da classe. Caso o nome da classe é diferente do nome da tabela, podemos indicar o nome da tabela que queremos no parâmetro da anotação.

Nas linhas 3-9 temos os campos do objeto, que por padrão vão ser as colunas da nossa tabela. Para criar um campo que não deva ser mapeado, devemos usar a anotação @Ignore (linha 8). Para definirmos uma chave primaria, usamos a anotação @PrimaryKey com parâmetro autoGenerate para deixarmos o banco de dados gerar os valores ou nós gerarmos. Não esqueça de criar os Getters e Setters (linha 22-26).

Também vale lembrar que o Room não permite que nossa classe tenha dois construtores (linha 10-21). Isso porque o Room usará construtores para popular os dados. Caso você queira ter um segundo construtor, você pode usar a anotação @Ignore no construtor (ou construtores) que você não deseja que o Room utilize (linha 15-21).

01. @Entity(tableName="name")
02. public class NameEntry{

03.   @PrimaryKey(autoGenerate=true)
04.   private int id;
05.   private String name;
06.   private int priority;
07.   private Date update;
08.   @Ignore
09.   private int dontIncludeMe;

10.   public NameEntry(String name,int priority,Date update){
11.       this.name = name;
12.       this.priority = priority;
13.       this.update = update;
14.   }

15.   @Ignore;
16.   public NameEntry(int id, String name,int priority,Date update){
17.       this.id = id;
18.       this.name = name;
19.       this.priority = priority;
20.       this.update = update;
21.   }


22.  //Getters e Setters
23.  public int getId() {return id;}
24.  public void setId(int id){ this.id = id; }
25.  public String getName() {return name; }
26.  public void setName(String name) {this.name = name; }
23.  public int getPriority() {return priority;}
24.  public void setPriority(int priority){ this.priority = priority; }
25.  public int getUpdate() {return update;}
26.  public void setUpdate(int update){ this.updateupdate; }

27. }

Criando uma DAO
DAO (Data Access Object) é o objeto responsável por permitir o acesso aos dados no banco de dados. Cada Entity criada vai possuir um DAO. 

Para criar um DAO usando Room, você deve implementar uma interface com a anotação @Dao (linha 1-2).
Um método que retorna uma lista da Entity na qual o DAO implementa o acesso deve ser criada para consulta, juntamente com sua anotação @Query, contendo a consulta SQL para obter todas as Entities (linha 3-4).
Consulta com parâmetros podem ser feitas também como mostra a linha 11 e 12.
Também precisamos inserir as operações INSERT (linha 5-6), UPDATE (linha 07-08) e DELETE (linha 09-10), juntamente com suas respectivas anotações @Insert, @Update e @Delete.
Uma observação no @Update do código abaixo (linha 07) é que usamos o parâmetro onConflict para especificar uma estratégia de conflito, que no caso seria um REPLACE. Mas o que seria isso? Conflitos são gerados quando definimos restrições, como a não repetição de valores em uma coluna (UNIQUE), chaves primárias ou chaves estrangeiras. No caso do exemplo, usamos o REPLACE como parâmetro, o que significa que se houver um conflito, os registros conflitantes serão deletados e, neste caso como se trata de um Update, o registro a ser alterado será atualizado.



01. @Dao
02. public interface NameDao{

03.   @Query("SELECT * FROM name ORDER BY priority")
04.   List<NameEntry> loadAllNames();

05.   @Insert
06.   void insertName(NameEntry nameEntry);

07.   @Update(onConflict=OnConflictStrategy.REPLACE)
08.   void updateName(NameEntry nameEntry);

09.   @Delete
10.   void deleteName(NameEntry nameEntry);

11.   @Query("SELECT * FROM name WHERE id=:id")
12.   NameEntry loadNameById(int id);
13. }

Criando um Type Converters
SQLite possui limitações quantos aos tipos. Ele não possui nativamente uma estrutura de dados para datas, valores booleanos e qualquer outra classe que não seja Null, Integer, Real, Text ou Blob. Se mesmo assim, nós queremos utilizar uma classe específica para este dado, então precisamos criar um Type Converter. Vamos olhar por exemplo, que a nossa classe NameEntry possui um campo de data. Então como fazemos?

Criaremos dois métodos, neste caso, um vai receber um valor do tipo long (do banco de dados) para um Objeto Date (linhas 02-05). Enquanto o outro vai fazer o contrário, vai receber um objeto Date e converter para um long (linhas 06-09). Note que ambos métodos possuem uma anotação @TypeConverter (linhas 02 e 06).



01. public class DateConverter{

02.     @TypeConverter
03.     public static Date toDate(long timestamp){
04.         return timestamp==null?null:new Date(timestamp);
05.     }

06.     @TypeConverter
07.     public static long toTimeStamp(Date date){
08.        return date==null?null:date.getTime();
09.     }
10. }


Criando o banco de dados
Para criar um banco de dados, vamos criar uma classe abstrata que herdará a classe RoomDatabase (linha 03). Também usaremos a anotação @Database, que levará como parâmetros, uma lista de todas as classes com a notação @Entity, a versão do banco de dados e se é para exportar um schema do BD (linha 01). Se tiver Type Converters, então também vamos declará-los aqui (linha 02).

Aqui usaremos um Singleton para criar/instanciar o banco de dados: Se já não houver uma instância, criaremos usando um databaseBuilder, que utiliza de um contexto para ser criado. (linhas 07-16)

Por fim, criaremos um método abstrato para cada DAOs que tivermos criado (linha 17).  

01. @Database(entities={NameEntry.class}, version=1, exportSchema=false) 
02. @TypeConverters(DateConverter.class);
03. public abstract class AppDatabase extends RoomDatabase{

04.   private static final Object LOCK = new Object();
05.   private static final String DATABASE_NAME = "NameList";
06.   private static AppDatabase instance;

07.   public static AppDatabase getInstance(Context context){
08.       if(instance == null){
09.         synchronized(LOCK){
10.            instance = Room.databaseBuilder(context.getApplicationContext(),
11.                       AppDatabase.class,
12.                       AppDatabase.DATABASE_NAME).build();
13.         }
14.       }
15.       return instance;
16.   }
  
17.   public abstract NameDao nameDao();
18. }

Como usar tudo isso?
Vamos pegar um exemplo de método para inserir um novo dado:

01.  public void insert(String name){
02.      NameEntry newName = new NameEntry(name, 1, new Date());
03.      AppDatabase.getInstance(this).taskDao().insertName(newName);
04.  } 

Basicamente, pegamos uma instancia do banco de dados, pegamos o DAO da nossa tabela e invocamos o método responsável por inserir. Simples assim.

Notas Importantes
- Nunca execute queries em threads principais de uma atividade. Apesar de poder desativar a proteção, por padrão, o mesmo não permitirá que utilize em threads principais. 



sexta-feira, 22 de junho de 2018

Rumo ao Certificado Android: Content Provider

Já vimos bastante da Activity, então hoje vamos ver mais um dos quatros classes bases do Android: os Content Providers. O objetivo de um Content Provider é criar uma interface para acessar dados, geralmente de um banco de dados, principalmente quando isso significa permitir que esses dados seja acessados por diversos aplicativos, inclusive dados como Agenda, alarme e outros nativos do Android. Para quem é da programação WEB, pode pensar que o Content Provider é um aplicativo REST que serve como uma interface aos dados.

Permissão
Para utilizar um Content Provider, é necessário registrar no manifesto a permissão para acessar o determinado Content Provider. Cada provider que app possuir deve ser registrado manualmente com a devida permissão de leitura/gravação.

Quando você quer dar permissão para somente leitura:
<uses-permission android:name="com.yourdomain.classedoprovider.TERMS_READ" />

Content Resolver
Content Resolver é a classe que fará a interface com o Content Provider. Qualquer classe que herdar o Context (ou ter acesso ao um objeto Context), pode usar o método getContentResolver para ter uma interface com os Provider

ContentResolver resolver = getContentResolver();

Ações
Com o Resolver, nós podemos agora realizar Queries e ações no banco de dados (limitados pela permissão). Existem quatro ações que podemos realizar:


  • Ler dados com o método query;
  • Adicionar dados com o método insert;
  • Atualizar dados com o método update;
  • Deletar dados com o método delete;


URI para Content Provider
A primeira ação é a da consulta, usando o resolver, temos o seguinte comando:

Cursor cursor = resolver.query(AppContract.CONTENT_URI,null,null,null,null);

Sendo que AppContract.CONTENT_URI é uma URI que apontará o Content Provider. Geralmente, o URI para Content Provider tem a seguinte sintaxe:

content://com.yourdomain.classedoprovider/termos

sendo:
  • content: Prefixo indicando que é um Content Provider
  • com.yourdomain.classedoprovider: é o que o Android chama de Autoridade de Conteúdo (Content Authority). É literalmente, a classe que controla os dados;
  • termos: Aqui é onde especificamos os dados que queremos acessar. Pode ter vários níveis, como tabela/id, e outras formas. Basicamente, o que vier aqui já é domínio do desenvolvedor, não mais do sistema.
Contrato
Uma das questões levantadas geralmente é o seguinte: como que eu vou saber de cabeça essa URI, ou suas composições. Se você observar o código da seção anterior, verá um AppContract que tem como uma constante a URI. 
Para padronizar essas informações, o Android recomenda criar uma classe Contract, que vai justamente manter essas informações de forma organizada e facilitar o desenvolvimento. A criação do Contrato é opcional em geral, mas para o Certificado, temos que seguir os padrões, e para a minha opnião, essa é um padrão que funciona tão bem que utilizo até fora do desenvolvimento Android.

Para acessar dados dos aplicativos próprio do Android, como Calendário, você pode encontrar na documentação os seus respectivos contratos (CalendarContract no caso)

Acessar o Content Provider
Content Provider, assim como qualquer recurso externo, precisa de tempo para ser carregado/executado. Então, assim, como vimos no caso de requisição Web, requisições ao Content provider devem utilizar um Loader ou pelo menos um AsyncTask (Exceto as que retornam um cursor, para esse, existe o CursorLoader).

Query
Agora vamos estuda o método Query mais detalhadamente:

Cursor cursor = resolver.query(uri,projection,selectionArg,sort);

Agora vamos estuda o método Query mais detalhadamente, observando seus parâmetros:
  • uri: é o URI para o Content Provider;
  • projection: contém filtro para selecionar quais colunas de uma base de dados você deseja obter. Neste parâmetro, quando usamos null, seria como ter dado um "SELECT *" no SQL;
  • selectionArg: é o filtro para selecionar os dados. No SQL, seria o equivalente ao WHERE;
  • sort: inidica se deve vir ordenado. É equivalente ao ORDER BY do SQL;
Cursor
É através do Cursor que podemos obter os dados do Content Provider. Um exemplo de código para conseguir todos os dados que retornaram pelo cursor:

while(cursor.moveToNext()){
   String str = cursor.getString("nomeCol");
   int inte = cursor.getInt(2);
}

Pontos importantes:
  • O cursor sempre inicia na posição anterior ao primeiro dado, por isso podemos usar o moveToNext já de cara sem perder dados.
  • Use o método get<tipo> de acordo com o tipo de dado;
  • Você pode usar o get<tipo> com o nome da coluna ou com o índice;
Cursor Loader
CursorLoader é uma classe derivada do AsyncTaskLoader, mas específico para as consultas com Cursor. Para criar um Cursor Loader, tenha em mente que o retorno dele é sempre um Cursor.
Em onCreateLoader, você deve retornar um novo  CursorLoader

return new CursorLoader(context,
                       forecastQueryUri,
                       MAIN_FORECAST_PROJECTION,
                       selection,
                       null,
                       sortOrder);

Pode reparar que a única diferença para o Query, é o contexto que deve ser passado por primeiro.

quinta-feira, 21 de junho de 2018

Rumo ao Certificado Android: Dados Persistentes - Shared Preferences

Hoje vou começar uma sub-série sobre persistência de dados no Android. Persistência de dados é a capacidade do aplicativo manter os dados mesmo depois do aplicativo ser finalizado. O Android possui 5 formas de lidar com persistência de dados, cada uma atendendo a uma necessidade específica.

A primeira já vimos, que é o Instance State, usado para persistir o estado de uma Activity quando o mesmo está preste a ser destruído pelo sistema sem que o usuário deseje isso. Como o usuário pode querer retomar a Activity, o ideal é que a mesma mantenha da mesma forma que o usuário deixou. Se o usuário fechar o aplicativo, então os dados do Instance State são apagados.

O segundo é o SharedPreferences, onde utiliza um arquivo e num sistema de chave/valor, permite armazenar strings. Os dados armazenados pela SharedPreferences são guardados até a desinstalação do aplicativo.

Para dados mais complexos e estruturados, o Android possui suporte nativo ao banco de dados SQLite, podendo criar tabelas e relacionamentos.

Para arquivos em geral, tanto gerado pelo aplicativo quanto para os download, podemos utilizar o Internal/External Storage. Interno salvará o arquivo na memória interna, enquanto que o Externo salvará em um cartão de memória. Aliás, esse foi o meu primeiro registro sobre programação Android, então vou ter que dar uma revisada neste ponto.

Por último, podemos salvar os dados na nuvem, Google Drive, Firebase, etc...


Como utilizar o Shared Preferences
Como dito antes, Shared Preferences possui uma estrutura de Chave/Valor, por isso, ele é ótimo para armazenar informações com uma semântica simples, por exemplo:

Nome = "João"
CorCamisa = "Verde"
CorBermuda = "Azul"
CorFundo = "Vermelho"

Acredito que deu para entender.

1. PreferenceFragment
Então, a primeira coisa que precisamos fazer é criar um PreferenceFragment, que é uma interface para nós modificar um Shared Preference. Não é exatamente necessário criar um, mas como é comum utilizar uma tela de configurações, então o PreferenceFragment já auxilia nesta tarefa.

Então, para nós criar, primeiro temos que criar uma depêndencia no arquivo do Gradle:

dependencies{
    compile 'com.android.support:preference-v7:25.0.0'
}

Em seguida, criaremos uma classe que herda da classe PreferenceFragment ou PreferenceFragmentCompat. Também implemente as classes que o Android Studio irá pedir para implementar. A classe ficará com esse template:

public class NomeDaClasse extends PreferenceFragmentCompat{
  
  @Override
  public void onCreatePreferences(Bundle savedInstanceState, String rootKey){
     
  }
}

2. Criando o Arquivo de Preferências
Com o botão direito do mouse, vá em "New > Directory" e crie um diretório chamado xml (isso se ela já não existir). Então, clique na pasta com botão direito, vá em "New > XML Resource File". Dê um nome. O arquivo XML de preferência sempre terá como nó raiz a tag <PreferenceScreen>.
Dentro da mesma, você vai determinar os elementos e como será a interface, como:

  • CheckBoxPreference: Configura a opção como Checkbox;
  • ListPreference: configura a opção com uma lista para selecionar
  • EditTextPreference: configura em um campo de texto

Exemplo de como pode ser o XML:
<?xml version="1.0" encoding="UTF-8" ?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
  <CheckBoxPreference
    android:defaultValue="true"
    android:key="show_bass"
    android:summaryOff="Hidden"
    android:summaryOn="Shown"
    android:title="Show Bass" />
  <ListPreference
    android:defaultValue="Valor default"
    android:entries="@array/opcoes"
    android:key="list_array"
    android:title="Array List" />
</PreferenceScreen>

3. Implementar onCreatePreferences
Aqui vamos implementar o método onCreatePreferences com base no arquivo XML

@Override
  public void onCreatePreferences(Bundle savedInstanceState, String rootKey){
     addPreferencesFromResource(R.xml.nome_xml);
  }

4. Adicionar o Fragmento na Activity
Aqui vamos usar inserir o fragmento na Activity (não entrarei agora na questão fragmento, mas veremos futuramente). No arquivo de layout da Activity, colocaremos como base, a tag fragment. Ficará assim:

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/activity_settings"
   android:name="android.example.com.visualizerpreferences.NomeDaClasse"
 android:layout_width="match_parent"
 android:layout_height="match_parent" />


Lendo um valor de Preferences
Para ler uma Preferences, você pode requisitar para PreferencesManager um acesso para as Preferences. Para isso, em algum lugar que a Activity esteja em execução, podemos invocar o um método para obeter a preferencias e usá-los para obter um valor, de forma muito semelhante a um Bundle, exceto que ele pede um segundo parâmetro que seria um valor padrão:

public void NomeActivity extends Activity{
   [...]

   public void pref(){
      SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
      int chave1 = sharedPref.getInt("chave1", 0);        
   }
}

Escrevendo um valor nas Preferences (Sem usar PreferenceFragment)
De forma bastante similar ao código anterior, mas usaremos um Editor para poder inserir um valor:


   public void prefEdit(){
      SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
      SharedPreferences.Editor editor =  sharedPref.edit();        
      editor.putInt("chave1",1);
      editor.apply();
   }


Atualizando a Activity sobre mudanças na Shared Preferences: onSharedPreferenceChangeListener
Um dos casos do SharedPreferences é que muitas vezes as opções que você alterou deve fazer efeito imediatamente, entretanto, o SharedPreferences é invisível para a Activity. Para isso precisamos implementar um Listener para ser notificado de alguma mudança.

1. Na Activity, implementar a interface onSharedPreferenceChangeListener

public void NomeActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener{
   
  @Override
  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key){
    //Implementar aqui o que vai mudar, geralmente baseado
    //na key
  }  

}

2. Registrar o Listener

public void NomeActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener{
    public void registerListener(){
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        sharedPref.registerOnSharedPreferenceChangeListener(this);

    }   

}

3. Remover o Listener quando a Activity for destruído:

public void NomeActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener{
    
   @Override
   protected void onDestroy(){
     super.onDestroy();
     PreferenceManager.getDefaultSharedPreferences(this)
         .unregisterOnSharedPreferenceChangeListener(this);
   }
}


Resumo Rápido de outras funcionalidades:


  • Em Preferences de campo de texto, você pode usar o listener PreferenceChangeListener para validar o texto e impedir que um dado inválido seja gravado;

quarta-feira, 20 de junho de 2018

Rumo ao Certificado Android: Loaders

No post anterior, vimos que o Sistema Android pode destruir e reconstruir atividades conforme necessário, como na rotação do aplicativo. Entretanto, isso pode ser complicado quando usamos um AsyncTask. Se a activity estiver controlando uma AsyncTask diretamente, um processo de destruição e reconstrução fará com que a AsyncTask responda a Activity destruída, mas não a nova instancia reconstruída, logo, não terá efeito. Ou pior ainda! Irá criar uma nova thread fazendo a mesma coisa, só que para a Activity nova.

Para resolver esse problema, podemos utilizar o Loader e o LoaderManager para gerenciá-los.

Vamos entender como usá-los:

1. Crie uma classe para implementar LoadManager.LoaderCallbacks<T>, onde o T é o resultado do AsyncTask;

public class Classe implements LoaderManager.LoaderCallbacks<String>{
}

O Android Studio vai pedir para importar a classe e criar os métodos da implementação, só fazer o que foi pedido.

2. Criar uma constante para servir como identificador. É recomendado que cada recurso que utilize Loader tenha um ID único.

public class Classe implements LoaderManager.LoaderCallbacks<String>{
private static final int RESOURCE_1 = 10;

}

3. Implementar o método onCreateLoader. Este método possui dois parâmetros, o ID que nós usamos e um Bundle com os parâmetros para carregar. Aqui vamos criar um novo AsyncTaskLoader e implementar seus métodos. O código ficará assim:

public class Classe implements LoaderManager.LoaderCallbacks<String>{
private static final int RESOURCE_1 = 10;

@Override
public Loader<String> onCreateLoader(int id, final Bundle bundle){
return new AsyncTaskLoader<String>(this){

@Override
protected void onStartLoading(){
super.onStartLoading();
if(bundle==null){
return;
}
//Aqui você pode fazer um feedback de loading, por
//exemplo, exibir uma mensagem indicando o inicio
}

@Override
public String loadInBackground(){
//Aqui é a rotina de carregamento, semelhante
//ao AsyncTask visto anteriormente
}
}
}


}

4. Implementar o método onLoadFinished. Esse método vai ser chamado quando a tarefa for concluída. O código fica o seguinte:

public class Classe implements LoaderManager.LoaderCallbacks<String>{
  [...]

@Override
public void onLoadFinished(Loader<String> loader, String data){
//Aqui faz o tratamento do retorno dos dados, seja correto
//erros
}


}

5. Chamar o LoaderManager. Cada Activity tem seu próprio LoadManager e podemos conseguir invocando o método getLoaderManager. Note que dependendo se você estiver usando fragmentos, ou appCompat, pode haver variações no nome, como getSupportLoaderManager. Uma discussão sobre isso pode ser visto aqui. Então, nós tentamos inicializá-lo ou recuperá-lo, dependendo se o mesmo já foi executado antes.

public class Classe implements LoaderManager.LoaderCallbacks<String>{
   private static final int RESOURCE_1 = 10;

   public void startLoad(Activity act, Bundle bundle){
      LoaderManager loaderManager = act.getLoaderManager();
      Loader<String> loader = loaderManager.getLoader(RESOURCE_1);
if(loader == null){
loaderManager.initLoader(RESOURCE_1, bundle, act);
}else{
loaderManager.restartLoader(RESOURCE_1, bundle, act);
}
   }
}

Passo Bônus: Guardar o resultado em cache. Para isso, podemos criar uma propriedade que vai armazenar o retorno do Loader. É uma boa prática se você sabe que o resultado não vai ser diferente para uma mesma consulta, ou algo semelhante. Então, no loadInBackground você pode verificar se o valor que está no cache já é o que deseja e retornar ele, ao invés de recarregar de novamente.

Experiência Própria: Quando usar Loaders ou AsyncTask?
A resposta é bem simples, se é uma Activity, use sempre Loader, caso contrário, use AsyncTask. Se você está seguindo a série "Rumo ao Certificado Android" na ordem de publicação, até agora só trabalhamos com Activity, mas existem outras situações, que no meu caso foi a implementação de um Widget, que não havia como obter um loader, e assim, precisei usar um AsyncTask.

terça-feira, 19 de junho de 2018

Rumo ao Certificado Android: Ciclo de Vida de Uma Activity + InstanceState

Engraçado, acredito que é a primeira vez neste blog que eu retomo um assunto para complementar: neste post de 2011, quando eu ainda estava aprendendo a programar no Android (e os recursos da API eram bem mais limitadas), eu comentei sobre o ciclo de vida de uma Activity. Entretanto, era mais um básico para colocar o app funcionando do que entender o que realmente é o ciclo de vida da Activity.

Bem, o conceito original não mudou, mas agora temos bem mais detalhado:


No caso, para obter o certificado, é importante ter o ciclo de vida em mente. A imagem acima é um resumo dos casos mais comum, mas existem outros fatores que mexem no ciclo de vida da Activity. Por exemplo, mudando a orientação do telefone geralmente fará com que o aplicativo se adapte a nova orientação, e isso acaba fazendo com que o Activity seja destruído e reconstruído tendo como base a orientação.

O fato que se a Activity não está ativa e nem tem previsão de ficar, ela pode ser finalizada a qualquer momento, como podemos ver no ciclo de vida. Entretanto, se ainda existe a possibilidade dela retornar, existe um conceito que vai ajudar aqui: o Instance State.

Quando o sistema destrói uma activity sem que o mesmo fosse o desejo do usuário ou da programação, o sistema dá uma oportunidade de gerar um Instance State para salvar os dados necessários para recriar a atividade no mesmo ponto de quando a Activity foi posta para ser destruída. Isso é realizado principalmente pelos métodos: onSaveInstanceState onRestoreInstanceState.

O método onSaveInstanceState tem como parâmetro um objeto Bundle, que é onde vamos colocar os dados. Se não lembra, o Bundle é o mesmo objeto que vimos em Intents, só que ao invés de enviar as informações para outra Activity, nós vamos salvar nele as informações da própria Activity. O código abaixo mostra como podemos usar este método:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {

    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);

    super.onSaveInstanceState(savedInstanceState);
}

Importante lembrar de sempre chamar o super e passar o Bundle para ele finalizar o processo.

Agora a activity já foi destruída e está sendo restaurada, vamos precisar fazer o reverso, e quem faz isso é o método onRestoreInstanceState. Ele vai receber como parâmetro a Bundle salva no método anterior, e com isso poderemos restaurar a tela. O código fica assim:

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);

    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
}

Assim como o método anterior, é importante chamar o método do super para completar a restauração. Também vale a pena lembrar que o mesmo bundle pode ser obtido como parâmetro no método onCreate, podendo restaurar o mesmo lá, entretanto, para fins de restauração o ideal é utilizarmos o onRestoreInstanceState, que será chamado logo após a execução do onStart.

Para finalizar, é importante lembrar que o Instance State só estará disponível se a activity for destruída sem a intenção (na imagem, quando o fluxo vai para "App process killed"). Se o usuário desejou fechar o aplicativo ou o próprio aplicativo finalizou a activity (fluxo vai para "Activity shut down"), não será possível utilizar o Instance State.

quinta-feira, 14 de junho de 2018

Rumo ao Certificado Android: Intents

1. O que é?
Permite comunicação e execução entre Activities. Existem várias maneiras de utilizar Intent e a maneira de criá-las vai depender da situação.

2. Intent e Activity
2.1 Iniciar outra Activity do mesmo aplicativo

public MainActivity extends Activity{

  [...]

  public void startOtherActivity(){
     
     //Obter contexto. Se o método não for uma atividade, obter o 
     //contexto de outra forma
     Context context = this;    

     //A classe da Activity que quer iniciar
     Class otherActivity = OtherActivity.class;

     //Instanciar o Intent
     Intent intent = new Intent(context, otherActivity);

     //Iniciar a atividade
     startActivity(intent);
  }
}

2.2 Passando dados para a próxima Activity
Você pode inserir dados nos Intents para ser recuperado na outra Activity. Para isso, pode usar o método putExtra para inserir. O putExtra utiliza dois parâmetros: uma chave que identifica e um valor para ser armazenado. Note que o putExtra é soobrecarregado, e com por isso, vale para diversos tipos de dados.

public MainActivity extends Activity{

  [...]

  public void startOtherActivity(String valueToPass){
     
     //Obter contexto. Se o método não for uma atividade, obter o 
     //contexto de outra forma
     Context context = this;    

     //A classe da Activity que quer iniciar
     Class otherActivity = OtherActivity.class;

     //Instanciar o Intent
     Intent intent = new Intent(context, otherActivity);
     intent.putExtra("key1",valueToPass);

     //Iniciar a atividade
     startActivity(intent);
  }
}

Já na Activity que vai ser aberto, nós vamos pegar a Intent que iniciou a Activity e pegar os valores com um dos métodos getExtra. Como este não é possível fazer uma sobrecarga, então exites um getExtra para cada tipo, como o getStringExtra, getIntExtra, etc:

public OtherActivity extends Activity{

  @Override
  protected void onCreate(Bundle savedInstanceStates){
     
     Intent intent = getIntent();
     String valuePassed = intent.getStringExtra("key1");

     //Também pode usar este método aqui para verificar se o valor
     //existe, antes de tentar pegá-lo
     if(intent.hasExtra("key2")){
        int value = intent.getIntExtra("key2");
     }
    
  }
}

3. Intent Implícito
Usado para chamar atividades que não sabemos ou que não cabe ao aplicativo usar, como por exemplo, abrir um site. O usuário pode tanto usar o navegador padrão do Android como pode usar um de terceiro. Como o navegador não importa, usamos um intent implícito para o Sistema Operacional ou o usuário tratar de escolher.

3.1 Exemplo para Abrir uma Página Web
Para  abrir uma página web, criamos um objeto Uri semelhante ao que vimos no artigo de Requisição Web. Então, criamos a Intent, neste caso, usaremos um outro construtor, que receberá uma constante ACTION da classe e o URI.
Essas constantes ACTION ajudam a definir qual ação será usado. A documentação está aqui para mais detalhes.
resolveActivity verificará se o intent é capaz de ser executada. É bom verificar sempre porque caso tente executar uma intent e o aparelho não tem um aplicativo necessário, por exemplo, isso pode fazer com que o aplicativo dê pau. PS: o getPackageManager é um método que contém uma lista com todas as ações que seu equipamento pode executar.

private void openWebPage(String url){
   Uri webpage = Uri.parse(url);
   Intent intent = new Intent(Intent.ACTION_VIEW, webpage);
   if(intent.resolveActivity(getPackageManager())!=null){
      startActivity(intent);
   }
}

3.2 Exemplo para Abrir um mapa
Aqui temos um esquema diferente. O Android usa um esquema de Uri para identificar os pedidos. Um Uri que inicia com um http significa que é uma página, enquanto um que inicia com geo, significa que é informação de geolocalização.

private void openMapa(String url){
   String adress = "1600 Amphitheatre Parkway, CA";
   Uri.Builder builder = new Uri.Builder();
   builder.scheme("geo")
          .path("0,0")
          .query(adress);

   Uri adressUri = builder.build();
   
   Intent intent = new Intent(Intent.ACTION_VIEW);
   intent.setData(adressUri);
   if(intent.resolveActivity(getPackageManager())!=null){
      startActivity(intent);
   }
}

4. Share Intent
É um intent especial para compartilhar mídias. Aqui utilizamos o ShareCompat, versão compat dos Share Intent. Basicamente serve para qualquer tipo de mídia que deseja compartilhar.

private void shareText(){
   String mimeType = "text/plain";
   String title = "Learning how to share";
   String text = "Hello World";

   ShareCompat.IntentBuilder.from(this)
              .setChooserTitle(title)
              .setType(mimeType)
              .setText(text);
}