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