domingo, 31 de janeiro de 2016

Acessando o SQLite de forma assíncrona

Olá povo,

Não importa o quanto eu estude, sempre tem algo novo sobre Android que eu não sabia. Conversando com os meus amigos Diego Nascimento e André [Mion], descobri a classe AsyncQueryHandler que nos permite realizar as transações de banco de dados em uma Thread separada.
Acho que a maioria de vocês deve saber que quando o usuário está interagindo com o aplicativo, ele está utilizando o que chamamos de Main Thread ou UI Thread. Para que não tenhamos problemas de desempenho na renderização da UI, ela deve executar a 60 fps (frames por segundo). Se fizermos um cálculo rápido, podemos constatar que nenhuma operação que é feita na UI Thread deve demorar que 16 milissegundos. Caso contrário, perderemos frames a aplicação e o usuário começará a notar a aplicação "travando" ou demorando a responder.
Aqui no blog já falei sobre várias técnicas para não utilizar a Main Thread, tais como: AsyncTask, AsyncTaskLoader e LoaderManager. Essa última, em particular resolver parte do problema do título desse post, pois com o CursorLoader + LoaderManager podemos fazer a consulta em Content Provider em background. Perfeito! Mas... E as operações de inclusão, exclusão e atualização?
Para esses casos temos a classe AsyncQueryHandler. Essa classe facilita a realização dessas operações em background. Embora, o SQLite seja muito rápido, caso você esteja fazendo uma transação que utilize várias tabelas, ou mesmo que trabalhe com muitos registros, esse processamento poderá demorar bem mais que 16ms.
A utilização dessa classe é bem simples.
public void inserir() {
    ContentValues values = new ContentValues();
    values.put("titulo", "Lembrete");
    values.put("descricao", "Escrever post");

    Uri uri = Uri.parse("content://seu.content.provider/mensagens");

    // Começa o processo assíncrono
    new MeuAsync(getActivity()).startInsert(0, null, uri, values);
}

class MeuAsync extends  AsyncQueryHandler {
    // Usando WeakReference para evitar leak da Activity
    private WeakReference<Context> mContext;

    public MeuAsync(Context ctx) {
        super(ctx.getContentResolver());
        mContext = new WeakReference<>(ctx);
    }

    @Override
    protected void onInsertComplete(int token, Object cookie, Uri uri) {
        super.onInsertComplete(token, cookie, uri);
        if (mContext.get() != null) {
            Toast.makeText(mContext.get(),
                    "Registro inserido com sucesso!",
                    Toast.LENGTH_SHORT).show();
        }
    }
}
Perceba que estamos utilizando o método startInsert() passando como parâmetro: um identificador para essa transação; um objeto qualquer que podemos utilizar posteriormente no método de callback (aqui passamos null); a URI para o seu Content Provider; e os valores que serão inseridos.
De forma semelhando, poderíamos utilizar os métodos: startUpdate(), startDelete() e startQuery().

Na classe MeuAsync, ficam os métodos que serão chamados após a realização da operação em background. No nosso exemplo utilizamos apenas o o onInsertComplete. Mas nos casos de atualização, exclusão e busca de registros, utilizaríamos os métodos onUpdateComplete(), onDeleteComplete e onQueryComplete().

4br4ç05,
nglauber

sábado, 9 de janeiro de 2016

Percentual Layout

Olá povo,

Quase sempre, quando vou ministrar aula de Android, sempre tem alguém que pergunta: "Professor tem como definir o tamanho de um componente usando percentual?". Normalmente quem faz essa pergunta é algum desenvolvedor web, onde utilizar valores percentuais para elementos de UI é algo bem comum no front-end de uma aplicação.
Mas no caso do Android, a resposta era sempre não, mas podíamos resolver esse problema utilizando o LinearLayout e definir nas views filhas a propriedade android:layout_weight como mostrei nesse post aqui.
Mas finalmente o Google criou para nós, gerenciadores de layout que suportam medidas percentuais. São eles: PercentFrameLayout e PercentRelativeLayout.

Para utiliza-los, basta adicionar a seguinte dependência no build.gradle.
dependencies {
    ...
    compile 'com.android.support:percent:23.1.1'
}

Depois é só utilizar no arquivo de layout.
<android.support.percent.PercentRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/centered_image"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#FF0000"
        app:layout_heightPercent="@fraction/fifty_percent"
        app:layout_widthPercent="@fraction/fifty_percent"/>

    <TextView
        android:id="@+id/caption"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/centered_image"
        android:background="#00FF00"
        android:text="Texto"
        app:layout_marginRightPercent="@fraction/twentyfive_percent"
        app:layout_marginLeftPercent="@fraction/twentyfive_percent"
        app:layout_widthPercent="50%"/>
</android.support.percent.PercentRelativeLayout>

Perceba que para evitar problemas de compilação adicionamos a largura e altura do componente como 0dp, mas as propriedades que realmente definirão o tamanho do componente são app:layout_heightPercent e app:layout_widthPercent.
Estamos utilizando uma referência para @fraction ao invés do valor hard-coded. Desta forma defina esses valores, por exemplo, no res/values/dimens.xml.
<resources>
    ...
    <fraction name="twentyfive_percent">25%</fraction>
    <fraction name="fifty_percent">50%</fraction>
</resources>
Outro detalhe importante é que podemos definir margens utilizando valores percentuais, recurso que não conseguíamos no LinearLayout.
O resultado ficará como a seguir:


Se tiver dúvidas, consulte a documentação oficial:
Ou deixe seus comentários aqui :)

4br4ç05,
nglauber

terça-feira, 5 de janeiro de 2016

RecyclerView no Android

Olá povo,

Devia ter escrito esse post a muito tempo, mas finalmente saiu :) Desde a primeira versão do Android em 2008 até o começo de 2014, para criarmos telas de listagem utilizávamos a classe ListView juntamente com alguma subclasse de BaseAdapter (ArrayAdapter ou CursorAdapter por exemplo) e esse conjunto funcionava muito bem. Mas conforme o tempo foi passando, ele foi apresentando algumas "limitações" que atrapalhavam sua conformidade aos requisitos/padrões de UI/UX atuais. Nada que fosse impossível de ser implementado, mas que eram mais trabalhosos de serem feitos, tais como:
  • Performance na atualização de itens: a ListView está ligada ao Adapter, que possui uma lista de objetos. Se inserirmos um objeto nessa lista, temos que invocar o método notifyDatasetChanged() que fará com que toda a lista seja refeita/redesenhada. Com a RecyclerView podemos atualizar só um item da lista (inserindo/atualizando/excluindo) ou um intervalo específico.
  • Layouts diferenciados para cada situação: com a RecyclerView podemos configurar gerenciadores de layouts, indicando por exemplo, que a lista terá uma única coluna quando o aparelho estiver em portrait e duas quando estiver em landscape. Ou ainda dizer que o primeiro item da lista será diferente dos demais.
  • Animações e gestos: Com a RecyclerView, à medida que os itens vão sendo adicionados ou removidos, uma animação é realizada dando um feedback visual para o usuário do que aconteceu. A utilização de gestos também ficou bastante simples. Ações como o swipe sobre um item da lista é algo trivial de ser feito.
  • Scroll em ambos os sentidos: a RecyclerView permite o scroll em na horizontal e na vertical, o que não era possível nativamente na ListView.
  • Baixa curva de aprendizagem: o conceito utilizado pela RecyclerView é muito parecido com o que temos na ListView. Então quem já a conhece não terá muitos problemas.
Estes são os mais importantes para mim. Mas já pode te convencer a mudar para a RecyclerView. Se é que não já mudou? Você já mudou, certo? ;)

Pondo em prática

