Persistência de Dados Com Realm no Android - Parte 1
(11591) (15)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Opa, blz?
Nesse vídeo dou inicio a uma nova série no blog, dessa vez sobre persistência de dados, mais precisamente sobre a lib Realm (suporte a partir da API 9), que assume o papel de ser a mais eficiente maneira de persistência local no Android (comparando com o SQLite puro e libraries que utilizam o SQLite).
O Realm veio do IOS e já é utilizado em modo de produção desde 2012 por algumas APPs na AppStore. Para o Android ele está disponível desde 2014 com já algumas grandes APPs o utilizando. O uso é bem simples, porém fique atento quanto ao uso dos métodos setters quando utilizando um objeto recuperado da base Realm, pois não há cópia, os objetos têm acesso direto aos dados da base Realm (salva no disco em arquivos .realm) realizando assim uma atualização ao invés de uma simples alteração de dados como em um objeto Java convencional. Com o decorrer do post e vídeo ficará mais tranquilo de entender. Veja um comparativo de inserção de dados:
Note que o Realm é thread safety, ou seja, a atualização de dados na base Realm em uma Thread será refletida em todas as outras Threads (vinculadas ao Looper), caso a Thread não esteja vinculada ao Looper será necessário chamar o método refresh() para evitar o uso de dados antigos aumentando as chances de ter problemas de memory leak, por exemplo. Caso o problema de memory leak venha a acontecer e seja com isso utilizado um bloco try...catch para manter a APP funcionando, terá um grande risco de conrromper a base Realm e ai "a coisa fica preta", logo o recomendado pela documentação da library é terminar a APP nesse tipo de exception, OutOfMemoryException.
Abaixo segue a primeira parte (post) de uma série. Aqui vamos trabalhar a inserção, atualização e remoção de disciplinas em nossa APP de Estudantes (RealmStudents). O primeiro passo é adicionar a library no gradle app level (Module: app) como abaixo:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.0'
compile 'io.realm:realm-android:0.84.1'
}
Note que aqui vou colocar as partes chaves de utilização da library nesse Android project, para acessar o projeto completo veja no GitHub (https://github.com/viniciusthiengo/realm-students). Segue imagem da estrutura do projeto:
O passo 2 é colocar nossa classe do domínio do problema com as configurações exatas para ser utilizada como parte integrante do Realm scheme:
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Discipline extends RealmObject {
public static final String ID = "br.com.thiengo.realmstudents.domain.RealmObject.ID";
@PrimaryKey
private long id;
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name == null ? "" : name;
}
public void setName(String name) {
this.name = name;
}
}
Note que a constante configurada na classe Discipline será utilizada posteriormente para que seja possível enviar o id da disciplina que será atualizada na entidade de atualização da APP, via Intnent. O annotation @PrimaryKey é da library Realm e faz o que o nome diz, coloca o atributo abaixo como sendo o Primary Key na "tabela" / objeto de Discipline no Realm, implicitamente o atributo que é @PrimaryKey é também @Index (que permite uma busca mais rápida apesar de a inserção e atualização serem mais lentas) e @Required (que obriga a definição de valor, porém como está sendo utilizado em um tipo primitivo, esses já iniciam com um valor, exceto String que inicia como null). Ponto negativo a ser notado nessa parte é que a principio não é possível a utilização de algo equivalente ao AUTO_INCREMENT do SQL e também não é possível utilizar @primeryKey composta.
O passo 3 é acessar a base Realm em nossa MainActivity para que possamos colocar no button "Disciplinas" de nosso layout o número de disciplinas ativas. Vamos acessar também o método where() que vai permitir buscar os resultados de acordo com a classe definida como parâmetro (Discipline.class, em nosso exemplo) e com os condicionais setados (não utilizados ainda nessa Activity):
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import br.com.thiengo.realmstudents.domain.Discipline;
import io.realm.Realm;
import io.realm.RealmResults;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onResume() {
super.onResume();
Button btDisciplines = (Button) findViewById(R.id.bt_disciplines);
Realm realm = Realm.getInstance(this);
RealmResults<Discipline> disciplines = realm.where(Discipline.class).findAll();
btDisciplines.setText( "Disciplinas ("+disciplines.size()+")" );
realm.close();
}
public void callDisciplines( View view){
Intent it = new Intent(this, DisciplinesActivity.class);
startActivity(it);
}
public void callStudents( View view){ }
}
Com isso conseguimos já lançar nossa APP de exemplo no emulador e consequentemente acessar o local onde o arquivo.realm foi criado e visualizar se o acesso as disciplinas para retornar o número total ocorreu ou não:
Para acessar o local do arquivo.realm vá em Tools > Android > Android Device Monitor, depois na parte esquerda da box aberta clique no device que está utilizando no exemplo, então na parte direita, no top, clique na aba "File Explorer", no conteúdo apresentado abaixo siga o caminho data/data/nomeDoPackageDeSeuProject/files/ então terá acesso a algo similar ao apresentado na figura abaixo:
Segue o layout da MainActivity (activity_main.xml):
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="5dp"
android:text="APP Realm Students"
android:textSize="18sp" />
<LinearLayout
android:layout_gravity="bottom"
android:padding="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/bt_disciplines"
android:text="Disciplinas"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginRight="5dp"
android:layout_marginEnd="5dp"
android:onClick="callDisciplines"/>
<Button
android:id="@+id/bt_students"
android:text="Estudantes"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="callStudents"/>
</LinearLayout>
</FrameLayout>
O passo 4 é configurarmos a Activity responsável por apresentar a lista de disciplinas já salvas em nossa base Realm. Vamos começar pelo layout que será utilizado na Activity DisciplinesActivity (activity_disciplines):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/lv_disciplines"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:onClick="callAddDiscipline"
android:text="Add disciplina" />
</LinearLayout>
Abaixo segue a configuração de classe já com Realm sendo utilizado junto ao RealmResults e a ListView do layout:
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ListView;
import br.com.thiengo.realmstudents.adapter.DisciplineAdapter;
import br.com.thiengo.realmstudents.domain.Discipline;
import io.realm.Realm;
import io.realm.RealmChangeListener;
import io.realm.RealmResults;
public class DisciplinesActivity extends AppCompatActivity {
private Realm realm;
private RealmResults<Discipline> disciplines;
private RealmChangeListener realmChangeListener;
private ListView lvDisciplines;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_disciplines);
realm = Realm.getInstance(this);
realmChangeListener = new RealmChangeListener() {
@Override
public void onChange() {
((DisciplineAdapter) lvDisciplines.getAdapter()).notifyDataSetChanged();
}
};
realm.addChangeListener(realmChangeListener);
disciplines = realm.where( Discipline.class ).findAll();
lvDisciplines = (ListView) findViewById(R.id.lv_disciplines);
lvDisciplines.setAdapter( new DisciplineAdapter( this, disciplines, false ));
}
@Override
protected void onDestroy() {
realm.removeAllChangeListeners();
realm.close();
super.onDestroy();
}
public void callAddDiscipline( View view){
Intent it = new Intent( this, AddUpdateDisciplineActivity.class );
startActivity(it);
}
}
Note o method callAddDiscipline() setado para que seja possível acessar a Activity que permitirá novas disciplinas e note também o adapter DisciplineAdapter sendo setado junto ao ListView lvDisciplines, esse adapter é o nosso próximo passo, segue o código completo da classe DisciplineAdapter que extends RealmBaseAdapter<Discipline> e implementa a interface ListAdapter:
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.TextView;
import br.com.thiengo.realmstudents.AddUpdateDisciplineActivity;
import br.com.thiengo.realmstudents.R;
import br.com.thiengo.realmstudents.domain.Discipline;
import io.realm.Realm;
import io.realm.RealmBaseAdapter;
import io.realm.RealmResults;
public class DisciplineAdapter extends RealmBaseAdapter<Discipline> implements ListAdapter {
public DisciplineAdapter( Context context, RealmResults<Discipline> realmResults, boolean automaticUpdate ){
super(context, realmResults, automaticUpdate);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
CustomViewHolder holder;
if( convertView == null ){
convertView = inflater.inflate(R.layout.item_discipline, parent, false);
holder = new CustomViewHolder();
convertView.setTag( holder );
holder.tvName = (TextView) convertView.findViewById(R.id.tv_name);
holder.btUpdate = (Button) convertView.findViewById(R.id.bt_update);
holder.btRemove = (Button) convertView.findViewById(R.id.bt_remove);
}
else{
holder = (CustomViewHolder) convertView.getTag();
}
final Discipline d = realmResults.get(position);
holder.tvName.setText( d.getName() );
holder.btUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent it = new Intent(context, AddUpdateDisciplineActivity.class);
it.putExtra(Discipline.ID, d.getId());
context.startActivity(it);
}
});
holder.btRemove.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Realm realm = Realm.getInstance(context);
realm.beginTransaction();
d.removeFromRealm();
realm.commitTransaction();
realm.close();
}
});
return convertView;
}
private static class CustomViewHolder{
TextView tvName;
Button btUpdate;
Button btRemove;
}
}
O button btUpdate é apensa responsável por também chamar a Activity de AddUpdateDisciplineActivity, como o button Add de DisciplinesActivity, porém o button btRemove já realiza uma alteração na base Realm, logo temos de chamar o método que permite a remoção do dado entre a chamada do método beginTransaction() e commitTransaction(), caso contrário uma exception é gerada e nada é alterado.
Note que os objetos não são cópias de dados que foram recuperados da base Realm, na verdade são utilizados objetos de classes Realm proxies que sobrescrevem os métodos getters e setters e que permitem o acesso direto aos dados na base Realm, ou seja, os objetos Real em si são vazios, por isso eles são ainda mais leves que os objetos convencionais Java. A problemática pode morar no momento de utilizar um método set no decorrer do código, pois como o objeto proxy acessa e atualiza os dados diretamente na base Realm, o acesso aos métodos setters do objeto devem ser realizados entre as chamadas de métodos beginTransaction() e commitTransaction() (cancelTransaction() também pode ser utilizado caso for necessária essa ação), como no modo remover apresentado no código acima, caso isso não seja seguido uma IllegalStateException será gerada com a seguinte mensagem: Changing Realm data can only be done from inside a transaction.
Finalizado esse passo, o que devemos fazer agora é implementar a Activity que permite adicionar e atualizar disciplinas na APP, caso contrário não será possível visualizá-las.
No passo 5 segue implementação da Activity AddUpdateDisciplineActivity e layout activity_add_update_discipline.xml, começando por esse último:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:gravity="center"
android:padding="5dp"
android:text="Nova disciplina"
android:textSize="18sp" />
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_title"
android:layout_marginTop="10dp"
android:hint="Nome" />
<Button
android:id="@+id/bt_add_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="5dp"
android:onClick="callAddUpdateDiscipline"
android:text="Add" />
</RelativeLayout>
Segue código da Activity AddUpdateDisciplineActivity:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import br.com.thiengo.realmstudents.domain.Discipline;
import io.realm.Realm;
import io.realm.RealmResults;
public class AddUpdateDisciplineActivity extends AppCompatActivity {
private Realm realm;
private RealmResults<Discipline> disciplines;
private Discipline discipline;
private EditText etName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_update_discipline);
discipline = new Discipline();
etName = (EditText) findViewById(R.id.et_name);
TextView tvTitle = (TextView) findViewById(R.id.tv_title);
Button btAddUpdate = (Button) findViewById(R.id.bt_add_update);
realm = Realm.getInstance(this);
disciplines = realm.where( Discipline.class ).findAll();
if( getIntent() != null && getIntent().getLongExtra( Discipline.ID, 0 ) > 0 ){
discipline.setId( getIntent().getLongExtra( Discipline.ID, 0 ) );
discipline = disciplines.where().equalTo("id", discipline.getId()).findAll().get(0);
etName.setText( discipline.getName() );
tvTitle.setText("Atualizar disciplina");
btAddUpdate.setText( "Update" );
}
}
public void callAddUpdateDiscipline( View view ){
String label = "atualizada";
if( discipline.getId() == 0 ){
disciplines.sort( "id", RealmResults.SORT_ORDER_DESCENDING );
long id = disciplines.size() == 0 ? 1 : disciplines.get(0).getId() + 1;
discipline.setId( id );
label = "adicionada";
}
try{
realm.beginTransaction();
discipline.setName(etName.getText().toString() );
realm.copyToRealmOrUpdate(discipline );
realm.commitTransaction();
Toast.makeText(AddUpdateDisciplineActivity.this, "Disciplina "+label, Toast.LENGTH_SHORT).show();
finish();
}
catch(Exception e){
e.printStackTrace();
Toast.makeText(AddUpdateDisciplineActivity.this, "Falhou!", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
No onCreate() temos além do acesso a instancias necessárias no contexto do código utilizado na Activity, a verificação do getIntent() para identificar se é uma atualização que está sendo acionada, quando o button btUpdate do DisciplineAdapter é acionado. Nesse script também já estamos utilizando uma forma de condicional para filtrar os dados que estão no conjunto disciplines, que foi preenchido pela busca similar a realizada na MainActivity (realm.where( Discipline.class ).findAll()). O método equalTo nos permite informa o campo / coluna no Realm scheme que queremos igualdade ao valor informado como segundo parâmetro. Dessa forma, no exemplo, vamos obter a disciplina correta no momento de atualização e assim já deixar o campo nome pré-preenchido.
Não coloquei no exemplo, mas o equalTo() pode ser utilizado com um terceiro parâmetro que indica se é para levar em consideração se caixa alta ou não, útil quando comparação com String. No caso utilizaria RealmQuery.CASE_INSENSITIVE para quando a caixa não tem importância e RealmQuery.CASE_SENSITIVE quando tem.
Note que o discipline.setName() já está sendo utilizado entre a chamada dos métodos beginTransaction() e commitTransaction(), isso porque caso seja um update, a variável de instancia discipline terá um objeto retornado da base Realm e não um criado no modelo convencional de objetos Java (new Discipline()), logo o set deve ser entre esses métodos de transaction, caso que não é necessário se for uma inserção de disciplina, pois o que temos é sim o RealmObject (Discipline extends ele), porém não acessando a base Realm, até porque ele ainda não foi inserido, por a necessidade então do método copyToRealmOrUpdate() que recebe como parâmetro um objeto que extends RealmObject e então verifica se já existe algum objeto com o mesmo primaryKey (em nosso exemplo é o campo id), caso sim esse objeto é atualizado na base Realm, caso contrário ele é inserido.
Anterior a inserção temos um script que é responsável por verificar se discipline tem ou não um id igual a 0, caso sim isso indica que devemos ordenar nossa lista de resultados (RealmResult) para que então possamos verificar se há algum item dentro da lista, se sim, colocamos o id desse item na posição 0 que tem o maior id da lista (devido a ordenação utilizada o de maior id é o primeiro da lista) somado a 1 na variável local id, se a lista está vazia, apenas colocamos 1 na variável local, pois é a primeira disciplina a ser inserida.
Inserindo a disciplina ou atualizando temos o feedback em um Toast, como nas figuras abaixo:
Depois já é possível acessar disciplinas em DisciplinesActivity como apresentado abaixo e também utilizar as funcionalidades de remoção e update:
Note que se adicionarmos algum outro atributo a classe Discipline, termos um problema de imcompatibilidade de schema no Realm, pois o Realm utiliza nossas entidades que extends RealmObject para construir e acessar o schema, se fizermos a alteração de adicionar o atributo grade (nota) na classe Discipline como abaixo:
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Discipline extends RealmObject {
public static final String ID = "br.com.thiengo.realmstudents.domain.RealmObject.ID";
@PrimaryKey
private long id;
private String name;
private double grade;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name == null ? "" : name;
}
public void setName(String name) {
this.name = name;
}
public double getGrade() {
return grade;
}
public void setGrade(double grade) {
this.grade = grade;
}
}
E tentarmos roda a APP, teremos uma RealmMigrationNeededException com a seguinte mensagem: RealmMigration must be provided. Que na verdade o que está dizendo é que ou nós provemos uma classe de migração de dados (explicada nos próximos posts) para que a base Realm seja reconstruída sem perder os dados já existentes, ou nós simplesmente deletamos a atual e criamos uma nova com o novo schema. Nesse primeiro post vamos optar pela versão mais simples que é deletar a base Realm existente e então criar uma nova.
Comece essa configuração criando o package "application" e então criando a classe CustomApplication com o seguinte código:
import android.app.Application;
import io.realm.Realm;
import io.realm.RealmConfiguration;
public class CustomApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this)
.name("realm-students.realm")
.deleteRealmIfMigrationNeeded()
.build();
Realm.setDefaultConfiguration( realmConfiguration );
}
}
O que estamos fazendo é criando uma configuração personalizada e padrão para a utilização da base Realm em nossa APP. O método name é opcional, utilizei para deixar o arquivo.realm com o mesmo nome da APP e não default (esse persiste, pois quando colocamos um name distinto do anterior, o anterior não é sobrescrito e sim continua no diretório, caso seja necessário futuramente para acesso a mais de uma base Realm na APP, caso não seja, delete). O método deleteRealmIfMigrationNeeded() faz o que o nome indica, deleta a base Realm da configuração utilizada caso uma novo schema seja necessário, todos os dados salvos serão também deletados.
Depois da inserção do script acima no projeto é necessário atualizar a tag <application> no AndroidManifest.xml, colocando o atributo name para apontar para nossa CustomApplication:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.realmstudents">
<application
android:name=".application.CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".DisciplinesActivity" />
<activity android:name=".AddUpdateDisciplineActivity"></activity>
</application>
</manifest>
Agora em todo o código do projeto, onde houver a chamada realm.getInstance(this) deverá ser trocada por realm.getDefaultInstance(). Caso isso não seja realizado o erro de inconsistência de schema será ainda printado.
Conclusão
Segundo o site do Realm ele é bem mais rápido que as outras alternativas, incluindo o SQLite no modo raw (sem utilização de libraries intermediárias), SharedPreferences não é considerado nessa comparação. Não fiz ainda teste de carga, mas a não utilização de uma grande quantidade de código para realizar um simples select ou insertion é a principio o grande diferencial, fora que não há necessidade de permissions e nem mesmo de uma configuração complexa, mesmo quando queremos fornecer nossa própria configuração. A documentação apesar de não disponível ainda em português está bem completa e concisa quanto aos exemplos disponibilizados no GitHub da library. A library ainda tem alguns pontos que estão sendo amadurecidos segundo a documanetação (Migração de dados, por exemplo), mas mesmo assim o custo / beneficio ainda achei bastante a favor do uso da library. Para quem está começando será ainda mais fácil trabalhar persistência local e para quem já conhece, vale o teste, pois tende a ser bem mais rápido e leve que as versões com SQLite.
Para complementar o conteúdo do vídeo, se ainda está no inicio do Android ou não conhece o BaseAdapter e a classe Application, veja os vídeos abaixo:
Utilizando BaseAdapter Para Personalização Completa da ListView
Application Class no Android, Entendendo e Utilizando
O projeto completo se encontra no Github: https://github.com/viniciusthiengo/realm-students
Segue site da library:
Vlw. Segue vídeo de implementação.
Comentários Facebook