Kurs Android (20)
Autor: Damian Chodorek • Opublikowany: 3 września 2016 • Ostatnia aktualizacja: 16 września 2016 • Kategoria: android, kursy
Obiektowa baza danych Realm.
W poprzedniej lekcji dowiedziałeś się w jaki sposób przechowywać dane w aplikacji. Przypomnę tylko, że dwa standardowe sposoby, to SharedPreferences
oraz SQLite. Dzięki klasie SharedPreferences
, możesz przechowywać proste wartości typu klucz : wartość. Drugi sposób, który przeznaczony jest do przechowywania większych i bardziej złożonych zbiorów danych, to baza danych SQLite
, natywnie wspierana przez system.
Dzisiejsza lekcja jest kontynuacją poprzedniej. Jeśli jeszcze się z nią nie zapoznałeś, to warto ją odwiedzić. W poprzedniej lekcji dowiedziałeś się, że prędzej czy później, wyniki które zwróci baza danych SQLite
, należy zmapować do obiektów Java. Dziś pokażę Ci rozwiązanie znacznie prostsze – bibliotekę Realm.
Baza danych Realm
Realm to obiektowa baza danych, która powstała z myślą o urządzeniach mobilnych. Jej głównym celem jest więc szybkość oraz ograniczenie zużycia pamięci. SQLite
jest bazą relacyjną, co oznacza, że opiera się na relacjach reprezentowanych przez tablice. Dlatego też wyróżniamy w niej takie pojęcia jak tabela, wiersz i kolumna. Realm jest bazą obiektową, co oznacza, że obsługujemy ją wyłącznie przy użyciu obiektów języka Java. Nie ma więc konieczności mapowania obiektu na postać nadającą się do zapisu i odczytu do/z bazy danych, ponieważ dzieje się to domyślnie.
Realm jest często wykorzystywaną bazą danych. Głównie ze względu na prostotę jej użycia i szybkość działania. Baza danych jest na tyle szybka, że operacje odczytu, nawet dużego zbioru danych, możemy robić synchronicznie, czyli w głównym wątku. Poważnym ograniczeniem bazy jest jednak brak możliwości przekazywania obiektów bazodanowych pomiędzy wątkami. Nie możemy więc np. odczytać zbioru danych w jednym wątku (w tle), a następnie wyniku operacji przekazać do głównego wątku, aby dane zostały wyświetlone na ekranie. Nastąpi wtedy wyjątek. Aby to obejść, należy skopiować obiekt realmowy na swój własny POJO, z którym możemy zrobić już wszystko.
Wystarczy teorii, czas na przykład. Utwórz nowy projekt com.damianchodorek.kurs.android20.realmapp
. Poniżej layout głównej aktywności activity_main.xml
.
<?xml version="1.0" encoding="utf-8"?>
<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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="android20.kurs.damianchodorek.om.realmapp.MainActivity">
<EditText
android:id="@+id/new_note_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"/>
<Button
android:id="@+id/add_new_note_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:onClick="addNewNote"
android:text="Dodaj"/>
<Button
android:id="@+id/delete_notes_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:onClick="deleteAll"
android:text="Usuń wszystkie"/>
<Button
android:id="@+id/filter_notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:onClick="filterNotes"
android:text="Filtruj"/>
<Button
android:id="@+id/show_realm_notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:onClick="showRealmNotes"
android:text="Pokaż Realm"/>
<ListView
android:id="@+id/note_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
Aplikacja jest brzydko zaprojektowana, ale łatwo przy jej pomocy pokazać przykładowe wykorzystanie Realma. Pierwszy przycisk doda notatkę do bazy danych i odświeży listę notatek. Drugi usunie wszystkie notatki z bazy danych. Trzeci przefiltruje notatki, zostawiając na ekranie tylko te, które w treści zawierają frazę filtered
. Ostatni przycisk wyświetli na ekranie obiekty realmowe (szczegóły w dalszej części).
Aplikacja będzie wyświetlać listę notatek, a więc potrzebujemy jeszcze layoutu pojedynczej notatki note_layout.xml
.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/note_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Aby zaimportować Realma do projektu, należy dodać odpowiednie zależności w pliku app/build.gradle
, który u mnie wygląda następująco:
apply plugin: 'com.android.application'
apply plugin: 'realm-android'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:1.2.0"
}
}
android {
compileSdkVersion 24
buildToolsVersion "24.0.1"
defaultConfig {
applicationId "damianchodorek.com.kurs.android20.realmapp"
minSdkVersion 16
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.2.0'
}
Do automatycznie wygenerowanego kodu dodałem więc:
apply plugin: 'realm-android'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:1.2.0"
}
}
Czas na zorganizowanie kodu. Stwórz pakiet data
, a w nim pakiety db
, pojo
, realm
. Zacznijmy od naszego modelu danych. Składa się on z dwóch klas Note
oraz NoteRealm
. Pierwsza klasa, to zwykły javowy obiekt POJO, który reprezentuje notatkę. Drugi obiekt to jego realmowy odpowiednik. Wygląda dokładnie tak samo jak oryginał, z wyjątkiem tego, iż dziedziczy po klasie RealmObject
, aby mógł być zapisany do bazy danych Realm. Wszystkie obiekty, które chcemy zapisywać/odczytywać, muszą dziedziczyć po klasie RealmObject
oraz muszą posiadać publiczne gettery i settery. Poniżej definicja klas.
// klasa Note w pakiecie data/pojo
public class Note {
private Integer id;
private String noteText;
public String getNoteText() {
return noteText;
}
public void setNoteText(String noteText) {
this.noteText = noteText;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String toString() {
return noteText;
}
}
// klasa NoteRealm w pakiecie data/realm
public class NoteRealm extends RealmObject {
@PrimaryKey
private int id;
private String noteText;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNoteText() {
return noteText;
}
public void setNoteText(String noteText) {
this.noteText = noteText;
}
}
Klasa realmowa posiada również adnotację @PrimaryKey
, która oznacza, że dane pole klasy będzie kluczem głównym i Realm będzie traktował je wyjątkowo. Ta adnotacja nie jest wymagana, ale warto ją stosować. Wtedy Realm sam dba przykładowo o to, aby pole było unikalne. Próba wstawienia drugiego obiektu, o takim samym id, spowoduje wyjątek.
Nie ma wymogu, aby stosować kopiowanie obiektów realmowych na POJO i vice versa. Nic nie stoi na przeszkodzie, aby korzystać tylko z jednej klasy, np. NoteRealm
(co również pokażę w tym przykładzie). Sam jednak uważam, że warto wprowadzić model danych niezależny od bazy danych czy biblioteki tak, aby nie odczuć zmian, jeśli będziemy musieli z niej zrezygnować. Dodatkowo jak wspomniałem wcześniej – obiekty, które dziedziczą po RealmObject
nie mogą być przekazywane pomiędzy wątkami. Nie możemy np. przy użyciu AsyncTask
odczytać listy notatek w tle i wyświetlić na ekranie (czyli na wątku głównym), ponieważ zostanie wyrzucony wyjątek.
Stworzyłem jeszcze dodatkową klasę NoteMapper
, której zadaniem jest zmapowanie obiektu realmowego na POJO.
// klasa w pakiecie data/db/NoteMapper
public class NoteMapper {
Note fromRealm(NoteRealm noteRealm) {
Note note = new Note();
note.setId(noteRealm.getId());
note.setNoteText(noteRealm.getNoteText());
return note;
}
}
W poprzedniej lekcji opisałem wzorzec projektowy Data Access Object (DAO), który jest obiektem dostępu do danych. Różne części aplikacji (np. aktywności, fragmenty) powinny go stosować w celu odczytu/modyfikacji danych. Nie powinny natomiast korzystać bezpośrednio z Realma, ponieważ jeśli zmieni się API, albo Ty zrezygnujesz z tej technologii, to będziesz musiał dokonywać zmian w całej aplikacji. Jeśli zastosujesz wspomniany wzorzec, to zmian będziesz musiał dokonać tylko w plikach DAO.
W poprzedniej lekcji nasza klasa NotesDao
, korzystała z bazy danych SQLite
. Zmodyfikujemy ją tak, aby korzystała z Realma. Deklaracje metod publicznych pozostaną bez zmian, więc aktywność, która korzysta z tego obiektu, nie zauważy, że zmieniła się baza danych (i o to właśnie chodzi we wzorcu DAO). Oczywiście, na potrzeby lekcji, dodamy kilka metod. Poniżej kod klasy data/db/NoteDao
.
public class NoteDao {
private Realm realm;
private RealmConfiguration realmConfig;
public NoteDao(Context context) {
// inicjalizacja realma
realmConfig = new RealmConfiguration.Builder(context).build();
realm = Realm.getInstance(realmConfig);
}
// zamknięcie realma
public void close() {
realm.close();
}
// wstawienie nowej notatki do bazy danych
public void insertNote(final Note note) {
// operacje zapisu muszą odbywać się w transakcji
realm.beginTransaction();
// tworzymy nowy obiekt przy pomocy metody createObject()
NoteRealm noteRealm = realm.createObject(NoteRealm.class);
noteRealm.setId(generateId());
noteRealm.setNoteText(note.getNoteText());
// commitTransaction() zapisuje stan obiektów realmowych do bazy danych
// jeśli więc stworzyliśmy nowy lub usunęliśmy stary, to w tym momencie
// te operacje zostaną odwzorowane w bazie
realm.commitTransaction();
}
// pobranie notatki na podstawie jej id
public Note getNoteById(final int id) {
// aby pobrać obiekt danej klasy korzystamy z metody where()
// dodatkowe warunki zapytania definiujemy przy pomocy metod takich jak equalTo()
// findFirst() lub findAll() to metody, które wykonują zdefiniowane zapytanie
NoteRealm noteRealm = realm.where(NoteRealm.class).equalTo("id", id).findFirst();
return new NoteMapper().fromRealm(noteRealm);
}
// aktualizacja notatki w bazie
public void updateNote(final Note note) {
NoteRealm noteRealm = realm.where(NoteRealm.class).equalTo("id", note.getId()).findFirst();
realm.beginTransaction();
noteRealm.setNoteText(note.getNoteText());
realm.commitTransaction();
}
// usunięcie notatki z bazy
public void deleteNoteById(final long id) {
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.where(NoteRealm.class).equalTo("id", id).findFirst().deleteFromRealm();
}
});
}
// pobranie wszystkich notatek
public List<Note> getAllNotes() {
List<Note> notes = new ArrayList<>();
NoteMapper mapper = new NoteMapper();
RealmResults<NoteRealm> all = realm.where(NoteRealm.class).findAll();
for (NoteRealm noteRealm : all) {
notes.add(mapper.fromRealm(noteRealm));
}
return notes;
}
// pobranie wszystkich notatek, które zawierają w treści dany tekst
public List<Note> getNotesLike(String text) {
List<Note> notes = new ArrayList<>();
NoteMapper mapper = new NoteMapper();
RealmResults<NoteRealm> all = realm.where(NoteRealm.class)
.contains("noteText", text)
.findAll();
for (NoteRealm noteRealm : all) {
notes.add(mapper.fromRealm(noteRealm));
}
return notes;
}
public List<NoteRealm> getRawNotes(){
return realm.where(NoteRealm.class).findAll();
}
// usunięcie wszystkich notatek
public void deleteAllNotes() {
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.where(NoteRealm.class)
.findAll()
.deleteAllFromRealm();
}
});
}
// wygenerowanie nowego id dla notatki
private int generateId() {
return realm.where(NoteRealm.class).max("id").intValue() + 1;
}
}
Oto kilka wniosków, które powinieneś wyciągnąć po spojrzeniu na powyższy kod.
- Aby zacząć korzystać z Realma, należy uzyskać instancję bazy danych przy użyciu metody
Realm.getInstance()
. - Instancję Realma musimy w końcu zamknąć, korzystając z metody
realm.close()
. Możemy to zrobić zaraz po wykonaniu operacji lub np. podczas niszczenia aktywności. - Modyfikacje bazy danych (tworzenie, modyfikacja i usuwanie obiektów) musi odbyć się w transakcji. Transakcję możemy wykonać na dwa sposoby. Pierwszy jest przy użyciu metody
public void executeTransaction(Transaction transaction)
, do której przekażemy obiekt zawierający instrukcje, które chcemy wykonać na bazie (np. w metodziedeleteAllNotes()
). Drugi sposób na transakcję, to rozpoczęcie jej metodąrealm.beginTransaction()
i zakończenierealm.commitTransaction()
. Jeśli chcemy, to Realm wspiera również transakcje asynchroniczne przy pomocy metodyexecuteTransactionAsync()
. Warto z niej korzystać jeśli mamy dużo operacji do wykonania i nie chcemy blokować głównego wątku. - Nowe obiekty w Realmie tworzymy przy pomocy metody
realm.createObject()
. Możemy również stworzyć obiekt korzystając z operatoranew
; wtedy będziemy musieli skopiować go do Realma, korzystając z metodycopyToRealm()
. - Aby pobrać obiekty danej klasy korzystamy z metody
realm.where()
np.realm.where(NoteRealm.class)
, która zwraca obiekt typuRealmQuery
. Do zapytania możemy dołożyć kolejne warunki, korzystając z metod np.equalTo()
,contains()
. Kiedy nasze zapytanie będzie gotowe możemy je wykonać metodąfindFirst()
lubfindAll()
, które zwrócą wyniki zapytania. JeślifindFirst()
nie znajdzie żadnych obiektów, które będą pasować do zapytania, to dostaniemynull
. - Aby definiować warunki, które muszą spełniać pola klasy podczas wyszukiwania, przekazujemy nazwę danego pola jako wartość tekstową, np.
realm.where(NoteRealm.class).equalTo("id", 25)
oznacza, że szukamy obiektu klasyNoteRealm
, którego poleid
posiada wartość25
. - Id każdego nowego obiektu musimy wygenerować sami. Id nie jest obowiązkowe, nie musimy z niego korzystać, choć w zdecydowanej większości przypadków będzie nam ono potrzebne.
Sporo już wyjaśniłem. Czas zastosować zdobytą wiedzę oraz obiekt NoteDao
. Poniżej znajduje się kod głównej aktywności, który wyświetla i modyfikuje notatki.
public class MainActivity extends AppCompatActivity {
private EditText newNoteText;
private ListView noteList;
private NoteDao noteDao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// inicjalizujemy pola klasy
newNoteText = (EditText) findViewById(R.id.new_note_text);
noteList = (ListView) findViewById(R.id.note_list);
// tworzymy obiekt DAO
noteDao = new NoteDao(this);
// wyświetlamy listę notatek
reloadNotesList();
}
@Override
protected void onDestroy() {
// zamykamy instancję Realma
noteDao.close();
super.onDestroy();
}
// dodaje nową notatkę do bazy danych i odświeża listę
public void addNewNote(View view) {
Note note = new Note();
String text = newNoteText.getText().toString();
if (text.length() > 0) {
note.setNoteText(text);
}
// wywołanie metody insertNote() jest takie jak w poprzedniej lekcji,
// gdzie korzystaliśmy z SQLite
noteDao.insertNote(note);
reloadNotesList();
}
// usuwa notatkę z bazy i odświeża listę
public void removeNote(Note note) {
noteDao.deleteNoteById(note.getId());
reloadNotesList();
}
// pokazuje listę notatek
private void reloadNotesList() {
// pobieramy z bazy danych listę notatek
List<Note> allNotes = noteDao.getAllNotes();
// ustawiamy adapter listy
noteList.setAdapter(new ArrayAdapter<Note>(this, R.layout.note_layout, allNotes) {
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final View noteView = super.getView(position, convertView, parent);
// po kliknięciu na notatkę zostanie ona usunięta
noteView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
removeNote(getItem(position));
}
});
return noteView;
}
});
}
// usuwa wszystkie notatki z bazy
public void deleteAll(View view) {
noteDao.deleteAllNotes();
reloadNotesList();
}
// pobiera wyłącznie notatki, które zawierają frazę "filtered"
// i wyświetla je na ekranie
public void filterNotes(View view) {
// pobieramy z bazy danych listę notatek z frazą "filtered"
List<Note> filteredNotes = noteDao.getNotesLike("filtered");
// ustawiamy adapter listy
noteList.setAdapter(new ArrayAdapter<Note>(this, R.layout.note_layout, filteredNotes) {
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
return super.getView(position, convertView, parent);
}
});
}
// pokazuje listę obiektów typu NoteRealm
public void showRealmNotes(View view) {
noteList.setAdapter(new ArrayAdapter<NoteRealm>(this, R.layout.note_layout, noteDao.getRawNotes()) {
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
return super.getView(position, convertView, parent);
}
});
}
}
Zwróć uwagę na ostatnią metodę. Wyświetla ona na liście obiekty, które pochodzą bezpośrednio z bazy danych Realma. Bez mapowania. Pobieramy je metodą noteDao.getRawNotes()
i przekazujemy do adaptera. Adapter wykonuje na nich metodę toString()
, która pochodzi z klasy RealmObject
(po której dziedziczy NoteRealm
). Operacja przebiega poprawnie. Jak wspomniałem wcześniej – nic nie stoi na przeszkodzie, aby korzystać bezpośrednio z obiektów realmowych, bez mapowania. Sam nie jestem jednak tego zwolennikiem, zwłaszcza przy większych projektach.
Na koniec dodam jeszcze, że Realm wspiera relacje między obiektami. Możemy do naszej klasy NoteRealm
wstawić inny obiekt, który dziedziczy po RealmObject
lub nawet listę obiektów. Wszystkie te zależności zostaną zapisane w bazie danych. Należy tylko pamiętać o usunięciu tych obiektów podczas usuwania obiektu nadrzędnego.
Oficjalną stronę Realma na Androida, wraz z tutorialami znajdziesz pod adresem https://realm.io/docs/java/latest/. Zdecydowanie warto poznać tę technologię, zwłaszcza, że jest również dostępna na inne platformy. Myślę, że dla większości jest ona lepszym i łatwiejszym rozwiązaniem niż standardowa baza SQLite
.
W przyszłości planuję napisać jeszcze o Firebase – bazie danych od Google, która automatycznie synchronizuje się z chmurą oraz z innymi urządzeniami danego użytkownika. Jest to rozwiązanie preferowane dla osób, które nie chcą zajmować się tworzeniem serwera dla swojej aplikacji, a chcą zapewnić synchronizację danych. Więcej na ten temat przeczytasz pod adresem https://firebase.google.com/docs/database/.