Veremos um exemplo simples de uma listagem de mensagens, mas que obviamente pode ser ajustado para qualquer outro propósito. Adicione as seguintes dependências no build.gradle do projeto.
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
    compile 'com.android.support:cardview-v7:23.1.1'
    compile 'com.jakewharton:butterknife:7.0.1'
}
A RecyclerView está biblioteca appcompat. Já a biblioteca de design foi adicionada aqui para podermos utilizar o FloatActionButton. Cada item da lista será um CardView, então adicionamos a dependência. E por fim, utilizamos a ButterKnife que falei nesse post aqui.

Vamos agora para a implementação começando pela classe básica.
public class Mensagem {
    public String titulo;
    public String texto;

    public Mensagem(String titulo, String texto) {
        this.titulo = titulo;
        this.texto = texto;
    }
}
Nada a comentar sobre essa classe, então vamos adicionar o arquivo de layout item_mensagem.xml que representará cada item da lista.
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="4dp"
    android:foreground="?android:attr/selectableItemBackground"
    app:cardCornerRadius="5dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="8dp">
        <TextView
            android:id="@+id/txtTitulo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textAppearance="?android:attr/textAppearanceLarge"/>
        <TextView
            android:id="@+id/txtMensagem"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"/>
    </LinearLayout>
</android.support.v7.widget.CardView>
Um ponto interessante a ressaltar nesse layout é a propriedade foreground. A RecyclerView, por padrão, não fornece o feedback de toque como faz a ListView, pois ela parte do pressuposto que seus itens não são clicáveis, ao contrário da ListView que mesmo que não haja tratamento do evento de clique, um feedback visual é dado ao usuário.

O arquivo de layout da activity (activity_main.xml) é exibido a seguir:
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="8dp">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/titulo"
            android:id="@+id/edtTitulo"/>
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/texto"
            android:id="@+id/edtTexto"/>
        <android.support.v7.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/recyclerView"/>
    </LinearLayout>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_marginRight="16dp"
        android:layout_marginBottom="16dp"
        android:id="@+id/fab"
        android:src="@drawable/ic_add_white_24dp"/>
</FrameLayout>
O único detalhe a comentar sobre esse layout é a imagem do ícone do botão. Ela pode ser encontrada no site de ícones do Material Design, que fornece mais de 800 ícones gratuitos que você pode utilizar no seu aplicativo.

Vamos agora implementar o adapter a ser utilizado por nossa RecyclerView.
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;

