Mostrando postagens com marcador ContentProvider. Mostrar todas as postagens
Mostrando postagens com marcador ContentProvider. Mostrar todas as postagens

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

terça-feira, 26 de novembro de 2013

ContentProvider, CursorLoader e CursorAdapter

Olá povo,

Quem já assistiu minhas aulas ou alguma palestra minha sobre Android sabe que eu sempre falo que o Android tem 4 componentes básicos: Activities, Services, Broadcast Receivers e Content Providers.
Nesse post vou falar sobre ContentProvider e os benefícios de utilizá-lo junto com um CursorLoader e um CursorAdapter.

Um ContentProvider, como o próprio nome diz, é um provedor de conteúdo que nos permite compartilhar informações da nossa aplicação com outras aplicações. Essas informações normalmente são dados do nosso banco de dados SQLite (mas nada impede que venha de outra fonte). Ao meu ver, as grande vantagens de utilizar esse mecanismo são: 1) se quisermos alterar a forma de persistir os dados, teremos apenas que alterar o provider internamente; 2) podemos deixar o provider público ou privado; 3) os dados ficam sincronizados com o cursor adapter, ou seja, alterou no provider, a tela é automaticamente atualizada.

No nosso exemplo, faremos um pequeno aplicativo que salva textos no SQLite. Para tal, vamos criar uma classe que herda de SQLiteOpenHelper e que vai criar a tabela.
public class DBHelper extends SQLiteOpenHelper {

  public static final String TABLE_NAME = "messages";
  public static final String COLUMN_ID = "_id";
  public static final String COLUMN_MESSAGE="message";
 
  public static final String[] ALL_COLUMNS = { 
    COLUMN_ID, COLUMN_MESSAGE
  }; 
 
  private static final String NOME_BANCO="dbMessages";
  private static final int    VERSAO_BANCO = 1;
 
  public DBHelper(Context context) {
    super(context, NOME_BANCO, null, VERSAO_BANCO);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL(
      "CREATE TABLE "+ TABLE_NAME +" ("+ 
      COLUMN_ID+" INTEGER PRIMARY KEY AUTOINCREMENT,"+
      COLUMN_MESSAGE +" TEXT )");
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, 
    int oldVersion, int newVersion) {
    // Utilizar só na proxima versão :)
  }
}
Essa classe ficará responsável por criar o banco (e as suas tabelas) se não existir, ou atualizá-las caso já existam (em uma nova versão da app por exemplo).

Declare o Content Provider no AndroidManifest.xml.
<provider
  android:name="ngvl.android.excp.db.MessageProvider"
  android:authorities="ngvl.android.excp"
  android:exported="false" />
Agora, vamos implementar nosso Content Provider conforme abaixo.
public class MessageProvider extends ContentProvider {
  // Deve estar igual ao Manifest
  private static final String 
    AUTHORITY = "ngvl.android.excp";
  // Tipo de acesso que retorna todas as mensagens
  private static final int TYPE_ALL_MESSAGES = 1;
  // Tipo de acesso que retorna apenas uma mensagem
  // usando o id da mesma
  private static final int TYPE_SINGLE_MESSAGE = 2;

  private static final String BASE_PATH = "messages";
  // É através dessa URI que acessamos nosso provider
  public static final Uri CONTENT_URI = Uri.parse(
    "content://" + AUTHORITY + "/" + BASE_PATH);

  // Classe para checar se a Uri passada é valida 
  private static final UriMatcher sUriMatcher;

  static {
    sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    sUriMatcher.addURI(AUTHORITY, 
      BASE_PATH, TYPE_ALL_MESSAGES);
    sUriMatcher.addURI(AUTHORITY, 
      BASE_PATH + "/#", TYPE_SINGLE_MESSAGE);
  }

  private DBHelper mOpenHelper;

  @Override
  public boolean onCreate() {
    // Ao criar o Provider, inicializamos o helper
    mOpenHelper = new DBHelper(getContext());
    return true; // success
  }

  @Override
  public String getType(Uri uri) {
    return null;
  }

  @Override
  public Uri insert(Uri uri, ContentValues values) {
    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();
    long id = 0;

    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      id = sqlDB.insert(
        DBHelper.TABLE_NAME, 
        null, 
        values);
      break;
   
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);

