Vytvoríme projekt o dvoch aktivitách: jedna obsahuje zoznam úloh, druhá detaily aktivity. Umožníme vytvárať nové upravovať existujúce úlohy.
ListView
v aktiviteArrayAdapter
a.V Studiu si založme nový projekt Taskr s jednou štandardnou aktivitou. Názov aktivity (Activity Name) nastavme na TaskListActivity
: budeme totiž používať už dve aktivity, ktoré by sme si mali poriadne pomenovať. Titulok aktivity (Title) nastavme na Taskr, čo je hodnota, ktorá sa objaví v hlavičke aktivity.
Všimnime si, ako Studio automaticky odvodilo názvy ostatných elementov: konkrétne názov pre layout (activity_task_list
) a menu (menu_task_list
), o ktorom si ešte povieme neskôr.
Hlavnú aktivitu budeme modelovať podľa článku Aktivita so zoznamom a modely/adaptéry. Použijeme však vlastnú triedu pre Task
i TaskDao
z kostry projektu. Zoznam úloh tak nebudeme mať uvedený napevno, ale získame ho pomocou metódy list()
z TaskDao
. Povedzme si o tom viac!
V kostre projektu nájdeme základnú triedu pre úlohu Task
, kde evidujeme celočíselný identifikátor (čo je zároveň príprava pre databázu), názov úlohy (reťazec) a booleovský príznak, či je úloha splnená.
Kód vyzerá nasledovne:
public class Task {
private Long id;
private String name;
private boolean isDone;
private static final boolean IS_NOT_DONE = false;
public Task() {
// empty constructor
}
public Task(String name, boolean isDone) {
this.name = name;
this.isDone = isDone;
}
public Task(String name) {
this(name, IS_NOT_DONE);
}
/* vynechané gettery a settery */
}
TaskDao
¶Ďalej využijeme triedu pre prácu s „pamäťovou databázou“. O plnoprávnych databázach si povieme na budúcom stretnutí, pretože vyžadujú pojesť trochu viac fazule, ale zatiaľ si vystačíme s pseudotriedou TaskDao
, ktorá poskytne nasledovné funkcionality:
Inštancia TaskDao
bude singletonová: predsa nepotrebujeme mať v aplikácii viac paralelne bežiacich, prepytujem, databáz.
Táto padatabáza slúži len na testovacie účely. Nedokáže prežiť reštart operačného systému ani korektne nepodporuje životný cyklus aktivity. Riešiť to naozaj nebudeme, pretože do produkčného nasadenia potrebujeme poriadnu databázu.
DAO a entity použijeme v aplikácii jednoducho: DAO objekt použijeme v úlohe inštančnej premennej, využijeme pritom návrhový vzor singleton, a zavoláme metódu list()
, ktorá nám vráti zoznam všetkých úloh.
public class TaskListActivity extends ActionBarActivity {
private ListView taskListView;
// singletonovská inštancia
private TaskDao taskDao = TaskDao.INSTANCE;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_list);
taskListView = (ListView) findViewById(R.id.taskListView);
// úlohy získame z DAO objektu
List<Task> tasks = taskDao.list();
ArrayAdapter<Task> listAdapter = new ArrayAdapter<Task>(this, android.R.layout.simple_list_item_1, tasks );
taskListView.setAdapter(listAdapter);
}
}
Prispôsobme potom v dialógovom okne našu novú aktivitu:
TaskDetailActivity
.TaskListActivity
.Navrhnime si výzor používateľského rozhrania!
Ak máme používateľské rozhranie, ktoré pripomína tabuľku, vieme využiť v layoute <TableLayout>
. Jednotlivé riadky pomyselnej tabuľky sa nachádzajú v elemente <TableRow>
a v bunkách sa nachádza vždy jeden komponent.
Deklarujme layout takto:
<TableLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
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=".TaskDetailActivity"
android:stretchColumns="1">
<TableRow>
<TextView android:text="Názov:" />
<EditText android:id="@+id/taskNameEditText"/>
</TableRow>
<TableRow>
<TextView android:text="Hotová:"/>
<CheckBox
android:id="@+id/taskDoneCheckBox"
android:layout_gravity="right"
/>
</TableRow>
</TableLayout>
Všimnime si napr. druhý riadok tabuľky: nachádza sa v ňom jeden TextView
pre popisok a jedno textové políčko pre zadanie názvu úlohy.
Každý layout má tiež svoje vlastné atribúty. Poďme si ich popísať:
match_parent
znamená, že zaberie toľko výšky (šírky), koľko má nastavený rodič. Keďže layout zaberie celú "obrazovku", hodnoty znamenajú vyplnenie celého displeja, ak to ide.@dimen/activity_vertical_margin
, ktoré nastavia "vhodné" množstvo vzduchu.tools:context
) alebo vlastnosťou XML dokumentu (xmlns:android
a xmlns:tools
).Ak chce aplikácia vyvolať novú aktivitu, zodpovedá to obvykle úmyslu používateľa začať vykonávať čosi nové s aplikáciou. Tento úmysel, intent sa naplní spustením novej aktivity.
Filozofia je jednoduchá a elegantná. Ak si používateľ povie, že "mám v úmysle spustiť aktivitu pre prihlásenie", čaká, že niektorá z mnohých aktivít systému jeho úmysel naplní a urobí ho šťastným.
Presnejšie povedané, intent predstavuje správu medzi dvoma aktivitami, ktorá v sebe nesie dve zložky:
Na platforme Android je intent reprezentovaný ako správa posielaná medzi aktivitami. Obsahuje akciu, ktorá sa má vykonať a dodatočné dáta.
V kóde môžeme intent vytvoriť pomocou triedy Intent
. Nasledovný príklad vyjadruje úmysel poslať e-mail adresátom uvedeným v dátach:
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity(intent);
Všimnime si tri dôležité veci: pri vytváraní intentu špecifikujeme:
startActivity()
Systém vyhľadá najvhodnejšiu aktivitu, ktorá dokáže splniť tento úmysel a spustí ju. Ak je vhodných aktivít viac, dostane používateľ na výber.
Ďalší príklad ukazuje spustenie prehliadača a zobrazenie konkrétnej URL adresy:
Uri uri = Uri.parse("https://ics.upjs.sk");
Intent runBrowser = new Intent(Intent.ACTION_VIEW, uri);
startActivity(runBrowser);
Dáta intentu sú tvorené URI adresou uloženou pod systémovo dohodnutý kľúč ACTION_VIEW
. Metódou startActivity()
odvysielame intent do systému, ktorý sa pokúsi nájsť všetky aktivity umožňujúce otvoriť túto adresu ako webovú stránku. Ak máme k dispozícii len jeden prehliadač, rovno sa začne načítavať webová stránka. Ak je prehliadačov viac, dostaneme na výber.
%TODO% screenshot
Spúšťanie aktivity v rámci jednej aplikácie je však o niečo jednoduchšie, o to viac, ak poznáme názov triedy pre novospúšťanú aktivitu. Slúži na to konštruktor s dvoma parametrami: kontextom (obvykle this
) a spomínaným názvom triedy pre novú aktivitu.
Intent intent = new Intent(this, TaskDetailActivity.class);
startActivity(intent);
Na to, aby sme vedeli spustiť detailovú aktivitu, potrebujeme nejaké tlačidlo alebo iný prvok používateľského rozhrania. A jeden z odporúčaných spôsobov je použiť lištu akcií.
Action Bar (lišta akcií) vyzerá na prvý pohľad ako hlavička okna. V skutočnosti však dokáže omnoho viac vecí: vie orať, siať, plieť, a mnoho iných akcií.
Teda, pardón.
Action Bar je hlavička okna, ktorá plní viacero účelov. Zobrazuje názov aktivity, ktorý pomáha používateľovi zorientovať, kde sa práve v aplikácii nachádza, dokáže vykresliť ikonu aplikácie, a okrem toho umožňuje zobraziť jedno či viacero tlačidiel pre tie najdôležitejšie akcie, ktoré môže používateľ vykonať v aktivite.
Tu je ukážka lišty akcií bez ikonky, s názvom aplikácie a jedným tlačidlom:
Tlačidlá lišty akcií sa definujú v prostriedku typu menu (je to ďalší typ prostriedku do zbierky popri drawable, layoutoch, či raw). Vždy, keď vytvoríte novú aktivitu, Android Studio automaticky vygeneruje aj súbor pre výzor action baru.
Nájdete ho v adresári res/menu
, kde by sme v tejto chvíli mali vidieť definíciu výzoru pre dve aktivity.
Otvorme si menu_task_list.xml
a pozrime sa naň a prispôsobme ho takto:
<menu xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
xmlns:tools="https://schemas.android.com/tools" tools:context=".TaskListActivity">
<item android:id="@+id/addNewTaskMenu"
android:title="Pridať"
android:icon="@android:drawable/ic_menu_add"
app:showAsAction="ifRoom" />
</menu>
Súbor definuje lištu akcií s jedinou položkou reprezentovanou elementom <item>
. Podobne ako komponenty aktivity má svoj vlastný identifikátor a samozrejme popisok (title
).
Ďalšia položka icon
definuje ikonu, ktorá sa zjaví spolu alebo namiesto popisku. V tomto prípade využijeme odkaz na zabudovanú ikonu (prostriedok typu @drawable
) z Androidu, čo definujeme prefixom @android:drawable
a názvom ikony ic_menu_add
.
Posledná položka app:showAsAction
definuje výzor položky menu. Ak je na lište príliš veľa tlačidiel, prebytočné sa spracú do menu schovaného pod tlačidlom s troma bodkami. Atribút ifRoom
hovorí, že položka sa má zobraziť ako tlačidlo na lište, ak je na ňu miesto a ak sa nezmestí, má sa ocitnúť v ponuke. Alternatívne možnosti sú always
(vždy zobrazovať ako tlačidlo) alebo never
(vždy zobrazovať pod dodatočnými položkami v menu).
Pozor, atribút showAsAction
nemá prefix android
, ale app
. Je to kvôli využívanie knižnice kompatibility (compat library) umožňujúcej spätnú kompatibilitu a využívanie nových vlastností na starých verziách Androidu.
Samotná definícia položiek action baru sa v aktivite načíta v metóde onCreateOptionsMenu
. Názov pochádza opäť zo starších verzií Androidu, a obsah je automaticky vygenerovaný Studiom. Vytvoril sa kód, ktorý načítal menu z definičného súboru a zobrazí ho, ak je to potrebné.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_task_list, menu);
return true;
}
Obsluha jednotlivých tlačidiel sa deje v metóde onOptionsItemSelected()
. Kód je jednoduchý: cez item.getItemId()
zistíme identifikátor aktuálne vybratej položky a pomocou megaif
u rozhodneme, čo sa má vykonať.
Ak položku nevieme obslúžiť, sme povinní použiť rodičovskú implementáciu. V opačnom, pozitívnom prípade, vždy vrátime true
.
Práve toto je miesto, kde sa môžeme rozhodnúť spustiť druhú aktivitu.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.addNewTaskMenu) {
// spustime novu aktivitu
Intent intent = new Intent(this, TaskDetailActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
Oživme teraz kódom aktivitu s detailom úlohy. Prvotná verzia kódu vytiahne jednotlivé komponenty z layoutu do inštančných premenných, aby sme ich vedeli využívať aj v iných metódach.
Popri tom potrebujeme inštančnú premennú pre TaskDao
, cez ktorú budeme ukladať nové a neskôr i upravené úlohy.
Poslednou vecou je inštančná premenná pre Task
, zodpovedajúca práve zobrazovanej úlohe. Ak si pamätáte úvahu o adaptéroch, ktoré reprezentovali model s dátami namaľované konkrétnym komponentom, uvidíte ju ešte raz, i keď v trochu inej podobe. V tomto prípade bude objekt Task
modelom pre celú aktivitu, teda bude niesť všetky dáta, ktoré bude aktivita maľovať na displej používateľa.
V tejto fáze ideme len vytvárať nové úlohy Task
, čomu zodpovedá vytvorenie čerstvej inštancie cez new Task()
.
public class TaskDetailActivity extends ActionBarActivity {
private EditText taskNameEditText;
private CheckBox taskDoneCheckBox;
private TaskDao taskDao = TaskDao.INSTANCE;
private Task task;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_detail);
taskNameEditText = (EditText) findViewById(R.id.taskNameEditText);
taskDoneCheckBox = (CheckBox) findViewById(R.id.taskDoneCheckBox);
task = new Task();
}
}
Tento kód toho veľa nerobí, poďme doimplementovať ukladanie do pseudodatabázy.
Naša detailová aktivita môže byť kedykoľvek prekrytá inou aktivitou (napr. aktivitou prichádzajúceho hovoru). V takom prípade odchádza na pozadie a je pozastavená. Doposiaľ sme reagovali na túto udalosť metódou saveInstanceState()
, kde sme uložili stav aktivity do Bundle
a po znovuobnovení aktivity sme stav obnovili v metóde onCreate()
. Žiaľ, takto uložený stav nedokáže prežiť reštart telefónu a ani situáciu, keď Android kvôli nedostatku pamäte odstrelí celú vašu aplikáciu.
onPause()
¶Na uloženie perzistentného stavu aktivity existuje iná dvojica metód životného cyklu aktivity. Vo chvíli, keď je aktivita pozastavená a odchádza na pozadie, zavolá sa metóda onPause()
. V tejto metóde môže metóda pozastaviť činnosti, ktoré nie sú na pozadí podstatné: napr. prehrávanie videí či zvukov, alebo uvoľnenie objektu fotoaparátu.
Zároveň táto metóda slúži na uloženie rozpracovaných údajov do databázy, pretože jej vykonanie je často prvým príznakom situácie, keď používateľ úplne opúšťa aktivitu.
V našej metóde onPause()
teda preklopíme údaje z ovládacích prvkov do modelového objektu Task
, ktorý následne uložíme. Nezabudneme volať rodičovskú implementáciu!
@Override
protected void onPause() {
super.onPause();
task.setName(taskNameEditText.getText().toString());
task.setDone(taskDoneCheckBox.isChecked());
taskDao.saveOrUpdate(task);
}
Vyzerá, že to stačí na ukladanie. Vždy, keď vyplníme dáta a vrátime sa do hlavnej aktivity (či už cez hardvérové tlačidlo Back alebo cez spätnú šípku v action bare), sa uloží nová úloha, vrátime sa do hlavnej aktivity a uvidíme ju ihneď v zozname.
Až na...
Čo sa stane ak otočíme zariadenie na detailovej aktivite? Čo ak ho otočíme trikrát? Pri každom otočení sa celá aktivita úplne reštartne (zničí a vytvorí nanovo), čo znamená, že sa trikrát zavolá metóda metóda onPause()
a rozpracované dáta sa uložia na trikrát. Ešte dôležitejšie je to, že trikrát sa zavolá i metóda onCreate()
, kde vždy vznikne nová čerstvá inštancia úlohy.
Ako tomu zabrániť?
Musíme to skĺbiť s onSaveInstanceState()
. Ak sa aktivita ide zničiť, zavolá sa práve táto metóda, kde môžeme uložiť do bundlu stav aktivity, ktorým je model reprezentovaný triedou Task
. V metóde onCreate()
si model vytiahneme z bundlu a obnovíme ho.
Na to, aby sme mohli Task
ukladať do bundlu, potrebujeme ho mať označený ako Serializable
a to implementáciou interfejsu.
Dodajme teda kód:
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(TASK_BUNDLE_KEY, task);
}
Konštanta TASK_BUNDLE_KEY
je definovaná nasledovne:
public static final String TASK_BUNDLE_KEY = "task";
Zároveň opravme aj metódu onCreate()
, kde dodajme vyťahovanie inštancie úlohy z bundlu:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_detail);
taskNameEditText = (EditText) findViewById(R.id.taskNameEditText);
taskDoneCheckBox = (CheckBox) findViewById(R.id.taskDoneCheckBox);
if(savedInstanceState != null) {
task = (Task) savedInstanceState.get(TASK_BUNDLE_KEY);
} else {
task = new Task();
}
}
Pri komplikovanejšom životnom cykle treba vedieť, čo sa v skutočnosti deje. Môžu nastať okrajové prípady, ktoré skomplikujú život.
Metóda onSaveInstanceState()
sa volá tesne pred zabitím aktivity (kill / destroy), a jej zodpovednosť je uložiť priebežný stav inštancie aktivity (napríklad stav hry z minulej Šibenice). Táto metóda však nie je súčasťou životného cyklu, čo znamená, že sa môže, ale nemusí zavolať.
Máme garantované, že sa zavolá tesne pred zničením aktivity, teda pred metódou onDestroy()
. Pozor však: môže sa zavolať buď pred alebo až po zavolaní onPause()
, čo musíme zobrať do úvahy!
Pri ukladaní teda môže nastať viacero prípadov:
onPause()
sa volá skôr než onSaveInstanceState()
onCreate()
sa vytvorí nová inštancia Task
uonPause()
, kde sa úloha task
uloží a trieda TaskDao
jej pridelí identifikátoronSaveInstanceState()
, kde sa úloha (spolu s ID) uloží do bundleonCreate()
, kde príde bundle, z ktorého sa vytiahne úloha task
s IDonPause()
, kde sa už len aktualizuje task
v rámci metódy saveOrUpdate()
onSaveInstanceState()
sa volá skôr než onPause()
onCreate()
sa vytvorí nová inštancia Task
uonSaveInstanceState()
, kde sa úloha (spolu s ID) uloží do bundleonPause()
, kde sa úloha task
uloží a trieda TaskDao
jej pridelí identifikátor.Opravíme to jednoducho: ukladanie úlohy vytiahneme do samostatnej metódy, ktorú zavoláme z oboch metód.
private void saveTask() {
task.setName(taskNameEditText.getText().toString());
task.setDone(taskDoneCheckBox.isChecked());
taskDao.saveOrUpdate(task);
}
Kód bude vyzerať nasledovne: v metóde onPause()
len uložíme úlohu (čím jej pridelíme IDčko, ako nemáme), a v metóde onSaveInstanceState()
sa pokúsime uložiť úlohu, a zároveň získať jej IDčko (alebo aktualizovať úlohu, ak náhodou bola uložená v metóde onPause()
a ihneď na to ju uložíme do bundle.
@Override
protected void onPause() {
super.onPause();
saveTask();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveTask();
outState.putSerializable(TASK_BUNDLE_KEY, task);
}
Všimnime si, že ukladanie sa niekedy môže diať dvakrát (raz v jednej metóde, raz v druhej.) To je však omnoho menší problém než nečakaná strata dát.
V metóde je nutné volať rodičovskú implementáciu, ktorá je zodpovedná za ukladanie stavu komponentov majúcich identifikátor.
Naša aplikácia už dokáže dve zázračné veci: prezerať zoznam úloh a pridávať nové úlohy. Zaslúžila by si ešte dve činnosti: mazanie úloh, a ich aktualizáciu. (Nemáme napríklad možnosť označiť úlohu ako splnenú.)
Poďme sa tomu venovať.
Otázka je, ako vyvoláme úpravu konkrétnej položky? Môžeme to dosiahnuť jednoducho: kliknutím na položku zoznamu v hlavnej aktivite prejdeme do detailovej aktivity, kde môžeme vykonať požadované zmeny (Napríklad označíme úlohu ako splnenú.)
Obsluhu kliku na položku zoznamu ListView
dosiahneme pridaním poslucháča OnClickItemListener
cez metódu setOnItemClickListener()
. V metóde onCreate()
dodáme k zoznamu nasledovný kód:
tasksListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = new Intent(TaskListActivity.this, TaskDetailActivity.class);
startActivity(intent);
}
});
Takto vždy po kliknutí na položku zoznamu spustíme detailovú aktivitu. Všimnime si len syntaktickú drobnosť: pri vytváraní intentu potrebujeme uviesť kontext, ktorý je obvykle reprezentovanú samotnou aktivitou, reprezentovanou premennou this
. Metóda onItemClick()
sa však nachádza v anonymnej vnútornej triede poslucháča a this
zodpovedá práve objektu typu OnItemClickListener
, ktorý nie je kontextom. Ak chceme pristúpiť k inštancii vonkajšej triedy, musíme this
uviesť v kvalifikovanom tvare: TaskListActivity.this
.
Preklik do detailovej aktivity už funguje. Ako však zobrazíme údaje o úlohe, na ktorú sme naozaj klikli v zozname?
Máme situáciu, keď hlavná zoznamová aktivita potrebuje odovzdať detailovej aktivite informáciu o vybranej položke. Dosiahneme to pomocou intentu: ako sme spomínali vyššie, intent predstavuje správu medzi aktivitami, ktorá nielen uvádza čo máme v úmysle, ale dokáže niesť aj dáta.
Medzi našimi dvoma aktivitami tak vieme posielať identifikátor úlohy, ktorú chceme zobraziť. V metóde obsluhy kliknutia využijeme parameter position
(reprezentujúci index položky v zozname: druhá položka zoznamu tak bude mať index 3), zo zoznamu úloh vytiahneme úlohu na danej pozícii a jej identifikátor vložíme do intentu.
Vkladanie údajov do intentu sa realizuje pomocou metódy putExtra()
, ktorý funguje podobne ako v prípade hashmapy: pod daný kľúč vieme vložiť takmer ľubovoľné dáta.
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Task task = taskDao.list().get(position);
Intent intent = new Intent(TaskListActivity.this, TaskDetailActivity.class);
intent.putExtra("taskId", task.getId());
startActivity(intent);
}
Prijímanie údajov v detailovej aktivite môžeme dosiahnuť jednoducho: zavolaním metódy getIntent()
vieme získať Intent
, ktorý viedol k spusteniu aktivity a z neho vieme podľa potreby vytiahnuť aj dodatočné dáta z extras.
Z intentu tak vytiahneme identifikátor úlohy, pomocou neho vytiahneme z databázy príslušnú úlohu Task
, ktorú pôjdeme upravovať, a priradíme ju do inštančnej premennej Task
.
Musíme si však veľmi pozorne rozanalyzovať možné situácie a dať ich do súvislosti si životným cyklom. V opačnom prípade veľmi ľahko dôjde k strate dát.
Všetko závažné sa udeje v metóde onCreate()
:
task
.Task
u a vložíme ju do inštančnej premennej.Task
u.Pozor však na poradie! Úloha uložená v bundli má prednosť pred úlohou vytiahnutou z databázy na základe dát v intente! (Aktivita totiž môže prechádzať ničením a obnovovaním). Až keď sa nenájdu dáta v bundli, dívame sa do intentu. Priority teda sú: hľadanie v bundli, hľadanie v intente a núdzové vytváranie novej aktivity.
Kód v metóde onCreate()
bude teda nasledovný:
if(savedInstanceState != null) {
task = (Task) savedInstanceState.get(TASK_BUNDLE_KEY);
} else {
Long taskId = (Long) getIntent().getSerializableExtra(TASK_ID_EXTRA);
if(taskId != null) {
task = taskDao.getTask(taskId);
} else {
task = new Task();
}
}
taskNameEditText.setText(task.getName());
taskDoneCheckBox.setChecked(task.isDone());
Konštanty TASK_BUNDLE_KEY
(názov kľúča v bundli pre úlohu) a TASK_ID_EXTRA
(názov kľúča pre identifikátor úlohy v extra) sú definované takto:
public static final String TASK_BUNDLE_KEY = "task";
public static final String TASK_ID_EXTRA = "taskId";
V kóde sme použili jednu fintu: identifikátor vytiahneme pomocou metódy getSerializableExtra()
, ktorá vráti null
, ak sa objekt pod daným kľúčom v mape extras nevyskytuje.
Dopracujme teraz do detailovej aktivity možnosť odstrániť práve zobrazenú úlohu. Využijeme pri tom znalosti, ktoré poznáme: do lišty akcií uvedieme tlačidlo pre vymazanie, ktorým dosiahneme, čo treba. Bude pri tom drobný zádrheľ (so životným cyklom), ale veľmi jednoducho ho vyriešime.
Do layoutu lišty (súbor menu_task_detail.xml
) dodajme jednu položku:
<item android:id="@+id/deleteTaskMenu"
android:title="Odstrániť"
app:showAsAction="always"
android:icon="@android:drawable/ic_menu_delete"
/>
Položka bude vždy zobrazená ako tlačidlo a pridelíme jej systémovú ikonku so smetným košom.
Obsluha tlačidiel na action bare bude jednoduchá: klik na položku odstráni položku z databázy a ihneď ukončí aktivitu pomocou finish()
.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.deleteTaskMenu) {
taskDao.delete(task);
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_task_detail, menu);
return true;
}
Ak to však vyskúšame, zistíme, že to prakticky nefunguje. Prečo?
Pri ladení by sme zistili, že aktuálna úloha sa síce odstráni, ale nastane problém so životným cyklom: akonáhle aktivitu ukončíme pomocou metódy finish()
, vykoná sa metóda onPause()
, ktorá... áno, zase uloží aktuálnu aktivitu.
Vyriešiť to môžeme jednoducho: vytvoríme si booleovský príznak, napr. ignoreSaveOnFinish
, ktorý nastavíme na true
, ak budeme aktivitu ukončovať po mazaní.
private boolean ignoreSaveOnFinish;
Kód pre obsluhu tlačidla teda bude vyzerať:
if (id == R.id.deleteTaskMenu) {
taskDao.delete(task);
ignoreSaveOnFinish = true;
finish();
return true;
}
Zároveň upravíme kód pre saveTask()
, aby bral do úvahy príznak pri ukladaní:
private void saveTask() {
if(ignoreSaveOnFinish) {
ignoreSaveOnFinish = false;
return;
}
task.setName(taskNameEditText.getText().toString());
task.setDone(taskDoneCheckBox.isChecked());
taskDao.saveOrUpdate(task);
}
Dosiaľ sme sa rozprávali o štartovaní aktivít. Čo však s ich ukončovaním? Zatiaľ máme už tri mechanizmy na ukončenie aktivity:
finish()
. V tomto prípade je aktivita ukončená, a zavolajú sa príslušné metódy životného cyklu (onPause()
, onStop()
a onDestroy()
).finish()
, ale možno ho prekryť v metóde onBackPressed()
.Zistiť, či je aktivita ukončovaná pomocou metódy finish()
možno zistiť v iných metódach pomocou isFinishing()
. Takto možno pri ukladaní dát napríklad vyhodiť toast s nápisom Dáta boli uložené
.
Podobne možno vypísať toast aj pri ukončení aktivity cez tlačidlo Back v metóde onBackPressed()
.
Ak chceme obslúžiť spätnú šípku v lište akcií, môžeme to urobiť v metóde onOptionsItemSelected()
. Identifikátor tlačidla šípky je android.R.id.home
:
if (id == android.R.id.home) {
/* osetrime sipku */
return true;
}
Posledná vec, ktorú spravíme v aplikácii, je formátovanie ukončených položiek. Zatiaľ sa hotové úlohy zobrazujú s textom Hotové, ale určite by bolo krajšie, keby sme hotové úlohy zobrazovali radšej preškrtnutým textom.
Ak používame ArrayAdapter
, stačí prekryť metódu getView()
. Vždy, keď potrebuje adaptér vykresliť položku, zavolá práve túto metódu, v ktorej je potrebné vytvoriť komponent obsahujúci položku a prepojiť ho s dátami z adaptéra.
Štandardná implementácia využívajúca layout simple_list_item_1
vytvorí pre každú položku jeden TextView
, vezme položku z poľa (v tomto prípade Task
), zavolá na nej toString()
a výsledok vloží do TextView
.
V našom prípade stačí získať tento TextView
z rodičovskej implementácie, okrem toho získať inštanciu Task
˛u, a ak je úloha Task
splnená, nastavíme vykresľovanie TextView
u tak, aby sa využil preškrtnutý text.
ArrayAdapter<Task> taskAdapter = new ArrayAdapter<Task>(this, android.R.layout.simple_list_item_1, taskDao.list()) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView listItemView = (TextView) super.getView(position, convertView, parent);
Task task = getItem(position);
if(task.isDone()) {
listItemView.setPaintFlags(listItemView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
listItemView.setText(task.getName());
return listItemView;
}
};
Preškrtnutý text dosiahneme cez tzv. príznaky vykresľovania, paint flags. Nastavovanie príznakov sa rieši starou školou a la C: použitím bitového maskovania. Nám stačí vedieť, že vytiahneme existujúce príznaky a pomocou bitového OR (|
) pridáme príznak preškrtnutého textu STRIKE_THRU_TEXT_FLAG
.
V poslednom kroku nastavíme text na TextView
na základe popisku úlohy a celý komponent vrátime von z metódy.
Poslednú vec, ktorú využijeme, je logovanie. Mnohokrát chceme sledovať beh našej aplikácie a činnosti, ktoré sa vykonávali a mať o tom prehľad pre prípady, že nastane chyba. Klasický spôsob logovania je využiť konzolu a System.out.println()
, ktorý prekvapivo funguje i napriek tomu, že telefón žiadnu konzolu neobsahuje. Všetky výsledky sa vypisujú do logcat
, ktorý možno vidieť v Studiu.
Lepšia alternatíva je využiť zabudovaný logovací objekt Log
, ktoré podporujú viacero úrovní logovania (od ladiacich hlášok až po kritické varovania. Každej hláške možno tiež priradiť tag, teda ľubovoľný dodatočný popis, podľa ktorého sa dá v logcat
e filtrovať.
Klasické použitie je:
Log.d(getClass().getName(), "Task " + task + " was saved");
V tomto prípade je tagom názov triedy, hláška sa loguje na úrovni debug
(d
ako debug
).
Logovacie hlášky majú úrovne:
v
: verbose, trasovacie hláškyd
: debug, ladiace hláškyi
: info, informačné hláškyw
: warn, varovné hláškye
: error, chybya komická úroveň:
wtf
: pre situácie, ktoré by nemali nikdy nastať.