public class MensagemAdapter extends 
    RecyclerView.Adapter<MensagemAdapter.VH> {

    List<Mensagem> mMensagens;
    AoClicarNaMensagem mListener;

    public MensagemAdapter(List<Mensagem> mensagens, 
                           AoClicarNaMensagem listener) {
        mMensagens = mensagens;
        mListener = listener;
    }

    @Override
    public VH onCreateViewHolder(ViewGroup parent, 
                                 int viewType) {
        View v= LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_mensagem, 
                         parent, false);

        final VH vh = new VH(v);
        vh.itemView.setOnClickListener(
          new View.OnClickListener() {
            @Override
            public void onClick(View v) {
              if (mListener != null) {
                int pos = vh.getAdapterPosition();
                Mensagem mensagem = mMensagens.get(pos);
                mListener.mensagemClicada(mensagem);
              }
            }
          });
        return vh;
    }

    @Override
    public void onBindViewHolder(VH holder, int pos) {
        Mensagem msg = mMensagens.get(pos);
        holder.textTitulo.setText(msg.titulo);
        holder.textTexto.setText(msg.texto);
    }

    @Override
    public int getItemCount() {
        return mMensagens != null ? 
            mMensagens.size() : 0;
    }

    public static class VH extends 
        RecyclerView.ViewHolder {

        @Bind(R.id.txtTitulo)   
        public TextView textTitulo;
        @Bind(R.id.txtMensagem) 
        public TextView textTexto;

        public MensagemViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }
    }

    public interface AoClicarNaMensagem {
        void mensagemClicada(Mensagem mensagem);
    }
}
Esssa sem sombra de dúvida é a classe mais importante desse exemplo. Então vamos aos detalhes sobre ela:
  • Para definir um adapter para a RecyclerView devemos criar uma subclasse de RecyclerView.Adapter, e como podemos observar, ela é "tipada". Ou seja, ela necessita de um tipo que deve ser uma subclasse de RecycleView.ViewHolder. No nosso exemplo, essa classe é chamada simplesmente de VH e está declarada dentro da própria classe MensagemAdapter.
  • Para criar uma instância do nosso adapter, além da lista de objetos, podemos passar um objeto que tratará o evento de clique do item da lista. Diferentemente do que é feito na ListView, onde temos o OnItemClickListener, o evento de clique em um item da lista é definido no próprio item, ou seja, no adapter. 
    • No nosso caso, quem estiver interessado em ouvir o evento de clique no item da lista, deverá passar como parâmetro para o adapter, um objeto que implemente a interface AoClicarNaMensagem. Na prática, a activity passará esse objeto para o adapter.
  • Obrigatoriamente devemos implementar 3 métodos: 
    • O método onCreateViewHolder criará a instância do ViewHolder baseado no arquivo de layout que representa cada item (no nosso caso item_mensagem.xml). Esse método já implementa, por padrão, a abordagem de um adapter eficiente que expliquei nesse post, e esse é o momento ideal para definirmos o evento de clique do item da lista. Percebam que por meio do atributo itemView (que já é da classe ViewHolder) definimos o evento de clique. E usando o método getAdapterPosition() podemos obter o índice da posição da lista que foi clicada.
    • No onBindViewHolder() é onde preenchemos cada View do ViewHolder. Essa abordagem de create/bind já era usada no CursorAdapter como falei nesse post.
    • O getItemCount() é quantidade de itens que sua lista exibirá, como já é de costume em qualquer adapter.
  • Percebam que estamos utilizando o ButterKnife no ViewHolder, mas fique a vontade em usar o bom e velho findViewById() :)
Vamos utilizar esse adapter na Activity.
public class MainActivity extends AppCompatActivity {
    @Bind(R.id.edtTitulo)
    EditText mEdtTitulo;
    @Bind(R.id.edtTexto)
    EditText mEdtMensagem;
    @Bind(R.id.recyclerView)
    RecyclerView mRecyclerView;

    List<Mensagem> mMensagens;
    MensagemAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        mMensagens = new ArrayList<>();
        mAdapter = new MensagemAdapter(
                mMensagens, mListener);
        mRecyclerView.setAdapter(mAdapter);

        GridLayoutManager layoutManager =
                new GridLayoutManager(this, 2);