    return Uri.parse(BASE_PATH + "/" + id);
  }

  @Override
  public int update(Uri uri, ContentValues values, 
    String selection, String[] selectionArgs) {

    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();

    int rowsUpdated = 0;

    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      rowsUpdated = sqlDB.update(
        DBHelper.TABLE_NAME, 
        values, 
        selection,
        selectionArgs);
      break;

    case TYPE_SINGLE_MESSAGE:
      String id = uri.getLastPathSegment();
      if (TextUtils.isEmpty(selection)) {
        rowsUpdated = sqlDB.update(
          DBHelper.TABLE_NAME, 
          values,
          DBHelper.COLUMN_ID + "=" + id, 
          null);
      } else {
        rowsUpdated = sqlDB.update(
          DBHelper.TABLE_NAME, 
          values,
          DBHelper.COLUMN_ID +"="+ id +
            " and "+ selection,
          selectionArgs);
      }
      break;

    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);
    return rowsUpdated;
  }

  @Override
  public int delete(Uri uri, String selection, 
    String[] selectionArgs) {

    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();

    int rowsDeleted = 0;
    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      rowsDeleted = sqlDB.delete(
        DBHelper.TABLE_NAME, 
        selection,
        selectionArgs);
      break;

    case TYPE_SINGLE_MESSAGE:
      String id = uri.getLastPathSegment();
      if (TextUtils.isEmpty(selection)) {
        rowsDeleted = sqlDB.delete(
          DBHelper.TABLE_NAME,
          DBHelper.COLUMN_ID + "=" + id, 
          null);
      } else {
        rowsDeleted = sqlDB.delete(
          DBHelper.TABLE_NAME,
          DBHelper.COLUMN_ID +"="+ id + 
            " and " + selection,
          selectionArgs);
      }
      break;
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);
    return rowsDeleted;
  }

  @Override
  public Cursor query(Uri uri, String[] projection, 
    String selection, String[] selectionArgs, 
    String sortOrder) {

    SQLiteQueryBuilder queryBuilder = 
      new SQLiteQueryBuilder();

    queryBuilder.setTables(DBHelper.TABLE_NAME);

    int uriType = sUriMatcher.match(uri);
    Cursor cursor = null;
    SQLiteDatabase db = 
      mOpenHelper.getWritableDatabase();
  
    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      cursor = queryBuilder.query(
        db, 
        projection, 
        selection,
        selectionArgs, 
        null, 
        null, 
        sortOrder);   
      break;

    case TYPE_SINGLE_MESSAGE:
      queryBuilder.appendWhere(
        DBHelper.COLUMN_ID + "= ?");

      cursor = queryBuilder.query(
        db, 
        projection, 
        selection,
        new String[]{ uri.getLastPathSegment() },
        null,
        null,
        null);
        break;
   
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    cursor.setNotificationUri(
      getContext().getContentResolver(), uri);
  
    return cursor;
  }
}
Nossa classe herda de ContentProvider e declaramos uma constante para o authority da mesma. Esse atributo deve estar igual ao que definimos no AndroidManifest.xml pois ele faz parte do caminho da Uri que utilizaremos para acessar o provider.
Nosso provedor de conteúdo responde por dois tipos de Uri: content://ngvl.android.excp/messages e content://ngvl.android.excp/messages/X onde "X" é o id da mensagem que queremos acessar.
Por isso, instanciamos um objeto do tipo UriMatcher para checar se a Uri passada bate (match) com uma das Uris disponíveis.
No onCreate instanciamos nosso SQLiteHelper, e depois temos os quatro principais métodos: insert, update, delete e query.
Os quatro métodos são bem parecidos, eles iniciam checando que tipo de Uri foi passada e instanciando o SQLiteDatabase a partir do nosso helper. Depois efetuamos a operação correspondente no banco de dados. A parte mais importante é que em todos os quatro métodos notificamos os cursores que alguma operação foi efetuada através da chamada: getContext().getContentResolver().notifyChange(uri, null).
Com isso, o adapter que estiver observando esse ContentProvider será atualizado automaticamente.

Agora vamos criar o CursorAdapter que acessará nosso provider para exibir as mensagens na tela. Ele é um pouquinho diferente do BaseAdapter, pois contém o método newView para quando uma nova View precisa ser criada e o bindView para quando precisamos apenas preencher os componentes da View.
public class MessageCursorAdapter 
  extends SimpleCursorAdapter {

  private static final int LAYOUT = 
    R.layout.item_message;

  public MessageCursorAdapter(
    Context context, Cursor cursor) {

    super(context, LAYOUT, cursor, 
      DBHelper.ALL_COLUMNS, null, 0);
  }

  @Override
  public void bindView(View view, Context context, 
    Cursor cursor) {

    TextView txtMessage = (TextView) 
      view.findViewById(R.id.txtMessage);
    TextView txtId = (TextView) 
      view.findViewById(R.id.txtId);

    txtId.setText(
      cursor.getString(
        cursor.getColumnIndex(
          DBHelper.COLUMN_ID)));
    txtMessage.setText(
      cursor.getString(
        cursor.getColumnIndex(
          DBHelper.COLUMN_MESSAGE)));
  }

  @Override
  public View newView(Context contex, Cursor cursor, 
    ViewGroup viewGroup) {

    return LayoutInflater.from(contex).inflate(
      LAYOUT, null);
  }
}
O arquivo de layout usado pelo adapter é listado abaixo.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >

  <TextView
    android:id="@+id/txtMessage"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:text="@null"
    android:textSize="20sp" />

  <TextView
    android:id="@+id/txtId"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@null"
    android:textSize="20sp"
    android:textColor="#FF0000" />
