Kurs Android (19)

Autor: Damian Chodorek • Opublikowany: 20 maja 2016 • Ostatnia aktualizacja: 11 września 2016 • Kategoria: android, kursy

Przechowywanie danych w SharedPreferences oraz w bazie danych SQLite.

Prawie każda aplikacja potrzebuje sposobu na przechowywanie danych. Dane mogą być różne i dotyczyć wielu rzeczy. Przykładowo aplikacja może posiadać zestaw ustawień związanych z jej wyglądem. W takim wypadku, gdy użytkownik dokona zmiany ustawień, będzie oczekiwał, że po ponownym wejściu w aplikację, zostaną one przywrócone.

Preferencje użytkownika to przykład danych złożonych z prostych wartości w postaci klucz-wartość. Innym rodzajem danych będzie np. lista kontaktów użytkownika, w której każdy kontakt będzie składał się z wielu pól jak np. adres e-mail, numer telefonu, imię, nazwisko. Po pierwsze dane składają się z wielu pól, a więc są złożone. Po drugie spodziewamy się, że będzie ich sporo (wiele kontaktów). Po trzecie może będziemy chcieli mieć łatwą możliwość przeszukiwania danych, np. użytkownik zechce znaleźć wszystkie kontakty o imieniu Jan.

Mamy więc dwa podstawowe rodzaje danych:

  • proste preferencje,
  • złożone struktury.

Obsługa preferencji

W systemie Android jednym ze sposobów zapisywania danych są SharedPreferences. Jest to klasa umożliwiająca zapisywanie typów prostych w postaci klucz-wartość. Zerknijmy poniżej na przykład użycia.

// ...
// tworzymy obiekt preferencji
SharedPreferences prefs = getSharedPreferences("myPreferencesFileName", MODE_PRIVATE);

// jeśli chcemy wstawić/usunąć wartość do/z preferencji musimy
// uzyskać dostęp do obiektu edytującego
SharedPreferences.Editor prefsEdit = prefs.edit();
// wstawiamy klucz : wartość -> "backgroundColor" : "red"
prefsEdit.putString("backgroundColor", "red");
// zapisujemy obecny stan preferencji w pamięci trwałej
prefsEdit.apply();

// ...

// sprawdzamy czy w tych preferencjach istnieje dany klucz
if (prefs.contains("backgroundColor")) {
    // pobieramy z preferencji wartość dla klucza "backgroundColor"
    // drugi parametr to domyślna wartość na wypadek gdyby jednak klucz nie istniał
    Log.d("tmp", prefs.getString("backgroundColor", "default value"));
}
// ...

Metoda getSharedPreferences() należy do klasy Context, możemy więc wywołać ją np. wewnątrz aktywności. Pierwszym jej parametrem jest nazwa preferencji, będąca jednocześnie nazwą pliku, do którego zapisane zostaną dane. Drugi parametr dotyczy widoczności pliku z preferencjami przez inne aplikacje. MODE_PRIVATE oznacza, że plik będzie dostępny wyłącznie dla naszej aplikacji.

Jak widać pobranie instancji preferencji jest banalnie proste. Podajemy tylko nazwę, a system za nas stworzy nowy plik (jeśli nie istnieje) i umożliwi na łatwy zapis/odczyt danych.

Metoda edit() zwraca obiekt typu Editor. Jeżeli chcemy w jakikolwiek sposób zmodyfikować dane w preferencjach (dodać lub usunąć), to pośrednikiem będzie właśnie ten obiekt. W powyższym przykładnie wstawiamy wartość tekstową prefsEdit.putString("backgroundColor", "red"). Dla każdego typu prostego istnieje odpowiednik tej metody, np. putBoolean(), putInt(). Aby zmodyfikowane dane zapisać na stałe, wykonujemy metodę prefsEdit.apply().

Aby sprawdzić czy mamy określone dane w preferencjach, korzystamy z metody contains(). Metoda zwraca true jeśli dany klucz jest w preferencjach oraz false jeśli go nie ma. Kolejna metoda getString() zwraca zapisaną wcześniej wartość. Pierwszy parametr to oczywiście klucz, a drugi to domyślna wartość w przypadku, gdyby w preferencjach nie było klucza. Dla każdego typu danych istnieje analogiczna metoda do ich odczytu, np. getInt(), getLong().