        layoutManager.setSpanSizeLookup(
                new GridLayoutManager.SpanSizeLookup() {
                    @Override
                    public int getSpanSize(int pos) {
                        return pos == 0 ? 2 : 1;
                    }
                });
        mRecyclerView.setLayoutManager(layoutManager);
    }

    @OnClick(R.id.fab)
    public void onClick(View view) {
        Mensagem mensagem = new Mensagem(
                mEdtTitulo.getText().toString(),
                mEdtMensagem.getText().toString());
        mMensagens.add(mensagem);
        mAdapter.notifyItemInserted(
                mMensagens.size() - 1);

        mEdtTitulo.getText().clear();
        mEdtMensagem.getText().clear();
    }

    private MensagemAdapter.AoClicarNaMensagem mListener=
        new MensagemAdapter.AoClicarNaMensagem() {
            @Override
            public void mensagemClicada(Mensagem msg) {
                String s = String.format(
                    "%s %s", msg.titulo, msg.texto);
                Toast.makeText(MainActivity.this, 
                    s, Toast.LENGTH_SHORT).show();
            }
        };
}
Vamos agora as explicações relevantes:
  • Estamos utilizando o gerenciador de layout GridLayoutManager que nos permite dividir nossa lista em colunas. 
    • No nosso caso, estamos dizendo que teremos duas colunas. O método setSpanSizeLookup() indica quais itens da linha vão ocupar mais de uma coluna. Para esse exemplo, estipulei que apenas o primeiro item da lista vai ocupar duas colunas, os demais ocuparão uma (das duas colunas) da lista.
    • Existem ainda os gerenciadores de layout LinearLayoutManagerStaggeredGridLayoutManager. O primeiro organiza os itens da lista de forma linear enquanto o segundo organiza os itens em forma de grid, mas suporta itens de tamanhos completamente diferentes e os exibe como se fosse um "mosaico". 
    • E você pode criar seus próprios gerenciadores de layout.
  • Percebam que no evento de clique, ao inserirmos um registo, utilizamos o método notifyItemInserted(). E como sempre adicionamos o item ao final da lista, passamos o tamanho da lista menos um. 
    • Existem também os métodos notifyItemRemoved(), notifyItemChanged(), notifyItemMoved() para quando um item da lista foi respectivamente removido, alterado ou movido de uma posição para a outra da lista.
    • Podemos utilizar os métodos notifyItemRangeInserted(), notifyItemRangeRemoved() e notifyItemRangeChanged(). Que indicam que um intervalo de itens da lista foi inserido, removido ou alterado.
    • Obviamente existe o notifyDatasetChanged(), que possui o mesmo comportamento da ListView.
Ao executar o exemplo e adicionar alguns itens, você terá um resultado similar ao da figura abaixo.
O aplicativo deve estar adicionando os itens na lista. Mas e para remover? Que tal usar o gesto de swipe? ;) Com a RecyclerView isso ficou bem simples. Vejamos o código a seguir.

private void configuraSwipe() {
  ItemTouchHelper.SimpleCallback swipe =
      new ItemTouchHelper.SimpleCallback(0, 
          ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {

          @Override
          public boolean onMove(
              RecyclerView recyclerView,
              RecyclerView.ViewHolder viewHolder,
              RecyclerView.ViewHolder target) {
            return false;
          }

          @Override
          public void onSwiped(
              RecyclerView.ViewHolder viewHolder, 
              int swipeDir) {

            final int position = 
              viewHolder.getAdapterPosition();
            mMensagens.remove(position);
            mAdapter.notifyItemRemoved(position);
          }
      };
  ItemTouchHelper itemTouchHelper = new ItemTouchHelper(swipe);
  itemTouchHelper.attachToRecyclerView(mRecyclerView);
}
Com a classe ItemTouchHelper estamos configurando o gesto de swipe para esquerda e para direita. E no método onSwiped() removemos o item da lista e notificamos o adapter. Perceba que com essa classe também podemos fazer o evento de mover itens na lista.
Para concluir, chame esse método no onCreate() da activity.
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    configuraSwipe();
}
Execute a aplicação novamente e insira alguns itens. Em seguida, tente exclui-los utilizando o gesto de swipe. Perceba que após um item ser excluído, os demais itens são reorganizados com uma animação.

Conclusão

Se você ainda não usa a RecylerView, você está perdendo diversos recursos bacanas que estão sendo adicionados a esse componente. Entretanto ele ainda tem algumas melhorias a serem feitas e o próprio pessoal do Google admitiu isso para mim no Android Dev Summit.
Coisas simples como a empty view já poderiam estar implementadas (nada demais, eu convivo com isso sem problemas). Definir um divisor entre as linhas da RecyclerView é um absurdo. Isso já deveria estar abstraído no componente. Outra coisa que eu sentia falta era um CursorAdapter para a RecyclerView. Mas depois de ver a implementação interna dele, achei melhor fazer o meu mesmo :)
No final das contas, sem sombra de dúvidas o RecyclerView é bem mais flexível que a ListView e o seu resultado final vale muito a pena.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

P.S.: Meus agradecimentos a André Mion que me ajudou a discutir os tópicos desse post.