Ukážková aplikácia
Vytvorme aplikáciu, ktorá zobrazí kalendárik, v ktorom si môžeme začiarkavať dni jednou z vybraných farieb.
Predpripravené materiály
Defaults.java
— konštanty pre chýbajúce a štandardné parametre metódDatabase.java
— definície metadát pre tabuľky
Výsledná aplikácia
Viď projekt DayGrid na GitHube. Dáta sú predbežne uložené v pamäti.
Elementy, ktoré sa neriešili
- posúvanie kalendára o mesiac dozadu či dopredu (zobrazujeme fixný dátum)
- action bar
- ukladanie stavu inštancie aktivity
Poznámka pre budúce generácie
Z hľadiska rozloženia času je lepšie najprv ukázať farbenie jednou pevnou farbou (teda zatiaľ ignorovať dialógy a ich buildery) a ušetrený čas venovať dokončeniu content providera. Zároveň je vhodné neskúšať rovno databázový content provider, ale naučiť sa najprv pamäťový a v budúcom stretnutí ho rozšíriť.
Koncepty, ktoré zvládneme
- vlastný layout komponentov na príklade tabuľky
- použitie užitočnej triedy
android.util.MonthDisplayHelper
- práca s poľami reťazcov v resources
- dialógy a ich buildery
- tvorba vlastných content providerov
Vlastné layouty
- dosiaľ sme layout vždy načítavali z XML súboru
- uložený v
res/layout
- nafúknutý cez
setContentView()
- uložený v
- layouty však môžeme vyrábať aj ručne
- štýl ako v Swingu
Kalendár a tabuľkový layout
- kalendár má fixný počet stĺpcov (dní): sedem
- rovnako fixný počet riadkov (týždňov): šesť
- určite sa nám nechce definovať XML s 7 x 6 elementami
Tabuľkový layout
- trieda
TableLayout
- filozofia a la HTML:
- pozostáva z riadkov (
TableRow
) - a buniek (ľubovoľný widget)
- pozostáva z riadkov (
- stĺpce natiahneme cez
setStretchAllColumns(true)
- nezabudnime na korektnú hierarchiu:
- pridávať bunky do riadkov cez
row.addView()
- a pridávať riadky do tabuľky cez
table.addView()
- a tabuľku do aktivity cez
setContentView()
- pridávať bunky do riadkov cez
- v našej aplikácii dva
for
cykly.
Obsluha udalostí
- prakticky vytvoríme dynamicky 7 x 6 buniek typu
TextView
- vytvoríme klasicky obsluhu ich kliku cez
setOnClickListener()
- najlepšie je delegovať vnútro metódy do metódy v aktivite
- zvýši sa prehľadnosť kódu
Míľník: vidíme naplnenú tabuľku
- rozbehnite aplikáciu tak, aby v tabuľke ukazovala riadkové a stĺpcové koordináty
- sledujte správanie pri rotovaní displeja
- stĺpce sa samé naťahujú
Pomocná trieda MonthDisplayHelper
- zabudovaná trieda na kreslenie kalendárika
- inicializujeme ju:
- rokom
- mesiacom (pozor, indexované od nuly!) ako v
java.util.Calendar
- prvým dňom v mesiaci: na Slovensku začíname
Calendar.MONDAY
om.
- vieme jej dať index riadku a stĺpca a vráti kalendárny deň v mesiaci
- využijeme ju pri napĺňaní tabuľky
Estetika tabuľky
- nastaviť farbu pozadia cez
textView.setBackgroundColor()
- nastavme centrovanie cez
setGravity(Gravity.CENTER)
- osivieť bunky môžeme cez
setEnabled(false)
- zabránime tým kliku, ale to u nás nevadí
- využijeme metódu
isWithinCurrentMonth()
, ktorá vrátitrue
, ak je stĺpec a riadok v aktuálnom mesiaci.
Míľník
Rozbehnime aplikáciu, ktorá zobrazuje dátumy pre pevne zadefinovaný mesiac a rok.
Doménové objekty a klik na políčko
Ako zistiť, ktorý dátum sa skrýva pod konkrétnym políčkom tabuľky? Musíme vedieť, na ktorý dátum sme klikli.
Doménový objekt
Vieme vytvoriť separátny doménový objekt reprezentujúci dátum:
- API pre
java.util.Date()
je zastaralé - API pre
java.util.Calendar()
je WTF (mesiace sú indexované od nuly) - Joda Time API ešte nemáme
- potrebujeme prevod na linuxový timestamp (kvôli databázam)
- a prevod na reťazec pre podmienky v SQLite
Klik na políčko
- asociujme konkrétny doménový objekt s políčkom
- každý objekt typu
View
má metódusetTag()
, kde vieme priradiť komponentu ľubovoľné dáta v obsluhe kliku vieme z komponentu, na ktorý sa kliklo, vytiahnuť doménový objekt dátumu a pracovať s ním cez
getTag()
DayMonthYear date = (DayMonthYear) dayTextView.getTag();
Výber farby
V prvej verzii podporujeme len zabudované farby:
- Deklarujeme názvy farieb v resources.
- Vytvoríme
enum
pre farby. - Vytvoríme dialóg pre ich výber
Názvy farieb v resources
V strings.xml
deklarujeme farby:
<string-array name="colors_array">
<item>Červená</item>
<item>Modrá</item>
<item>Zelená</item>
<item>Fialová</item>
<item>Oranžová</item>
<item>Biela</item>
<item>Čierna</item>
<item>Bez farby</item>
</string-array>
Tieto farby definujú len názvy pre ľudí, teda použíateľov.
Enum pre farby
V kóde využijeme
enum
: farieb na výber je totiž len konečný a nemenný počet. V Androide síce existuje triedaandroid.util.Color
, ale to je len biedny obal s konštantami a niekoľkými pomocnými metódami. V skutočnosti je androidná farba reprezentovaná akoint
a táto pomocná trieda rieši prevody medzi reprezentáciami.Poradie farieb v
enum
e musí byť rovnaké ako poradie v resourcoch!- Zdrojový súbor je enum
Daycolor
.
Výber farby
- výber farby urobme v modálnom dialógu
- neoplatí sa vytvárať aktivitu, tá je príliš ťažkotonážna. Dialógy sa hodia presne pre prípady, ak chceme od používateľa vyžiadať informácie, bez ktorých sa nepohneme pri danej postupnosti krokov
na vybudovanie dialógu slúži
AlertDialog.Builder
:new AlertDialog.Builder(this) .setTitle("Vyberte farbu") .setItems(R.array.colors_array, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { colorize(date, DayColor.values()[which]); prepareLayout(); } }) .show();
definujeme titulok title
- zoznam položiek do výberu, ktorý vytiahneme z resources
- obsluhu pre klik v podobe
OnClickListener
a.- pozor, toto je iný
DialogInterface.OnClickListener
, čo je iný interfejs ako ten, ktorý sa používa v obsluhe kliku na políčko tabuľky! (musíme ho uviesť názvom triedy). - dostaneme k dispozícii
which
, čo je index položky, na ktorú sa kliklo
- pozor, toto je iný
- metódou
show()
vybudujeme a zobrazíme dialóg.
Providery
- prostriedok pre prístup k tabuľkovým dátam
- automaticky beží v separátnom vlákne!
- videli sme v akcii pri minulom stretnutí
Vlastný provider
- vlastná trieda dediaca od
android.content.ContentProvider
˛ - deklarácia v manifeste
Deklarácia v manifeste
Provider potrebuje:
- názov triedy, ktorá ho implementuje v atribúte
name
- symbolické meno pre providera: autorita (authority)
- typicky v tvare názovBalíčkaAplikácie
.provider
- ostatné aplikácie využívajú autoritu na vyhľadanie konkrétneho providera
- typicky v tvare názovBalíčkaAplikácie
- či je provider dostupný:
enabled
či je exportovaný pre použitie v cudzích aplikáciách:
exported
<provider android:name="sk.upjs.ics.novotnyr.daygrid.DayColorContentProvider" android:authorities="sk.upjs.ics.novotnyr.daygrid.provider" android:enabled="true" android:exported="false" > </provider>
Vlastná trieda
- potrebujem definovať content URI, adresu, ktorá jednoznačne identifikuje providera a konkrétnu tabuľku
- v tvare content://autorita/tabuľka
zvoliť si ju môžeme ľubovoľne, v našom prípade:
content://sk.upjs.ics.novotnyr.daygrid.provider/daycolor
Táto content URI pristupuje k tabuľke
daycolor
.alternatívne vybudujme pomocou triedy
Uri.Builder
:private static final String AUTHORITY = "sk.upjs.ics.novotnyr.daygrid.provider"; private static final String DAYCOLOR_TABLE = "daycolor"; public static final Uri YEAR_MONTH_DAY_COLOR = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY) .appendPath(Database.DayColor.TABLE_NAME) .build();
Názvy tabuliek
- Konvencia je vybudovať typovo bezpečný popis schémy: názvy tabuliek a ich stĺpcov
- Predídeme tým lepeniu reťazcov s názvami tabuliek a stĺpcov
- Uľahčuje prípadný refactoring, ak sa zmení schéma databázy.
Príklad pre databázu daygrid
s jednou tabuľkou daycolor
a štyroma stĺpcami.
import android.provider.BaseColumns;
public class Database {
public static final String NAME = "daygrid";
public static final int VERSION = 1;
public interface DayColor extends BaseColumns {
public static final String TABLE_NAME = "daycolor";
public static final String TIMESTAMP = "timestamp";
public static final String COLOR = "color";
}
}
Stĺpec _id
sa automaticky zdedí z interfejsu BaseColumns
.
Prekrytie metód
onCreate()
: pri vytvorení content providera. Beží vo vlákne UI, musí preto zbehnúť rýchlo. Ak využívame databázu, nesmieme tu získavať objekt databázy cezget***Database()
, keďže je to pomalá operácoainsert()
,query()
,update()
adelete()
: operácie pre vyberanie dát, resp. ich modifikáciu. Bežia v samostatnom vlákne.getType()
: identifikácia množstva dát, ktoré sa vrátia z dopytu priquery()
: buď jeden objekt alebo sada.
Operácie nad dátami a URI
- každá z metód pracujúcich nad dátami má parameter
URI
- je to content URI, kde
- musíme zistiť, či ju náš content provide podporuje
- a voliteľne z nej vytiahneme dáta
Návrh URI pre kalendár a rozhodovacia logika pre podporu
Návrh URI
Dohoda:
content://sk.upjs.ics.novotnyr.daygrid.provider/daycolor/2014/03/15
URI pre prácu s farbou 15. marca 2014.
Príprava UriMatchera
Rozhodovaciu logiku i parsovanie rieši android.content.UriMatcher
. Použijeme ho ako inštančnú premennú:
private UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
V metóde onCreate()
definujeme pravidlo pre zhodu:
uriMatcher.addURI(AUTHORITY, Database.DayColor.TABLE_NAME + "/*/*/*", YEAR_MONTH_DAY_COLOR_CODE);
- pre autoritu v konštante definujeme sufix a tri hviezdičkové zástupné znaky zodpovedajúce trom segmentom v ceste, ktoré sú reťazcového typu
zároveň tomuto pravidlu priradíme číselný kód definovaný v našej konštante:
private static final int YEAR_MONTH_DAY_COLOR_CODE = 0;
Použitie matchera:
Pre URI
z parametra metódy contentprovidera zavoláme metódu match()
a vráti sa nám číselný kód pravidla, oproti ktorému oswitchujeme:
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
switch(uriMatcher.match(uri)) {
case YEAR_MONTH_DAY_COLOR_CODE:
return queryDayColorForYearAndMonth(uri);
default:
return null;
}
}
Hodnoty jednotlivých segmentov získame cez uri.getPathSegments().get(i)
, kde i
je index segmentu (od nuly).
Zistenie množstva dát
Kurzory s jediným riadkom
Ak metóda query()
vracia jediný riadok, musí vrátiť špeciálny MIME typ. Prefix musí byť vnd.android.cursor.item
, sufix definujeme my, autori content providera:
vnd.android.cursor.item/vnd.sk.upjs.ics.novotnyr.daygrid.provider.daycolor
V kóde:
private static final String MIME_TYPE_DAYCOLOR_SINGLE_ROW = CURSOR_ITEM_BASE_TYPE + "/vnd." + AUTHORITY + "." + Database.DayColor.TABLE_NAME;
Kurzory s nula a viacerými riadkami
Ak metóda query()
vracia nula či viac riadkov, použijeme prefix vnd.android.cursor.dir
vnd.android.cursor.dir/vnd.sk.upjs.ics.novotnyr.daygrid.provider.daycolor
Výber typu
Typ samozrejme môžeme opäť naswitch
ovať:
@Override
public String getType(Uri uri) {
switch(uriMatcher.match(uri)) {
case YEAR_MONTH_DAY_COLOR_CODE:
return MIME_TYPE_DAYCOLOR_SINGLE_ROW;
default:
return null;
}
}
Aktualizačné operácie
Je slušné notifikovať poslucháčov o zmene dát cez:
getContext().getContentResolver().notifyChange(uri, NO_CONTENT_OBSERVER);
kde NO_CONTENT_OBSERVER
je konštanta, za ktorou sa skrýva null
.
Použitie providera v kóde aktivity
- Využijeme, ako minule content resolver, do ktorého pošleme príslušnú URI.
Našu URI vybudujeme pomocnou metódou:
content://sk.upjs.ics.novotnyr.daygrid.provider/daycolor/2014/03/15
Budujme cez:
Uri uri = Uri.withAppendedPath(DayColorContentProvider.YEAR_MONTH_DAY_COLOR, String.valueOf(date.getYear())); uri = Uri.withAppendedPath(uri, String.valueOf(date.getMonth())); uri = Uri.withAppendedPath(uri, String.valueOf(date.getDay()));
Zdroje
- Android SQLite database and content provider na Vogella.com
- Creating a Content Provider v dokumentácii Androidu
- Ukážkový content provider v demách Androidu:
sdk/samples/android-19/legacy/NotePad/src/com/example/android/notepad/NotePadProvider.java