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
2 comentários:
Primeira vez por aqui.. e devo dizer que por esse bom(e enorme!)trabalho, começarei a frequentar o blog:D
Oi Rodrigo,
Valeu! Seja sempre bem-vindo :)
4br4ç05,
nglauber
Postar um comentário