Preferencje zapisywane są do pliku, a więc nie giną w momencie, gdy aplikacja zostanie zamknięta. Jest to łatwy sposób na przechowywanie pojedynczych wartości, świetnie nadający się do zapisu jakichś ustawień użytkownika. Jest to operacja tak częsta że Android wspiera programistów specjalnymi klasami jak PreferenceFragment oraz PreferenceActivity, które dokonują automatycznego zapisywania danych bez konieczności odwoływania się ciągle do SharedPreferences.

Kiedy jednak potrzebujemy zapisać kilkaset kontaktów, które złożone są z wielu pól, będziemy potrzebować czegoś innego.

Baza danych w systemie Android

Android posiada natywne wsparcie dla bazy danych SQLite. Jest to relacyjna baza danych zorganizowana w postaci jednego pliku. Baza wspiera transakcje i można na niej wykonywać złożone zapytania SQL. Stworzymy prostą aplikację, w ramach której poznasz podstawową obsługę bazy danych SQLite. Stwórz więc nową aplikację z jedną aktywnością. Aplikacja będzie służyć do gromadzenia notatek użytkownika. Składać się będzie z pola tekstowego, przycisku i listy notatek. Poniżej layout aktywności.

<?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="android19.kurs.damianchodorek.om.dbapp.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" />

    <ListView
        android:id="@+id/note_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

Jak widzisz element Button posiada atrybut onClick, którego wartość to nazwa metody, która będzie uruchomiona po kliknięciu w przycisku.

Aktywność będzie naszą główną klasą odpowiadającą za działanie aplikacji. Poza nią stworzymy jeszcze kilka klas związanych z zarządzaniem bazą danych:

  • Note - obiekt POJO który reprezentuje pojedynczą notatkę,
  • Notes - interfejs posiadający wyłącznie stałe związane z tabelą w bazie danych,
  • DBHelper - klasa wspomagająca zarządzanie bazą danych,
  • NoteDAO - czyli Note Data Access Object, obiekt z którym komunikuje się bezpośrednio aktywność, jest pośrednikiem pomiędzy klientem a bazą danych; korzysta z DBHelper.

Spójrz jeszcze w jakich pakietach znajdują się powyższe klasy.

Zacznijmy od rzeczy podstawowych, a nie ma nic prostszego niż POJO. To obiekt, który reprezentuje jedną notatkę. Poniżej kod.

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;
    }
}

Zarządzanie bazą danych

API Androida udostępnia klasę do obsługi bazy danych SQLiteOpenHelper. Służy ona to tworzenia bazy oraz wykonywania na niej zapytań SQL. Typowo powinniśmy ją rozszerzyć i zaimplementować dwie podstawowe metody. Kod poniżej.

class DBHelper extends SQLiteOpenHelper {
    private final static int DB_VERSION = 1;
    private final static String DB_NAME = "AppDB.db";

    public DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(
                "create table "
                        + Notes.TABLE_NAME
                        + " ( "
                        + Notes.Columns.NOTE_ID
                        + " integer primary key, "
                        + Notes.Columns.NOTE_TEXT
                        + " text )"
        );
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // kasujemy bazę
        db.execSQL("DROP TABLE IF EXISTS " + Notes.TABLE_NAME);
        // tworzymy nową bazę
        onCreate(db);
    }
}

Wywołując konstruktor musimy podać nazwę pliku z bazą danych, jej wersję i kontekst. W momencie gdy zmieni się wersja, zostanie odpalona metoda onUpdate(), która typowo powinna skasować obecną bazę i stworzyć ją na nowo. Wersję bazy danych zmieniamy, bo przykładowo zmieniliśmy strukturę kolumn.

W metodzie onCreate() tworzona jest nowa baza danych (tylko jeśli nie istnieje). Jak widzisz tworzenie bazy danych sprowadza się do stworzenia konkretnego polecenia w języku SQL. Z pomocą przychodzi nam interfejs Notes, w którym przechowujemy wszystkie informacje na temat tabeli. Nie chcemy wpisywać nazw, ale korzystać ze stałych. Kod interfejsu Notes poniżej.

public interface Notes {
    String TABLE_NAME = "notes";

    interface Columns {
        String NOTE_ID = "_id";
        String NOTE_TEXT = "note_text";
    }
}

