sqlite w systemie android. własny dostawca treści. · 1 sqlite w systemie android. własny...
TRANSCRIPT
1
SQLite w systemie Android. Własny dostawca treści.
Materiał teoretyczny
2
Spis treści I. Wprowadzenie ........................................................................................................................ 3
II. Tworzenie bazy danych .......................................................................................................... 5
III. Tworzenie dostawcy treści ................................................................................................. 6
IV. Tworzenie aktywności wykorzystujących bazę danych i zaimplementowanego
dostawcę treści ............................................................................................................................. 12
V. Bibliografia ............................................................................................................................ 22
3
I. Wprowadzenie
Aplikacje (nie tylko) mobilne często korzystają ze źródeł danych, które są ulokowane albo
lokalnie w systemie, albo zdalnie na serwerach zewnętrznych. Udostępnianiem danych zawartych w
wymienionych źródłach zajmują się dostawcy treści. Dostawcy treści są wykorzystywani do
udostępniania danych na zewnątrz aplikacji lub do wymiany (współdzielenia) danych pomiędzy
aplikacjami. Najbardziej pospolitym przykładem źródła danych umieszczonego w dostawcy treści jest
baza danych SQLite („opakowana” przez dostawcę treści).
Android (podobnie jak iPhone OS czy Symbian) używa wbudowanej wersji sqlite3. Ta
uproszczona wersja bazy oferuje częściową obsługę wyzwalaczy (triggers) i pozwala na generowanie
większości złożonych zapytań (za wyjątkiem stosowania outer join; pozwala używać języka SQL w
standardzie SQL92). Wyzwalacze to procedury wykonywane w odpowiedzi na zdarzenia takie jak np.
dodanie czy usunięcie rekordu.
Silnik bazodanowy SQLite po kompilacji zajmuje nie więcej niż 275 KB. Jest stosunkowo szybki
(w porównaniu do popularnych baz danych opartych na modelu klient-serwer) oraz dostępny na
wiele platform programowych. SQLite obsługuje bazy danych o wielkości rzędu terabajtów a
kompletna baza przechowywana jest w pojedynczym pliku.
Istnieją jednak pewne „niedogodności” związane z niepełnym wsparciem standardu SQL-92,
które zostały przedstawione poniżej w postaci listy:
Brak zaimplementowanego pola FOREIGN KEY (klucz obcy), czyli pola, którego
wartość odpowiada kluczowi głównemu (PRIMARY KEY) w innej tabeli dla systemu
Android poniżej wersji 2.2;
Brak zaimplementowanych niektórych właściwości triggerów (wyzwalaczy) - w SQLite
pominięte zostały takie właściwości triggerów jak: FOR EACH STATEMENT (wszystkie
wyzwalacze muszą być FOR EACH ROW) i INSTEAD OF na tabelach (INSTEAD OF
możliwy tylko na widokach) ;
Brak niektórych wariantów polecenia ALTER TABLE - czyli polecenia zmieniającego
właściwości istniejącej tabeli. W SQLite wspierane są tylko dwa warianty tego
polecenia mianowicie zawierające atrybuty RENAME TABLE (zmiana nazwy tabeli) i
ADD COLUMN (dodanie kolumny). Pozostałe rodzaje operacji ALTER TABLE, takie jak
DROP COLUMN (usunięcie kolumny), czy ADD CONSTRAINT(dodanie ograniczenia)
zostały pominięte;
Brak obsługi transakcji zagnieżdżonych - w SQLite obecnie możliwe są tylko
pojedyncze transakcje;
Brak operacji łączenia prawostronnego (RIGHT OUTER JOIN) i pełnego (FULL OUTER
JOIN) - w SQLite obecnie można używać łączenia lewostronnego LEFT OUTER JOIN;
4
Ograniczenia w operacjach na widokach - widoki (VIEWS) czyli wirtualne tabele w
SQLite są tylko do odczytu (nie można wykonywać na nich DELETE, UPDATE i INSERT);
Brak poleceń GRANT i REVOKE - komendy te służą do nadawania i odbierania
uprawnień użytkownikom. SQLite zapisuje i odczytuje dane bezpośrednio z pliku,
więc prawa dostępu nadawane są dla pliku z poziomu OS.
Baza danych SQLite udostępnia kilka typów podstawowych wymienionych poniżej:
INTEGER (1 do 8 bajtów) INT, INTEGER, TINYINT, SMALLINT, MEDIUMINT, BIGINT,
UNSIGNED BIG INT, INT2, INT8;
TEXT – typ tekstowy - VARCHAR(255), CLOB;
NONE – typ nieokreślony BLOB;
REAL – typ zmienno-przecinkowy – REAL, DOUBLE, DOUBLE PRECISION, FLOAT;
NUMERIC – typ stałoprzecinkowy – NUMERIC, DECIMAL(10,5), BOOLEAN, DATE,
DATETIME.
Bazy danych projektu aplikacji Android zapisywane są w katalogu:
/DATA/data/NAZWA_APLIKACJI/databases/NAZWA_BAZY
DATA jest ścieżką aplikacji, zwracaną po wywołaniu metody Environment.getDataDirectory().
NAZWA_APLIKACJI określa podaną w projekcie nazwę aplikacji. NAZWA_BAZY to nazwa pliku z
rozszerzeniem „.db”, w którym znajduje się baza danych aplikacji.
Pakiet android.database zawiera wszystkie klasy potrzebne do pracy z bazą danych,
natomiast pakiet android.database.sqlite zawiera klasy specyficzne dla SQLite.
Aby utworzyć lub zmodernizować bazą danych we własnej aplikacji Android, należy utworzyć
podklasę klasy SQLiteOpenHelper. W konstruktorze utworzonej podklasy należy wywołać metodę
super() dla klasy SQLiteOpenHelper, podając nazwę bazy danych i bieżącą jej wersję.
W podklasie rozszerzającej SQLiteOpenHelper należy przesłonić następujące metody, tak aby
istniała możliwość utworzenia i modernizacji bazy danych:
onCreate() – wywoływana przez framework, w przypadku gdy do bazy danych żądany
jest dostęp a sama baza nie została jeszcze utworzona;
onUpgrade() – wywoływana, jeżeli wersja bazy została inkrementowana w kodzie
aplikacji. Metoda ta pozwala na aktualizację bazy lub jej usunięcie a następnie
przywrócenie poprzez metodę onCreate().
Wymienione metody jako argument pobierają obiekt klasy SQLiteDatabase, będący
reprezentacją bazy danych w Javie. Klasa SQLiteOpenHelper dostarcza metody
5
getReadableDatabase() i getWritableDatabase() służące do dostępu do obiektu klasy SQLiteDatabase
odpowiednio w trybie odczytu i zapisu.
Tabele bazy danych powinny używać identyfikatora „_id” jako klucza głównego (kilka funkcji
Android bazuje na tym standardzie). Dodatkowo dobrą praktyką jest tworzenie oddzielnej klasy dla
każdej z tabeli bazy danych. Klasa ta powinna mieć statyczne definicje metod onCreate() i
onUpgrade(), które są wywoływane jako odpowiednie metody podklasy SQLiteOpenHelper. W ten
sposób implementacja podklasy SQLiteOpenHelper pozostaje czytelna, nawet w przypadku kilku
tabel zawartych w bazie danych. W kolejnej części omówiono przykładową procedurę tworzenia bazy
danych oraz implementacji podklasy SQLiteOpenHelper.
II. Tworzenie bazy danych
Pierwszym krokiem w omawianej procedurze tworzenia i obsługi bazy danych jest
implementacja klasy odzwierciedlającej tabelę w bazie danych. W prezentowanym przykładzie
utworzona została aplikacja zarządzająca listą rzeczy do zrobienia, na podstawie [6].
Aplikacja składa się z dwóch aktywności: jednej widocznej jako lista wszystkich rzeczy do
zrobienia, druga – widoczna jako ekran edycji lub tworzenia nowego elementu listy. Obie aktywności
komunikują się za pomocą intencji. Do asynchronicznej pracy z bazą danych wykorzystano idee
kursora zawartości (klasa Cursor) oraz klasę Loader – służącą do asynchronicznej pracy z bazą danych.
Poniżej zaprezentowano implementację klasy TabelaNotatki, odzwierciedlającej tabelę w
bazie danych aplikacji.
public class TabelaNotatki { public static final String TABLE_NOTATKI = "notatki"; public static final String COLUMN_ID = "_id"; public static final String COLUMN_CATEGORY = "kategoria"; public static final String COLUMN_SUMMARY = "streszczenie"; public static final String COLUMN_DESCRIPTION = "opis"; // wyrażenie opisujące tworzenie bazy danych private static final String DATABASE_CREATE = "create table " + TABLE_NOTATKI + "(" + COLUMN_ID + " integer primary key autoincrement, " + COLUMN_CATEGORY + " text not null, " + COLUMN_SUMMARY + " text not null," + COLUMN_DESCRIPTION + " text not null" + ");"; public static void onCreate(SQLiteDatabase database) { database.execSQL(DATABASE_CREATE); } public static void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { Log.w(TabelaNotatki.class.getName(), "Aktualizuję bazę z wersji " + oldVersion + " do wersji " + newVersion + ", co spowoduje usunięcie wszystkich danych.");
6
database.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTATKI); onCreate(database); }
}
W klasie TabelaNotatki zdefiniowano publiczne statyczne pola opisujące tabelę „notatki”.
Następnie utworzona została klasa NotatkiDatabaseHelper będąca podklasą SQLiteOpenHelper. Klasa
NotatkiDatabaseHelper zawiera wywołania statycznych metod klasy TabelaNotatki. Implementacja
tej podklasy została przedstawiona poniżej.
public class NotatkiDatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "todotable.db"; private static final int DATABASE_VERSION = 1; public NotatkiDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } // metoda wywoływana podczas tworzenia bazy @Override public void onCreate(SQLiteDatabase database) { TabelaNotatki.onCreate(database); } //metoda wywoływana podczas modernizacji bazy @Override public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { TabelaNotatki.onUpgrade(database, oldVersion, newVersion); }
}
Jak widać na powyższym kodzie klasy, plik bazy danych nosi nazwę „todotable.db”.
Konstruktor klasy wywołuje w swoim ciele konstruktor nadklasy („super”), który pobiera jako
parametry: kontekst, nazwę pliku bazy danych, referencję do obiektu fabryki kursorów treści
(domyślna wartość: null) oraz numer wersji bazy danych. W ciałach metod onCreate oraz onUpgrade
znajdują się wywołania odpowiednich statycznych metod klasy TabelaNotatki, związanych z
tworzeniem oraz modernizacją bazy danych.
W kolejnych krokach dokonano implementacji dostawy treści oraz aktywności
wykorzystujących utworzoną bazę i dostawcę treści.
III. Tworzenie dostawcy treści
Baza danych SQLite jest przeznaczona do użytku prywatnego dla aplikacji, która ją stworzyła.
Aby udostępnić dane zawarte w tej bazie innym aplikacjom, można wykorzystać dostawcę treści.
Chociaż dostawca treści może być użyty wewnątrz aplikacji do dostępu do danych, to jego głównym
7
celem jest współdzielenie danych z innymi aplikacjami. Dostawca treści przed wykorzystaniem musi
zostać zadeklarowany w deskryptorze aplikacji.
Dostęp do dostawcy treści odbywa się za pośrednictwem identyfikatora URI, który
jednoznacznie określa dostawcę. Struktura tego identyfikatora przypomina identyfikator URI
protokołu HTTP. Ogólna struktura identyfikatora URI przedstawia się następująco:
content://authority-name/path-segment/…
content – jest elementem określającym dostawcę treści,
authority-name – jest niepowtarzalnym identyfikatorem upoważnienia używanym
do zlokalizowania dostawcy w rejestrze dostawców,
path-segment – to człon określający ścieżkę dostępu do danych (inną dla każdego
dostawcy); człon ten może być powtarzany wielokrotnie.
Przykład poniżej przedstawia wywołanie listy kontaktów z podaniem identyfikatora jednego
kontaktu:
content://com.android.contacts/contacts/lookup/0n293F33292B314F2929292929/2
Dla dostawców wbudowanych (com.android) nie trzeba używać całego identyfkator,
wystarczy wskazać odpowiednie słowo: content://contacts/contacts/1.
W dalszej części omówiono przykład implementacji własnego dostawcy treści, który
wykorzystuje zdefiniowaną wcześniej bazę danych.
Aby utworzyć własnego dostawcę treści należy utworzyć klasę, która rozszerza klasę
android.content.ContentProvider. Dodatkowo w pliku AndroidManifest.xml aplikacji należy
zadeklarować utworzonego dostawcę treści, tak by był dostępny w rejestrze dostawców. Wpis w
pliku deskryptora aplikacji, rejestrujący dostawcę, określa nazwę dostawcy (parametr android:name)
oraz identyfikator (parametr android:authorities).
Tworzony dostawca treści musi również implementować kilka metod, np. query(), insert(),
update(), delete(), getType() oraz onCreate(). W przypadku braku obsługi określonych metod, dobrą
praktyką jest wywołanie wyjątku klasy UnsupportedOperationException(). Dodatkowo metoda
query() musi zwracać obiekt klasy Cursor.
Poniżej zaprezentowany został pierwszy fragment implementacji klasy MyContentProvider,
będącej dostawcą treści, który obsługuje utworzoną wcześniej bazę danych.
public class MyContentProvider extends ContentProvider { // baza danych private NotatkiDatabaseHelper database; // pola wykorzystane przez obiekt klasy UriMatcher private static final int TODOS = 10; private static final int TODO_ID = 20;
8
private static final String AUTHORITY = "com.example.mk3_ap4.contentprovider"; private static final String BASE_PATH = "todos"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH); public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/todos"; public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/todo"; // utworzenie obiektu urimatchera private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { sURIMatcher.addURI(AUTHORITY, BASE_PATH, TODOS); sURIMatcher.addURI(AUTHORITY, BASE_PATH + "/#", TODO_ID);
}
W przedstawionym powyżej fragmencie kodu zadeklarowano obiekt klasy
NotatkiDatabaseHelper, który jest łącznikiem pomiędzy dostawcą treści a silnikiem bazy danych
aplikacji. Za jego pomocą w dalszych implementacjach metod dostawcy treści odbywać się będzie
aktualizacja danych w bazie danych. Dodatkowo zainicjowano stałe wykorzystywane przez obiekt
UriMatcher, który wykorzystywany jest w dalszych metodach tej klasy do sprawdzania ścieżek Uri
modyfikowanych przez dostawcę treści wartości. W końcowej części przedstawionego fragmentu
utworzono obiekt klasy UriMatcher, przechowujący odpowiednie identyfikatory, ścieżkę do bazy
danych oraz wartość przechowywaną w rejestrze dostawców treści (zdefiniowaną później w pliku
deskryptora aplikacji).
Następnie zaimplementowano kolejne metody dostawcy treści przedstawione w dalszej
kolejności.
@Override public boolean onCreate() { database = new NotatkiDatabaseHelper(getContext()); return false; }
W metodzie onCreate tworzony jest nowy obiekt bazy danych za pomocą konstruktora klasy
NotatkiDatabaseHelper. Kolejną zaimplementowaną metodą dostawcy treści jest metoda query,
której ciało przedstawiono poniżej.
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
9
// obiekt budujący zapytanie SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); // sprawdza czy rozmówca wysłał żądanie o kolumny, które nie istnieją checkColumns(projection); // ustawienie tabeli queryBuilder.setTables(TabelaNotatki.TABLE_NOTATKI); int uriType = sURIMatcher.match(uri); switch (uriType) { case TODOS: break; case TODO_ID: // dodanie ID do oryginalnej ścieżki queryBuilder.appendWhere(TabelaNotatki.COLUMN_ID + "=" + uri.getLastPathSegment()); break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } SQLiteDatabase db = database.getWritableDatabase(); Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); // upewnienie się, że potencjalni słuchacze zostali poinformowani cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; }
Metoda query służy do obsługi zapytań pochodzących od klientów i może być wywoływana z
różnych wątków. Do budowy zapytania wykorzystany został mechanizm oferowany przez klasę
SQLiteQueryBuilder. Metoda pobiera parametry w postaci: adresu URI zapytania (Uri uri), listy
kolumn, które mają być przekazane do obiektu kursora treści (String[] projection), kryterium selekcji
wierszy tabeli (String selection), lity argumentów kryterium selekcji wierszy (String[] selectionArgs)
oraz porządek sortowania (String sortOrder). Metoda query zwraca obiekt klasy Cursor, który
pozwala nawigować pomiędzy wynikami zapytania do bazy danych.
@Override public String getType(Uri uri) { return null; }
Metoda obsługująca zwracanie różnych typów MIME dla danego adresu URI. Jej
implementacja jest wymagana, jednak w tym przypadku nie istnieje potrzeba implementacji obsługi
zwracania typów elementów. W takim wypadku metoda zwraca wartość „null”.
10
@Override public Uri insert(Uri uri, ContentValues values) { int uriType = sURIMatcher.match(uri); SQLiteDatabase sqlDB = database.getWritableDatabase(); long id = 0; switch (uriType) { case TODOS: id = sqlDB.insert(TabelaNotatki.TABLE_NOTATKI, null, values); break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return Uri.parse(BASE_PATH + "/" + id); }
Kolejną zaimplementowaną metodą dostawy treści jest metoda insert, która służy do
dodawania nowego elementu określonego URI do bazy danych za pomocą obiektu klasy
ContentValues. W wyniku metoda ta zwraca adres URI wstawionego elementu.
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { int uriType = sURIMatcher.match(uri); SQLiteDatabase sqlDB = database.getWritableDatabase(); int rowsDeleted = 0; switch (uriType) { case TODOS: rowsDeleted = sqlDB.delete(TabelaNotatki.TABLE_NOTATKI, selection, selectionArgs); break; case TODO_ID: String id = uri.getLastPathSegment(); if (TextUtils.isEmpty(selection)) { rowsDeleted = sqlDB.delete(TabelaNotatki.TABLE_NOTATKI, TabelaNotatki.COLUMN_ID + "=" + id, null); } else { rowsDeleted = sqlDB.delete(TabelaNotatki.TABLE_NOTATKI, TabelaNotatki.COLUMN_ID + "=" + id + " and " + selection, selectionArgs); } break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return rowsDeleted; }
Następnie zaimplementowana została metoda delete, służąca do usuwania wybranego
elementu (lub elementów) z bazy. Metoda pobiera parametry w postaci: adresu URI zapytania,
11
kryterium wyboru elementów oraz listy argumentów kryterium wyboru. Po przekazaniu sterowania
do głównej pętli programu zwracana jest liczba usuniętych wierszy.
@Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int uriType = sURIMatcher.match(uri); SQLiteDatabase sqlDB = database.getWritableDatabase(); int rowsUpdated = 0; switch (uriType) { case TODOS: rowsUpdated = sqlDB.update(TabelaNotatki.TABLE_NOTATKI, values, selection, selectionArgs); break; case TODO_ID: String id = uri.getLastPathSegment(); if (TextUtils.isEmpty(selection)) { rowsUpdated = sqlDB.update(TabelaNotatki.TABLE_NOTATKI, values, TabelaNotatki.COLUMN_ID + "=" + id, null); } else { rowsUpdated = sqlDB.update(TabelaNotatki.TABLE_NOTATKI, values, TabelaNotatki.COLUMN_ID + "=" + id + " and " + selection, selectionArgs); } break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return rowsUpdated; }
Następnie zaimplementowano metodę update odpowiedzialną za aktualizację danych w
bazie. Metoda pobiera parametry w postaci: pełnego adresu URI zapytania, kolekcji wartości do
aktualizacji przekazaną w postaci obiektu klasy ContentValues, kryterium wyboru wierszy do
aktualizacji wartości oraz listy argumentów tego kryterium. Metoda zwraca liczbę zaktualizowanych
wierszy tabeli.
private void checkColumns(String[] projection) { String[] available = { TabelaNotatki.COLUMN_CATEGORY, TabelaNotatki.COLUMN_SUMMARY, TabelaNotatki.COLUMN_DESCRIPTION, TabelaNotatki.COLUMN_ID }; if (projection != null) {
12
HashSet<String> requestedColumns = new HashSet<String>(Arrays.asList(projection)); HashSet<String> availableColumns = new HashSet<String>(Arrays.asList(available)); // Sprawdzenie czy wszystkie kolumny, które są wymagane, są dostępne if (!availableColumns.containsAll(requestedColumns)) { throw new IllegalArgumentException("Unknown columns in projection"); } }
}
Ostatnią metodą zaimplementowaną w dostawcy treści jest metoda checkColumns,
wykorzystywana w metodzie query do sprawdzenia poprawności żądania klienta odnośnie kolumn,
które mają zostać przekazane do obiektu kursora (klasa Cursor).
Dostawca treści, aby mógł być wykorzystany, musi zostać zarejestrowany w manifeście
aplikacji. Dla zaimplementowanego wcześniej dostawcy fragment odpowiedzialny za jego rejestrację
w pliku AndroidManifest.xml przedstawiono poniżej.
<provider android:name="com.example.mk3_ap4.MyContentProvider" android:authorities="com.example.mk3_ap4.contentprovider" > </provider>
W kolejnym rozdziale przedstawiono implementację aktywności oraz układu graficznego
aplikacji wykorzystującej utworzonego dostawcę treści oraz bazę danych notatek.
IV. Tworzenie aktywności wykorzystujących bazę danych i zaimplementowanego
dostawcę treści
Zanim przedstawiona zostanie implementacja aktywności aplikacji, pokazane zostaną
definicje wykorzystywanych przez nie zasobów.
Poniżej przedstawiono definicję menu dostępnego w pasku akcji (ActionBar) w postaci napisu
Dodaj, zdefiniowanego w pliku strings.xml. Za wyświetlenie elementu menu na pasku zadań
odpowiedzialny jest argument android:showAsAction=”always”. Definicja pliku menu znajduje się w
katalogu /res/menu.
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/insert" android:showAsAction="always" android:title="@string/insert"> </item>
13
</menu>
Następnie w katalogu /res/values zdefiniowano tablicę napisów opisujących priorytety
dodawanych do listy elementów. Lista składa się z dwóch elementów, których napisy zdefiniowano w
pliku „strings.xml”.
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="priorities"> <item>@string/urgent</item> <item>@string/reminder</item> </string-array> </resources>
Wygląd pliku “strings.xml” został przedstawiony poniżej. Znajdują się w nim wszystkie napisy
wykorzystywane podczas funkcjonowania aktywności.
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">MK3_AP4</string> <string name="action_settings">Ustawienia</string> <string name="insert">Dodaj</string> <string name="urgent">Pilne</string> <string name="reminder">Przypomnienie</string> <string name="no_todos">Brak rzeczy do zrobienia</string> <string name="menu_insert">Dodaj element</string> <string name="menu_delete">Usuń element</string> <string name="todo_summary">Streszczenie</string> <string name="todo_description">Usuń wpis</string> <string name="todo_edit_summary">Streszczenie</string> <string name="todo_edit_description">Opis</string> <string name="todo_edit_confirm">Zatwierdź</string> <string name="title_activity_edit">EditActivity</string> <string name="hello_world">Hello world!</string> </resources>
Z kolei na kolejnym fragmencie kodu przedstawiono definicję XML układu graficznego
pojedynczego wiersza listy elementów, składającego się z kontrolki TextView. Plik z definicją wiersza
znajduje się w katalogu /res/layout.
<?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="wrap_content" > <TextView android:id="@+id/label" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="6dp" android:lines="1" android:text="@+id/TextView01" android:textSize="24sp" >
14
</TextView> </LinearLayout>
Poniżej z kolej przedstawiono definicję układu graficznego listy notatek, w którym znajduje
się kontrolka ListView oraz TextView, wyświetlająca odpowiedni napis w przypadku pustej listy
elementów.
<?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="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" > </ListView> <TextView android:id="@android:id/empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_todos" /> </LinearLayout>
Kolejny fragment przedstawia definicję układu graficznego ekranu
edycji/dodawania/aktualizacji elementu listy. Układ ten powiązany jest z aktywnością, do której
przekazywane jest sterowanie z głównej aktywności aplikacji. W interfejsie zdefiniowano kontrolkę
Spinner (do wyboru priorytetu notatki), pola EditText oraz kontrolkę Button.
<?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" > <Spinner android:id="@+id/category" android:layout_width="wrap_content" android:layout_height="wrap_content" android:entries="@array/priorities" > </Spinner> <LinearLayout android:id="@+id/LinearLayout01" android:layout_width="match_parent" android:layout_height="wrap_content" > <EditText android:id="@+id/todo_edit_summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"
15
android:hint="@string/todo_edit_summary" android:imeOptions="actionNext" > </EditText> </LinearLayout> <EditText android:id="@+id/todo_edit_description" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:gravity="top" android:hint="@string/todo_edit_description" android:imeOptions="actionNext" > </EditText> <Button android:id="@+id/todo_edit_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/todo_edit_confirm" > </Button> </LinearLayout>
Implementacja głównej aktywności została przedstawiona poniżej. Do obsługi kontrolki
ListView wykorzystany został adapter klasy SimpleCursorAdapter. Aktywność główna rozszerza klasę
ListActivity oraz implementuje interfejs LoaderManager.LoaderCallbacks do asynchronicznej obsługi
kursorów treści. Mechanizm ten zabezpiecza aplikację przez zawieszeniem w przypadku długiego
oczekiwania na wynik z bazy danych lub błędu zapytania.
public class MainActivity extends ListActivity implements LoaderManager.LoaderCallbacks<Cursor> { private static final int ACTIVITY_CREATE = 0; private static final int ACTIVITY_EDIT = 1; private static final int DELETE_ID = Menu.FIRST + 1; private SimpleCursorAdapter adapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.notatki_list); this.getListView().setDividerHeight(2); fillData(); registerForContextMenu(getListView()); } // Tworzenie menu bazującego na definicji w pliku XML @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.listmenu, menu); return true; }
16
// Reakacja na wybór elementu menu @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.insert: createTodo(); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case DELETE_ID: AdapterContextMenuInfo info = (AdapterContextMenuInfo) item .getMenuInfo(); Uri uri = Uri.parse(MyContentProvider.CONTENT_URI + "/" + info.id); getContentResolver().delete(uri, null, null); fillData(); return true; } return super.onContextItemSelected(item); } private void createTodo() { Intent i = new Intent(this, EditActivity.class); startActivity(i); } // Wywołanie drugiej aktywności po kliknięciu na element listy @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); Intent i = new Intent(this, EditActivity.class); Uri todoUri = Uri.parse(MyContentProvider.CONTENT_URI + "/" + id); i.putExtra(MyContentProvider.CONTENT_ITEM_TYPE, todoUri); startActivity(i); } private void fillData() { // Pola z bazy danych (projekcja) // Zmienna musi zawierać identyfikatory (_id) kolumn aby adapter działała prawidłowo String[] from = new String[] { TabelaNotatki.COLUMN_SUMMARY }; // Pola interfejsu do zmapowania z adapterem int[] to = new int[] { R.id.label }; getLoaderManager().initLoader(0, null, this); adapter = new SimpleCursorAdapter(this, R.layout.notatki_row, null, from, to, 0);
17
setListAdapter(adapter); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.add(0, DELETE_ID, 0, R.string.menu_delete); } // Tworzenie nowego loadera po wywołaniu metody initLoader() @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { String[] projection = { TabelaNotatki.COLUMN_ID, TabelaNotatki.COLUMN_SUMMARY }; CursorLoader cursorLoader = new CursorLoader(this, MyContentProvider.CONTENT_URI, projection, null, null, null); return cursorLoader; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { adapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { // dane nie są już dostępne, usunięcie referencji adapter.swapCursor(null); }
}
Implementacja aktywności edycji elementu została przedstawiona poniżej. Komunikacja
pomiędzy aktywnością główną (MainActivity) a przedstawioną poniżej odbywa się za pomocą obiektu
Intent, w którym następuje przekazanie wartości pojedynczego elementu listy notatek.
public class EditActivity extends Activity { private Spinner mCategory; private EditText mTitleText; private EditText mBodyText; private Uri todoUri; @Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(R.layout.activity_edit); mCategory = (Spinner) findViewById(R.id.category); mTitleText = (EditText) findViewById(R.id.todo_edit_summary); mBodyText = (EditText) findViewById(R.id.todo_edit_description); Button confirmButton = (Button) findViewById(R.id.todo_edit_button); Bundle extras = getIntent().getExtras();
18
// sprawdzenie danych z zapisanej instancji todoUri = (bundle == null) ? null : (Uri) bundle .getParcelable(MyContentProvider.CONTENT_ITEM_TYPE); // lub przekazanych przez inną aktywność if (extras != null) { todoUri = extras .getParcelable(MyContentProvider.CONTENT_ITEM_TYPE); fillData(todoUri); } confirmButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { if (TextUtils.isEmpty(mTitleText.getText().toString())) { makeToast(); } else { setResult(RESULT_OK); finish(); } } }); } private void fillData(Uri uri) { String[] projection = { TabelaNotatki.COLUMN_SUMMARY, TabelaNotatki.COLUMN_DESCRIPTION, TabelaNotatki.COLUMN_CATEGORY }; Cursor cursor = getContentResolver().query(uri, projection, null, null, null); if (cursor != null) { cursor.moveToFirst(); String category = cursor.getString(cursor .getColumnIndexOrThrow(TabelaNotatki.COLUMN_CATEGORY)); for (int i = 0; i < mCategory.getCount(); i++) { String s = (String) mCategory.getItemAtPosition(i); if (s.equalsIgnoreCase(category)) { mCategory.setSelection(i); } } mTitleText.setText(cursor.getString(cursor .getColumnIndexOrThrow(TabelaNotatki.COLUMN_SUMMARY))); mBodyText.setText(cursor.getString(cursor .getColumnIndexOrThrow(TabelaNotatki.COLUMN_DESCRIPTION))); // zamknięcie kursora cursor.close(); } } protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); saveState(); outState.putParcelable(MyContentProvider.CONTENT_ITEM_TYPE, todoUri); }
19
@Override protected void onPause() { super.onPause(); saveState(); } private void saveState() { String category = (String) mCategory.getSelectedItem(); String summary = mTitleText.getText().toString(); String description = mBodyText.getText().toString(); // Zapisanie tylko gdy pole summary i descriptions są wypełnione if (description.length() == 0 && summary.length() == 0) { return; } ContentValues values = new ContentValues(); values.put(TabelaNotatki.COLUMN_CATEGORY, category); values.put(TabelaNotatki.COLUMN_SUMMARY, summary); values.put(TabelaNotatki.COLUMN_DESCRIPTION, description); if (todoUri == null) { // Nowy element todoUri = getContentResolver().insert(MyContentProvider.CONTENT_URI, values); } else { // Aktualizacja elementu getContentResolver().update(todoUri, values, null, null); } } private void makeToast() { Toast.makeText(EditActivity.this, "Proszę wprowadzić streszczenie", Toast.LENGTH_LONG).show(); }
}
Na rys. 1 przedstawiono ekran aplikacji z pustą listą notatek. U góry po prawej na pasku
ActionBar znajduje się napis Dodaj. Jego kliknięcie powoduje wywołanie aktywności dodawania
elementu.
20
Rys. 1 Uruchomienie programu
Rys. 2 przedstawia ekran dodawania/edycji elementu listy w postaci notatki o podanym
priorytecie (kontrolka Spinner). Odpowiednie wypełnienie poszczególnych pól ekranu i kliknięcie w
przycisk „Zatwierdź” powoduje dodanie/aktualizację notatki.
21
Rys. 2 Ekran edycji
Na rys. 3 przedstawiony został ekran aplikacji w przypadku próby usunięcie zaznaczonego na
liście elementu (notatki). Przyciśnięcie napisu „Usuń element” powoduje usunięcie notatki z listy.
22
Rys. 3 Usuwanie elementu
Przedstawiony tu przykład implementacyjny ma na celu wprowadzenie odbiorcę w
problematykę tworzenia adapterów baz danych oraz własnych dostawców treści w środowisku
aplikacji Android. Treści przekazane w ramach tej pracy są jednie wstępem do dalszego poszukiwania
informacji na ten temat w literaturze fachowej.
V. Bibliografia
1. Arsoba, R. (2011). Programowanie urządzeń mobilnych. Zagadnienia podstawowe. Pobrano
Czerwiec 12, 2012 z lokalizacji
http://grafika.weii.tu.koszalin.pl/android/Programowanie_Android.pdf
2. Conder S., D. L. (2011). Android. Programowanie aplikacji na urządzenia przenośne. Wydanie
II. Gliwice: Helion.
3. Geetha, S. (2011, Maj 17). Sai Geetha's Blog - Android. Pobrano Czerwiec 20, 2013 z
lokalizacji Sai Geetha's Blog: http://saigeethamn.blogspot.in/2011/05/contacts-api-20-and-
above-android.html
4. Komatineni S., M. D. (2012). Android 3. Tworzenie aplikacji. Gliwice: Helion.
23
5. Lee, W.-M. (2011). Beginning Android Application Development. Indianapolis: Wiley
Publishing Inc.
6. Vogel, L. (2013, sierpień 19). Android SQL database and content provider - tutorial. Pobrano z
lokalizacji
http://www.vogella.com/articles/AndroidSQLite/article.html#contentprovider_own