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
Um comentário:
Cara, muito bom! Obrigado pelo conteúdo!
Postar um comentário