Jak widzisz interfejs zawiera nazwę tabeli oraz jej kolumn. Kolumnę z identyfikatorami nazwałem _id. W niczym to nie przeszkadza, a umożliwi korzystanie z klasy CursorAdapter jeżeli zajdzie taka potrzeba (wyjaśnienie tutaj).

Obiekt DAO

DAO (Data Access Object) to wzorzec projektowy, w którym chodzi o to, że mamy jeden obiekt przy pomocy, którego komunikujemy się z bazą danych. Owszem, moglibyśmy do tego wykorzystywać stworzoną klasę DBHelper, ale wtedy klient musiałby sam troszczyć się o konstruowanie konkretnych zapytań. Stworzymy więc klasę NoteDAO, która będzie to robić zamiast klienta. Tak naprawdę klasa udostępni kilka metod, dzięki którym pobierzemy, wstawimy, usuniemy i zmodyfikujemy dane. Klient (np. aktywność) nie będzie więc w żaden sposób martwił się o SQLa. Tak naprawdę to nie interesuje go czy dane pobierane będą z bazy danych, pliku XML czy z internetu. O szczegóły martwi się DAO. Klient otrzymuje jedynie elegancko przygotowane dane.

Poniżej przedstawiam implementację NoteDAO z kilkoma podstawowymi operacjami.

public class NoteDAO {

    // obiekt umożliwiający dostęp do bazy danych
    private DBHelper dbHelper;

    public NoteDAO(Context context) {
        dbHelper = new DBHelper(context);
    }

    // wstawienie nowej notatki do bazy danych
    public void insertNote(final Note note) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(Notes.Columns.NOTE_TEXT, note.getNoteText());

        dbHelper.getWritableDatabase().insert(Notes.TABLE_NAME, null, contentValues);
    }

    // pobranie notatki na podstawie jej id
    public Note getNoteById(final int id) {
        Cursor cursor = dbHelper.getReadableDatabase().rawQuery("select * from " + Notes.TABLE_NAME + " where " + Notes.Columns.NOTE_ID + " = " + id, null);
        if (cursor.getCount() == 1) {
            cursor.moveToFirst();
            return mapCursorToNote(cursor);
        }
        return null;
    }

    // aktualizacja notatki w bazie
    public void updateNote(final Note note) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(Notes.Columns.NOTE_TEXT, note.getNoteText());

        dbHelper.getWritableDatabase().update(Notes.TABLE_NAME,
                contentValues,
                " " + Notes.Columns.NOTE_ID + " = ? ",
                new String[]{note.getId().toString()}
        );
    }

    // usunięcie notatki z bazy
    public void deleteNoteById(final Integer id) {
        dbHelper.getWritableDatabase().delete(Notes.TABLE_NAME,
                " " + Notes.Columns.NOTE_ID + " = ? ",
                new String[]{id.toString()}
        );
    }

    // pobranie wszystkich notatek
    public List getAllNotes() {
        Cursor cursor = dbHelper.getReadableDatabase().query(Notes.TABLE_NAME,
                new String[]{Notes.Columns.NOTE_ID, Notes.Columns.NOTE_TEXT},
                null, null, null, null, null
        );

        List results = new ArrayList<>();

        if (cursor.getCount() > 0) {
            while (cursor.moveToNext()) {
                results.add(mapCursorToNote(cursor));
            }
        }

        return results;
    }

    // zamiana cursora na obiekt notatki
    private Note mapCursorToNote(final Cursor cursor) {
        int idColumnId = cursor.getColumnIndex(Notes.Columns.NOTE_ID);
        int textColumnId = cursor.getColumnIndex(Notes.Columns.NOTE_TEXT);

        Note note = new Note();
        note.setId(cursor.getInt(idColumnId));
        note.setNoteText(cursor.getString(textColumnId));
        return note;
    }
}

Obiekt posiada podstawowe metody do zarządzania bazą danych, a konkretnie pojedynczą tabelą. NoteDAO porozumiewa się ze światem zewnętrznym (np. aktywnością) przy pomocy obiektu Note. Służy on zarówno do zwracania wyników z bazy danych jak i do jej aktualizacji. NoteDAO wykorzystuje do wykonywania zapytań na bazie danych obiekt DBHelper.

Zapis danych do bazy

Jeżeli modyfikujemy bazę danych wykorzystujemy metodę dbHelper.getWritableDatabase(), jeżeli wyłącznie odczytujemy dane, korzystamy z dbHelper.getReadableDatabase(). Do zapisu i aktualizacji bazy danych wykorzystujemy obiekt pośredni ContentValues, który reprezentuje jeden wiersz w bazie danych. Wstawiamy w niego dane w postaci klucz-wartość, gdzie kluczem jest nazwa kolumny w tabeli. Przykładem jest metoda insertNote().

Odczyt danych z bazy

Wyniki z bazy są zawsze zwracane poprzez obiekt pośredni klasy Cursor. Kursor zawiera wiersze zwrócone z zapytania i umożliwia poruszanie się po nich. Na początku warto sprawdzić ile wyników zwróciła baza metodą cursor.getCount(). Następnie przesuwamy kursor do pierwszego wiersza metodą cursor.moveToNext(). Aby dobrać się do danych musimy najpierw pobrać identyfikatory każdej kolumny przy pomocy metody cursor.getColumnIndex(). Następnie przy jego pomocy pobieramy konkretną wartość z danej kolumny i wiersza, na który aktualnie pokazuje kursor, metodą cursor.getInt(columnId). Dla innych typów istnieją analogiczne metody, np. cursor.getString().

Spójrz dokładniej na metodę mapCursorToNote(). Jej zadaniem jest zmapowanie aktualnego wiersza, na który pokazuje kursor, na obiekt typu Note.

Wykonywanie zapytań

Na bazie danych zapytania wykonujemy przy pomocy metod query() oraz rawQuery(). Tak wygląda deklaracja pierwszej z metod: public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit). Jak widzisz, każdy parametr zapytania SQL zrealizowany jest w postaci osobnego parametru metody. Takie podejście zwiększa odporność na błędy.

Główna aktywność

Aktynwność dodaje notatkę na podstawie uzupełnionego pola tekstowego oraz usuwa ją, gdy na nią klikniemy.

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();
    }

    // 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);
        }

        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;
            }
        });
    }
}

Na koniec jeszcze kod layoutu pojedynczej notatki.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/note_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

Jak dodać kolejną kolumnę?

Aby dodać kolejną kolumnę do bazy danych musimy wykonać poniższe kroki.

  1. Tworzymy interfejs opisujący tabelę, analogicznie do interfejsu Notes.
  2. Tworzymy nowy obiekt DAO, analogicznie do NoteDAO.
  3. Zwiększamy wartość stałej DBHelper.DB_VERSION o jeden.
  4. Modyfikujemy polecenie SQL w metodzie DBHelper.onCreate() w taki sposób, aby tworzyło nową kolumnę.

Korzystanie z SQLite nie należy do najwygodniejszych, zwłaszcza gdy mamy rozbudowane relacje. Zauważ, że dla każdej kolumny będziemy mapować ją na obiekt POJO. Jest to operacja dosyć powtarzalna. W następnej lekcji poznasz mechanizm, który będzie to robił automatycznie.

część 20

4 komentarze

  • Kuba napisał(a):

    Przypadkowo znalazłem twój blog i lekcje które tworzysz (nie tylko z androida) bardzo mi pomogły. Oczywiście blog wylądował w zakładach. Dzięki!

  • Przemek napisał(a):

    Cześć, Twoje lekcje bardzo mi pomagają, jestem na początku drogi programisty (choć mam swoje lata).
    Nie zawsze wiem gdzie dana klasa powinna się znaleźć, do jakiego katalogu powinna być podpięta. Może taki schemat katalogów i klas mógłbyś do lekcji podpinać. Dzięki

    Dzięki

    • Damian Chodorek napisał(a):

      Hej. Poruszyłeś ciekawy temat. Pakiety w zasadzie powinny być zorganizowane tak żebyś się mógł w nich łatwo odnaleźć. Nie ma złotej zasady. Ogólnie mamy dwa podejścia:
      – pakiety grupują klasy o tym samym przeznaczeniu, np. W pakiecie ui gromadzimy same aktywności, a w pakiecie adapter adaptery do list itd.
      – pakiety grupują klasy dotyczące tej samej funkcjonalności biznesowej, np. pakiet login zawiera zarówno aktywność logowania jak i adapter z której ona korzysta.

      Pakiety powinny być tak samo wygodne jak np. Foldery na twoim komputerze. Nie może być ich za dużo na jednym poziomie i muszą mieć deskryptywne nazwy.

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.