domingo, 28 de fevereiro de 2016

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

Nenhum comentário: