quinta-feira, 10 de março de 2016

Tchau CESAR :(

Olá povo,

No dia 16 de janeiro de 2006 eu começava a trabalhar no Centro de Estudos e Sistemas Avançados do Recife. Ainda estava terminando a faculdade e lembro que meu sonho era "usar o crachá laranja".
Hoje, 10 de março de 2016 é o meu último dia de trabalho no CESAR. Foram mais de 10 anos de muito desafios, aprendizados, crescimento pessoal e profissional, e muita história boa para contar.
Tive a satisfação de trabalhar com muita gente boa (e outras nem tão legais assim) e fiz muitas amizades em todos os projetos em que passei eu acho.

O nome do CESAR estará cravado na minha história para sempre, com muito orgulho, pois foi nessa empresa fantástica que construí a maior e melhor parte da minha carreira. Isso sem contar a parte acadêmica, já que eu cursei o mestrado no CESAR.edu.

Mas como é evidente a todos que me conhecem, estou fortemente envolvido com a área de desenvolvimento para dispositivos móveis, e nos últimos meses não estava tendo a oportunidade de trabalhar com mobile e nem com desenvolvimento de software (codificação em si). Isso acabou me deixando um pouco desmotivado, então resolvi conversar com meus superiores e verificar a possibilidade de me alocar em um projeto mobile dentro da instituição. Mas infelizmente, fui informado de que não havia previsão de novos projetos com mobile no curto/médio prazo. Sendo assim, chegamos a conclusão de que meu plano de carreira não estava alinhado com as expectativas do CESAR, então resolvi procurar novos desafios em um novo lugar.

Espero de todo coração estar deixando as portas abertas, pois o CESAR é um lugar incrível para trabalhar, que eu considero minha segunda casa, onde (quem sabe?) um dia terei a satisfação de voltar.
Obrigado a todos vocês que fizeram parte da minha vida durante esses 10 anos, vocês me fizeram muito feliz. :)

Chegou a hora de começar uma nova etapa(!) e escrever um novo capítulo da minha história.

4br4ç05,
nglauber

domingo, 28 de fevereiro de 2016

CursorAdapter para RecyclerView (parte 2)

Olá povo,

Na primeira parte desse post, implementamos as classes básicas, conexão com o banco de dados e o ContentProvider. Partiremos agora para a implementação da interface gráfica.

Adapter

Vamos começar a implementação da UI pelo adapter que utilizaremos na lista. Adicione a classe MensagemCursorAdapter e deixe-a como a seguir.
public class MensagemCursorAdapter extends
        RecyclerView.Adapter<MensagemCursorAdapter.VH> {

    private Cursor mCursor;
    private AoClicarNoItem mListener;

    public MensagemCursorAdapter(AoClicarNoItem listener) {
        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);
        v.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = vh.getAdapterPosition();
                mCursor.moveToPosition(position);
                if (mListener != null) mListener.itemFoiClicado(mCursor);
            }
        });

        return vh;
    }

    @Override
    public void onBindViewHolder(VH holder, int position) {
        mCursor.moveToPosition(position);
        int idx_titulo = mCursor.getColumnIndex(MensagemContract.TITULO);
        int idx_descr = mCursor.getColumnIndex(MensagemContract.DESCRICAO);

        String titulo = mCursor.getString(idx_titulo);
        String descricao = mCursor.getString(idx_descr);

        holder.mText1.setText(titulo);
        holder.mText2.setText(descricao);
    }

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

    @Override
    public long getItemId(int position) {
        if (mCursor != null) {
            if (mCursor.moveToPosition(position)) {
                int idx_id = mCursor.getColumnIndex(MensagemContract._ID);
                return mCursor.getLong(idx_id);
            } else {
                return 0;
            }
        } else {
            return 0;
        }
    }

    public Cursor getCursor(){
        return mCursor;
    }

    public void setCursor(Cursor newCursor){
        mCursor = newCursor;
        notifyDataSetChanged();
    }

    public interface AoClicarNoItem {
        void itemFoiClicado(Cursor cursor);
    }

    public static class VH extends RecyclerView.ViewHolder {
        public TextView mText1;
        public TextView mText2;

        public VH(View v) {
            super(v);
            mText1 = (TextView) v.findViewById(R.id.text1);
            mText2 = (TextView) v.findViewById(R.id.text2);
        }
    }
}
Vou ressaltar apenas os pontos mais importantes do Adapter, pois uma explicação mais introdutória sobre RecyclerView.Adapter foi feita nesse post aqui.
  • Ao invés de termos uma lista de objetos, o Adapter utilizará um cursor para retornar/preencher as views da RecyclerView. Isso pode ser percebido nos métodos onCreateViewHolder() e onBindViewHolder() e no getItemCount().
  • Esse cursor será atribuído por meio do método setCursor(). Utilizaremos esse método no Fragment que mostraremos a seguir. Ao atualizarmos o cursor, chamamos o notifyDatasetChanged() para que a RecyclerView seja atualizada.
  • ATENÇÃO! Não esqueçam de implementar o método getItemId()! Ele é utilizado internamente para atualizar a RecyclerView. Dessa forma, esse método está retornando o ID do registro correspondente.
Como podemos ver, a implementação não é nada complexa comparada ao que fazemos com uma lista de objetos.
A adapter está utilizando o arquivo de layout item_mensagem.xml descrito a seguir.

<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="wrap_content"
    android:layout_marginTop="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    android:orientation="vertical"
    android:foreground="?android:attr/selectableItemBackground"
    app:cardCornerRadius="5dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="8dp">

        <TextView
            android:id="@+id/text1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="20sp"/>

        <TextView
            android:id="@+id/text2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"/>
    </LinearLayout>
</android.support.v7.widget.CardView>
Utilizamos o CardView para dar um visual mais interessante para o nosso modesto aplicativo :)

Um DialogFragment para inserir

Vamos utilizar um Dialog para inserir, mas você pode utilizar o componente que preferir.
Deixe o arquivo de layout conforme a seguir.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:padding="16dp"
              tools:context=".MensagemFragment">

    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/edtTitulo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/titulo"/>
    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/edtDescricao"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/descricao"/>
    </android.support.design.widget.TextInputLayout>

</LinearLayout>
Estamos utilizando o TextinputLayout para dar um toque de Material Design ao nosso aplicativo :) Ele fará uma animação ao preenchermos o conteúdo do EditText. No mais, o arquivo de layout não possui nada de muito especial.
Vamos adicionar no strings.xml as strings utilizadas no layout e as que vamos utilizar no código do Fragment.
<string name="titulo">Título</string>
<string name="descricao">Descrição</string>
<string name="nova_mensagem">Nova mensagem</string>
<string name="editar_mensagem">Editar mensagem</string>
<string name="salvar">Salvar</string>
<string name="cancelar">Cancelar</string>

Vamos agora a implementação da classe MensagemFragment.
public class MensagemFragment extends DialogFragment
        implements DialogInterface.OnClickListener {

    private static final String EXTRA_ID = "id";

    EditText mEdtTitulo;
    EditText mEdtDescricao;
    long id;

    public static MensagemFragment newInstance(long id){
        Bundle bundle = new Bundle();
        bundle.putLong(EXTRA_ID, id);

        MensagemFragment mensagemFragment = new MensagemFragment();
        mensagemFragment.setArguments(bundle);
        return mensagemFragment;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        View view = getActivity().getLayoutInflater()
                .inflate(R.layout.fragment_mensagem, null);

        mEdtTitulo = (EditText)view.findViewById(R.id.edtTitulo);
        mEdtDescricao = (EditText)view.findViewById(R.id.edtDescricao);

        boolean novaMensagem = true;
        if (getArguments() != null && getArguments().getLong(EXTRA_ID) != 0){
            id = getArguments().getLong(EXTRA_ID);
            Uri uri = Uri.withAppendedPath(
                    MensagemContract.URI_MENSAGENS, String.valueOf(id));

            Cursor cursor = getActivity().getContentResolver()
                    .query( uri, null, null, null, null);
            if (cursor.moveToNext()) {
                novaMensagem = false;
                mEdtTitulo.setText(cursor.getString(
                        cursor.getColumnIndex(MensagemContract.TITULO)));
                mEdtDescricao.setText(cursor.getString(
                        cursor.getColumnIndex(MensagemContract.DESCRICAO)));
            }
            cursor.close();
        }

        return new AlertDialog.Builder(getActivity())
                .setTitle(novaMensagem ? 
                        R.string.nova_mensagem : R.string.editar_mensagem)
                .setView(view)
                .setPositiveButton(R.string.salvar, this)
                .setNegativeButton(R.string.cancelar, null)
                .create();
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        ContentValues values = new ContentValues();
        values.put(MensagemContract.TITULO, 
                mEdtTitulo.getText().toString());
        values.put(MensagemContract.DESCRICAO, 
                mEdtDescricao.getText().toString());
        if (id != 0){
            Uri uri = Uri.withAppendedPath(
                    MensagemContract.URI_MENSAGENS, String.valueOf(id));
            getContext().getContentResolver().update(uri, values, null, null);
        } else {
            getContext().getContentResolver().insert(
                    MensagemContract.URI_MENSAGENS, values);
        }
    }
}
A classe é bem simples e será utilizada tanto para inserir quanto para atualizar uma mensagem. Por isso, no método newIntance() estamos recebendo um parâmetro que representa o ID da mensagem. Se passarmos um ID válido, as informações da mensagem serão exibidas na tela. Isso é feito no método onCreateDialog(). Já no método onClick, estamos inserindo/atualizando o registro no banco banco de dados. Perceba que aqui não estou utilizando o AsyncQueryHandler que falei nesse post para simplificar o exemplo, mas ela seria a melhor opção.

Fragment de Listagem

Implementado o adapter, vamos criar o Fragment que utilizará o Adapter para exibir a listagem de mensagens vindas do banco de dados. Vamos começar por seu arquivo de layout.

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ListaMensagensFragment">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"/>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fabAdd"
        android:src="@drawable/ic_add_white_24dp"
        app:fabSize="normal"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp" />
</android.support.design.widget.CoordinatorLayout>
Estamos utilizando o FloatActionButton e o CoordinatorLayout da biblioteca de design.
Partiremos agora para a implementação da classe ListaMensagensFragment.
public class ListaMensagensFragment extends Fragment
        implements LoaderManager.LoaderCallbacks<Cursor< {

    private RecyclerView mRecyclerView;
    private MensagemCursorAdapter mAdapter;
    private LinearLayoutManager mLayoutManager;

    @Override
    public View onCreateView(LayoutInflater inflater, 
                             ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(
            R.layout.fragment_lista_objetos, container, false);

        view.findViewById(R.id.fabAdd).setOnClickListener(
            new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new MensagemFragment()
                        .show(getFragmentManager(), "dialog");
                }
            });

        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);

        configuraSwipe();

        mLayoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new MensagemCursorAdapter(
            new MensagemCursorAdapter.AoClicarNoItem() {
                @Override
                public void itemFoiClicado(Cursor cursor) {
                    long id  = cursor.getLong(
                        cursor.getColumnIndex(MensagemContract._ID));
                    MensagemFragment f = MensagemFragment.newInstance(id);
                    f.show(getFragmentManager(), "dialog");
            }
        });
        mAdapter.setHasStableIds(true);
        mRecyclerView.setAdapter(mAdapter);

        getLoaderManager().initLoader(0, null, this);

        return view;
    }

    private void configuraSwipe() {
        ItemTouchHelper.SimpleCallback simpleItemTouchCallback = 
            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 x = viewHolder.getLayoutPosition();
                Cursor cursor = mAdapter.getCursor();
                cursor.moveToPosition(x);
                final long id = cursor.getLong(
                        cursor.getColumnIndex(MensagemContract._ID));

                Uri uriToDelete = Uri.withAppendedPath(
                        MensagemContract.URI_MENSAGENS, String.valueOf(id));
                getActivity().getContentResolver().delete(
                        uriToDelete,
                        null, null);
            }
        };
        ItemTouchHelper itemTouchHelper = 
                new ItemTouchHelper(simpleItemTouchCallback);
        itemTouchHelper.attachToRecyclerView(mRecyclerView);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), 
                                MensagemContract.URI_MENSAGENS, 
                                null, null, null, 
                                MensagemContract.TITULO);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        mAdapter.setCursor(data);
    }

    @Override
    public void onLoaderReset(Loader loader) {
        mAdapter.setCursor(null);
    }
}
Vamos as explicações.
  • No método onCreateView(), definimos o evento de clique do FAB onde criamos uma nova instância do MensagemFragment sem passar um ID pois queremos criar uma nova mensagem (e que obviamente não possui um ID).
  • Um detalhe importante aqui é a chamada ao método setHasStableIds() para indicar que cada item da lista possui um identificador único, que no nosso caso é a coluna "_id" da tabela.
  • O método configuraSwipe() é usado para definir o comportamento ao fazermos swipe sobre os itens da lista. No nosso caso, estamos excluindo um registro. Perceba que estamos obtendo a posição do item da lista utilizando o método getLayoutPosition().
  • Voltando ao onCreateView(), estamos criando uma instância do nosso adapter passando como parâmetro um objeto que tratará o evento de clique no item da lista. Note que estamos criando a instância de MensagemFragment passando o ID do item que foi clicado.
  • Após atribuir o adapter à RecyclerView, invocamos o LoaderManager para iniciar o processo de busca no banco de dados de forma assíncrona utilizado o método initLoader(). Quando o Loader estiver criado, o método onCreateLoader será invocado, e nele instanciamos um CursorLoader apontando para URI do provider. Quando a busca no provider é concluída, o método onLoadFinish() é invocado, e onde finalmente atribuímos o Cursor recebido como parâmetro ao nosso adapter.
Precisamos adicionar esse fragment à nossa activity. Abra o arquivo activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ngvl.android.exrecyclerview.MainActivity">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="ngvl.android.exrecyclerview.ListaMensagensFragment"
        android:id="@+id/fragment"/>
</RelativeLayout>

Execute a aplicação e o resultado será similar ao vídeo a seguir.
O código completo da aplicação está disponível no meu github.
https://github.com/nglauber/playground/tree/master/android/ExRecyclerView

4br4ç05,
nglauber

CursorAdapter para RecyclerView (parte 1)

Olá povo,

Recentemente escrevi um post falando sobre como criar uma tela de listagem utilizando o RecyclerView. Mencionei os seus principais benefícios como animações, gestos, gerenciadores de layout, atualização parcial da lista, etc. Para exemplificar esses conceitos, foi criado um adapter para uma lista de objetos. Mas e se quisermos utilizar um Cursor vindo do seu Content Provider?

Já mostrei aqui no blog como utilizar o conjunto ContentProvider + CursorLoader + CursorAdapter. Onde foi utilizado uma ListView para exibir na tela as informações vindas do SQLite. Mas esse conjunto pode ser utilizado com a RecyclerView? Vamos analizar cada um deles...
  • O ContentProvider é uma abstração para um mecanismo de persistência (que normalmente é um SQLite). Como não tem nada a ver com UI, então esse componente funcionará com a RecyclerView. 
  • O CursorLoader nos ajuda a fazer a requisição em background e receber notificações quando alguma operação é realizada no provider. Uma vez que é independente da UI também é compatível. 
  • Já o CursorAdapter não pode ser utilizado com a RecyclerView. Pois ele herda de BaseAdapter e a RecyclerView espera uma instância de RecyclerView.Adapter. 
Sendo assim, devemos implementar nosso CursorAdapter "na mão", mas relaxe, não é nada complicado... ;) Vamos criar um exemplo completo aqui para entendermos melhor esses conceitos. Crie um novo projeto no Android Studio e vamos lá.

Adicionando dependências

Antes de começarmos a implementação vamos adicionar no build.gradle algumas dependências que utilizaremos no nosso exemplo.
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "ngvl.android.exrecyclerview"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.+'
    compile 'com.android.support:cardview-v7:23.+'
    compile 'com.android.support:recyclerview-v7:23.+'
    compile 'com.android.support:support-v4:23.+'
    compile 'com.android.support:design:23.+'
}

Classes básicas

Vamos adicionar uma interface que possui informações sobre o nosso banco de dados e do ContentProvider.
public interface MensagemContract extends BaseColumns {
    String AUTHORITY = "ngvl.android.exrecycler";
    Uri BASE_URI = Uri.parse("content://"+ AUTHORITY);
    Uri URI_MENSAGENS = Uri.withAppendedPath(BASE_URI, "msgs");

    String TABELA_MENSAGEM = "Mensagem";
    String TITULO = "titulo";
    String DESCRICAO = "descricao";
}
Nossa interface possui uma constante que representa a authority do nosso provider. A authority nada mais é do que o identificador único do nosso provider e faz parte do endereço que utilizaremos para acessá-lo. O endereço completo é representado pela constante URI_MENSAGENS. Em seguida, definimos constantes para o nome da tabela e seus dois campos.
Agora crie a classe DbHelper que será responsável por criar a estrutura do banco de dados da aplicação.
public class DbHelper extends SQLiteOpenHelper {

    public DbHelper(Context context) {
        super(context, "dbNotas", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE "+ MensagemContract.TABELA_MENSAGEM +" ("+
                MensagemContract._ID +" INTEGER NOT NULL PRIMARY KEY, " +
                MensagemContract.TITULO +" TEXT NOT NULL, " +
                MensagemContract.DESCRICAO +" TEXT NOT NULL)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}
Quando utilizarmos a classe DbHelper dentro da nossa aplicação, internamente é verificado se banco de dados existe. Caso ele não exista, o método onCreate() será invocado e nele criamos a tabela que armazenará a lista de mensagens.

Content Provider

Adicione agora o ContentProvider da aplicação que se chamará MensagemProvider.
public class MensagemProvider extends ContentProvider {

    public static final int MENSAGENS = 1;
    public static final int MENSAGENS_POR_ID = 2;

    UriMatcher mUriMatcher;
    DbHelper mDbHelper;

    @Override
    public boolean onCreate() {
        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mUriMatcher.addURI(
            MensagemContract.AUTHORITY, "msgs", MENSAGENS);
        mUriMatcher.addURI(
            MensagemContract.AUTHORITY, "msgs/#", MENSAGENS_POR_ID);

        mDbHelper = new DbHelper(getContext());
        return true;
    }

    @Override
    public String getType(Uri uri) {
        throw new UnsupportedOperationException("Não implementada.");
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (mUriMatcher.match(uri) == MENSAGENS){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            long id = db.insert(
                    MensagemContract.TABELA_MENSAGEM, null, values);
            Uri insertUri = Uri.withAppendedPath(
                    MensagemContract.BASE_URI, String.valueOf(id));
            db.close();
            notifyChanges(uri);
            return insertUri;
        } else {
            throw new UnsupportedOperationException("Não suportada");
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        if (mUriMatcher.match(uri) == MENSAGENS_POR_ID){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int linhasAfetadas = db.delete(MensagemContract.TABELA_MENSAGEM,
                    MensagemContract._ID +" = ?",
                    new String[]{ uri.getLastPathSegment() });
            db.close();
            notifyChanges(uri);
            return linhasAfetadas;

        } else {
            throw new UnsupportedOperationException(
                "Uri inválida para exclusão.");
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        if (mUriMatcher.match(uri) == MENSAGENS_POR_ID){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int linhasAfetadas = db.update(MensagemContract.TABELA_MENSAGEM,
                    values,
                    MensagemContract._ID +" = ?",
                    new String[]{ uri.getLastPathSegment() });
            db.close();
            notifyChanges(uri);
            return linhasAfetadas;

        } else {
            throw new UnsupportedOperationException(
                "Uri inválida para atualização.");
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        if (mUriMatcher.match(uri) == MENSAGENS){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            Cursor cursor = db.query(MensagemContract.TABELA_MENSAGEM,
                projection, selection, selectionArgs, null, null, sortOrder);
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;

        } else if (mUriMatcher.match(uri) == MENSAGENS_POR_ID) {
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            Cursor cursor = db.query(MensagemContract.TABELA_MENSAGEM,
                    projection,
                    MensagemContract._ID +" = ?",
                    new String[]{ uri.getLastPathSegment() }, 
                    null, null, sortOrder);
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;

        } else {
            throw new UnsupportedOperationException("Not yet implemented");
        }
    }

    private void notifyChanges(Uri uri){
        if (getContext() != null 
                && getContext().getContentResolver() != null){
            getContext().getContentResolver().notifyChange(uri, null);
        }
    }
}
A primeira coisa que devemos ter em mente é que podemos inserir, alterar, excluir e obter registros em qualquer lugar. Aqui estamos utilizando um banco de dados SQLite, mas nada impede de utilizarmos outras fontes.
Vamos entender um pouquinho do nosso provider:
  • Se acessarmos o provider utilizando qualquer endereço (representado por um Uri) que comece com "content://ngvl.android.exrecycler" ele será chamado. Para restringir esse acesso para apenas alguns endereços específicos utilizamos a classe UriMatcher. Com ela, limitamos o acesso ao provider a apenas dois tipos de endereços:
    • content://ngvl.android.exrecycler/msgs que será utilizado quando não quisermos acessar nenhuma mensagem específica. Utilizaremos esse endereço para inserir uma mensagem e recuperar a lista de mensagens cadastradas. Identificamos esse tipo de endereço como MENSAGENS  com o valor 1.
    • content://ngvl.android.exrecycler/msgs/ID_DA_MENSAGEM será utilizado quando quisermos realizar uma operação em um registro específico. Por exemplo, quando formos excluir ou alterar um registro. Nesse caso, definimos que esse tipo de endereço é MENSAGENS_POR_ID e tem o valor 2.
    • Para cada operação que realizaremos no provider, checamos o tipo da Uri. Se ela não for de nenhum dos dois tipos especificados, levantamos uma exceção.
  • No método insert(), estamos utilizando nossa classe DbHelper para obter a instância do banco e fazer a inclusão no banco de dados. O detalhe importante aqui é que após inserir o registro, estamos invocando o método notifyChange().
    Ele é crucial, pois avisará que o provider representado pela Uri recebida como parâmetro sofreu modificações. No nosso exemplo, quando isso acontecer, a lista de mensagens que implementaremos mais adiante será atualizada automaticamente.
  • Os métodos delete e update seguem a mesma lógica do insert. Eles excluem e atualizam um determinado registro e avisam que isso aconteceu invocando o método notifyChange().
  • A outra parte da mágica é feita no método query(). Quando realizamos uma busca no banco de dados, um objeto cursor é retornado e nele, atribuímos uma Uri. Quando essa Uri sofre alguma modificação, esse cursor é notificado e atualizado com as novas informações. E quem está notificando esse cursor? Nosso método notifyChange() ;)
    Outra curiosidade no método query() é que podemos obter uma mensagem utilizando a Uri content://ngvl.android.exrecycler/msgs/ID_DA_MENSAGEM.
Certifique-se de que o Content Provider está declarado no AndroidManifest.xml como a seguir:
<application ...>
    <provider
        android:name=".MensagemProvider"
        android:authorities="ngvl.android.exrecycler"
        android:enabled="true"
        android:exported="true">
    </provider>
Criadas as classes básicas e o mecanismo de persistência no banco, vamos começar a implementação da interface gráfica do nosso exemplo que implementaremos no próximo post.

4br4ç05,
nglauber

sexta-feira, 12 de fevereiro de 2016

Dominando o Android 2 edição: correções e melhorias

Olá povo,

Quem me vê no dia a dia, sabe que eu ando com um exemplar do meu livro o tempo todo. Além de utiliza-lo como forma de consulta, eu saio anotando nele:
  • as novidades em APIs que saíram após o lançamento do livro; 
  • novos tópicos para colocar em versões futuras; 
  • e obviamente erros que os leitores vão reportando. 
Na primeira edição, o exemplar que eu andava foi muito útil para melhorar a segunda edição, mas quase que eu perco o livro quando ele caiu em uma poça d'água! o.O
Por isso resolvi compartilhar com vocês minhas anotações, assim vocês já vão sabendo o que está errado, o que já mudou em termos de API, e o que pode mudar para a próxima ;) A medida que for achando mais problemas, vou atualizando esse post.
Ah! Antes que vocês perguntem, não tem nenhuma previsão para a próxima edição. Mas provavelmente o que vai acontecer é eu começar a trabalhar no livro após o Google I/O desse ano que vai acontecer entre 18 e 20 de maio.
Segue abaixo o que eu anotei até agora. Eu dividi em duas categorias: melhorias e erros.

Melhorias

Capítulo 0 / Pág. 29 - essa seção deve receber uma atualização com as novidades do emulador 2.0.
http://tools.android.com/tech-docs/emulator

Capítulo 3 - vou adicionar mais uma seção nesse capítulo falando dos layouts percentuais PercentFrameLayout e PercentRelativeLayout que eu falei nesse post aqui.

Capítulo 4 / Pág. 132 - esqueci de falar de uma propriedade bacana do TextInpuLayout que mostra a quantidade de caracteres digitados.
http://developer.android.com/reference/android/support/design/widget/TextInputLayout.html#setCounterEnabled(boolean)
<android.support.design.widget.TextInputLayout
    ...
    app:counterEnabled="true">
    <EditText
        ...
        android:maxLength="100"/>

Capítulo 4 / Pág. 163 - esqueci de mencionar que o método performFiltering() é executado em uma thread separada, enquanto que o método publishResults() é executado na UI thread.

Capítulo 6 / Pág. 254 - deveria ter criado um estilo para a Toolbar.

Capítulo 7 / Pág. 297 - era para ter enfatizado que, se uma classe implementar OnPreferenceChangeListener, caso uma SharedPreference seja modificada em qualquer ponto da aplicação, essa classe será notificada (obviamente se ela estiver instanciada).

Capítulo 7 / Pág. 310 - o trecho do método onClick() deveria estar em negrito.

Capítulo 7 / Pág. 325 - deveria estar realizando as operação com o ContentProvider utilizando a classe AsyncQueryHandler como falei nesse post aqui.

Capítulo 10 / Pág. 388 - substituir a enum Status por annotations como falei nesse post.

Capítulo 14 / Pág. 473 - no método onMessageReceived(), checar se from.equals(senderId) e se !data.isEmpty().

Capítulo 15 / Pág. 519 - o método getMap() tornou-se obsoleto (deprecated), em seu lugar, deve-se usar o método getMapAsync().
https://developers.google.com/android/reference/com/google/android/gms/maps/SupportMapFragment.html#getMapAsync(com.google.android.gms.maps.OnMapReadyCallback)

Capítulo 15 / Pág. 551 - podemos enviar localização fictícia com o emulador 2.0 do Android ou com o Genymotion.

Capítulo 20 - Falar sobre TransitionManager. Depois de ver essa palestra do Lúcio Maciel, me arrependi de não ter colocado esse assunto nesse capítulo.
https://www.youtube.com/watch?v=GKjPf0NI9ps

Capítulo 23 - Muitos fundamentos interessantes que são mostrados no treinamento de Gradle do Udacity seriam interessantes de colocar no livro. Principalmente linha de comando.
https://www.udacity.com/course/gradle-for-android-and-java--ud867

Capítulo 26 / Pág. 843 - Colocar alguns detalhes interessantes no uso da RecyclerView que eu coloquei nesse post.

Capítulo 26 / Pág. 891 - A biblioteca Otto foi recentemente marcada como deprecated em favor do RxJava e RxAndroid. Mas não se preocupem, o Otto continua funcionando. Uma outra opção seria usar o EventBus.

Capítulo 27 - Colocar um exemplo de DataBinding com listas.
http://developer.android.com/intl/pt-br/tools/data-binding/guide.html


Erros

Capítulo 1 / Pág. 39 e 49 - Quando falo das pasta res/mipmap eu menciono quatro pastas, quando na verdade são 5.

Capítulo 2 / Página 81 - Ao girar a activity sem selecionar um estado, o texto do botão fica em branco. Para contornar isso, basta colocar o código a seguir no onCreate.

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    if (savedInstanceState != null) {
        estado = savedInstanceState.getString(STATE_ESTADO);
        if (estado != null) botaoEstado.setText(estado);
    }
}

Capítulo 3 / Pág. 110 - a biblioteca de compatibilidade do GridLayout é melhor do que a versão nativa, pois ela teve vários bugs corrigidos.
http://developer.android.com/tools/support-library/features.html#v7-gridlayout
Basta adicionar a dependência no build.gradle.
compile 'com.android.support:gridlayout-v7:+'
E no layout modificar para
<android.support.v7.widget.GridLayout ...

Capítulo 5 / Pág. 170 - a figura 5.6 está errada... ela deveria estar mostrando as marcações na ferramenta draw9patch. Ao invés disso está mostrando o GIMP :P

Capítulo 6 / Pág. 212 - No último parágrafo, a primeira frase deve ser "... o parâmetro savedInstanceState é igual a nulo, ..."

Capítulo 6 / Pág. 255 - o exemplo está com problema na Toolbar quando rodamos a aplicação no KitKat. A correção do bug já está no GitHub nesse commit aqui. E eu fiz até um vídeo explicando a solução :)

Capítulo 10 / Pág. 385 - na definição da tabela Hotel, o campo "estrelas" deve usar o tipo DECIMAL(2,1). Essa modificação já está no GitHub.
https://github.com/nglauber/dominando_android2/blob/master/server/hotel_service/webservice.php

Capítulo 14 / Pág. 511 - no último parágrafo, falei que após o realizar o login com a conta do Google, uma boa opção seria utilizar o ID da conta e enviar para o seu serviço web. Mas a solução recomendada pelo Google é o uso do Token.
http://android-developers.blogspot.com.br/2016/01/using-google-sign-in-with-your-server.html

Capítulo 24 / Pág. 764 - a figura 24.8 ficou "estranha".

Capítulo 25 / Pág. 839 - esse foi o pior erro do livro. Ficou faltando praticamente uma página.
Notei que um trecho grande do capítulo 26 (1ª impressão) acabou ficando de fora do livro. Creio que foi no momento da diagramação do livro na editora. Na página 839, no segundo parágrafo, após o texto "... que estamos executando." deveria vir o texto abaixo:

Biblioteca 1: ButterKnife
O ButterKnife (http://jakewharton.github.io/butterknife/) é um biblioteca desenvolvida por Jake Wharton (Google Developer Expert de Android) que facilita a atribuição de views à classes sem precisar utilizar o método findViewById(int).
Vejamos o exemplo a seguir:
public class MainActivity extends Activity {
  @Bind(R.id.txtTitulo) TextView mTxtTitulo;
  @Bind(R.id.edtNome) EditText mEdtNome;
  @Bind(R.id.listview) ListView mListPessoas;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    // Pronto! Pode usar os atributos!
  }
}
Perceba que anotamos os atributos da classe que representam componentes no arquivo de layout com a anotação @Bind(int) passando o id do componente. No método onCreate(Bundle) inicializamos esses atributos simplemente invocando ButterKnife.bind(this). O ButterKnife também funciona em Fragments e Adapters como veremos mais adiante. Essa biblioteca ainda possui outros recursos interessantes como definir evento de clique em botões.
@OnClick(R.id.botao1)
public void onClickBotao1(View view) {
}

@OnClick(R.id.botao2)
public void onClickBotao2() {
}
Perceba que no evento de clique do segundo botão, não colocamos o parâmetro, pois para a biblioteca ele é opcional. Vamos utilizar o ButterKnife em nosso aplicativo. Para isso, adicione a dependência no build.gradle.
dependencies {
    …
    compile 'com.jakewharton:butterknife:7.0.1'
}

Biblioteca 2: OkHttp
No capítulo 8 vimos como acessar um servidor web utilizando a classe HttpUrlConnection. Ela funciona muito bem, o único incoveniente é quando precisamos ler o retorno do servidor,

x.x.x.x.x.x.x.x.x.x.x

Peço desculpas pelo inconveniente, mas pelos menos agora vocês vão entender porque o texto está maluco :)
------------------

Quero deixar meu muito obrigado a todos os leitores que reportam os erros encontrados no livro. E se você adquiriu ou livro, não se esqueça de se cadastrar no grupo de discussão da segunda edição do "Dominando o Android".

4br4ç05,
nglauber

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 LinearLayoutManager e StaggeredGridLayoutManager. 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.