</LinearLayout>
Agora é só usar na Activity. No nosso exemplo temos um EditText, um Button e uma ListView. Ao clicar no botão, adicionamos o texto no banco de dados via ContentProvider. Ao clicar em um item da lista, o EditText é preenchido e se clicarmos em salvar o mesmo será atualizado. Se clicarmos no item, apagar o conteúdo do EditText e clicarmos em salvar, o mesmo será excluído. Assim, podemos testar todas as operações do nosso ContentProvider.
É na Activity que usamos o CursorLoader. Esse padrão é essencial no uso de banco de dados no Android, pois ele fará a consulta dos dados em segundo plano, evitando assim o bloqueio da Thread de UI, que faz com que a aplicação apareça estar travada se tivermos com uma grande quantidade de dados. A interface LoaderCallbacks notifica quando podemos criar o loader, quando a busca foi concluída e quando o cursor foi resetado por algum motivo.
public class MainActivity extends FragmentActivity 
  implements 
    LoaderManager.LoaderCallbacks<Cursor>, 
    OnClickListener, 
    OnItemClickListener {

  SimpleCursorAdapter mAdapter;
 
  EditText mEdtMessage;
 
  boolean isEditing;
  long currentMessageId;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  
    mEdtMessage = (EditText)
      findViewById(R.id.edtMessage);
    findViewById(R.id.btnAdd).setOnClickListener(this);
  
    mAdapter = new MessageCursorAdapter(this, null);
    getSupportLoaderManager().initLoader(0, null, this);
  
    ListView listView = (ListView)
      findViewById(R.id.listMessages);
    listView.setOnItemClickListener(this);
    listView.setAdapter(mAdapter);
  }

  @Override
  public Loader<Cursor> onCreateLoader(
    int id, Bundle args) {

    return new CursorLoader(
      this,
      MessageProvider.CONTENT_URI,
      DBHelper.ALL_COLUMNS,
      null, 
      null, 
      DBHelper.COLUMN_ID);
  }

  @Override
  public void onLoadFinished(
    Loader<Cursor> loader, Cursor cursor) {

    mAdapter.swapCursor(cursor);
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
  }

  @Override
  public void onClick(View v) {
    String message = mEdtMessage.getText().toString();
  
    if (!isEditing && TextUtils.isEmpty(message)){
      Toast.makeText(this, "Preencha a mensagem", 
        Toast.LENGTH_SHORT).show();
      return;
    }
  
    mEdtMessage.getText().clear();
  
    ContentValues values = new ContentValues();
    values.put(DBHelper.COLUMN_MESSAGE, message);
  
    if (isEditing){
      String whereClause = DBHelper.COLUMN_ID +" = ?";
      String[] whereArgs = new String[]{ 
        String.valueOf(currentMessageId) };

      if (TextUtils.isEmpty(message)){
        getContentResolver().delete(
          MessageProvider.CONTENT_URI, 
          whereClause,
          whereArgs);

      } else {
          getContentResolver().update(
            MessageProvider.CONTENT_URI, 
            values, 
            whereClause,
            whereArgs);
      }

    } else {
      getContentResolver().insert(
        MessageProvider.CONTENT_URI, 
        values);
    }
    isEditing = false;
  }

  @Override
  public void onItemClick(AdapterView<?> adaptView, 
    View view, int position, long id) {

    Cursor cursor = mAdapter.getCursor();
    cursor.moveToPosition(position);
  
    long messageId = cursor.getLong(
      cursor.getColumnIndex(DBHelper.COLUMN_ID));

    String messageText = cursor.getString(
      cursor.getColumnIndex(DBHelper.COLUMN_MESSAGE));
  
    currentMessageId = messageId;
    mEdtMessage.setText(messageText);
    isEditing = true;
  }
}
Pra que ninguém reclame, e peça o código fonte, segue abaixo o arquivo de layout :)
<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=".MainActivity" >

  <Button
    android:id="@+id/btnAdd"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    android:text="Save" />

  <EditText
    android:id="@+id/edtMessage"
    android:hint="Digite a mensagem aqui"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/btnAdd"
    android:layout_alignBottom="@+id/btnAdd"
    android:layout_alignParentLeft="true"
    android:layout_toLeftOf="@+id/btnAdd"/>

  <ListView
    android:id="@+id/listMessages"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/edtMessage"
    android:layout_below="@+id/btnAdd" />

</RelativeLayout>

Bem povo, esse post ficou gigante. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber