3. stretnutie: Taskr

Projekt Debilníček / ToDo list / Taskr

Vytvoríme projekt o dvoch aktivitách: jedna obsahuje zoznam úloh, druhá detaily aktivity. Umožníme vytvárať nové upravovať existujúce úlohy.

Cieľové elementy

  • Aplikácia s dvoma aktivitami.
  • Práca so zoznamami ListView v aktivite
  • Adaptéry ako modely dát pre widgety: príklad ArrayAdaptera.
  • Použitie lišty akcií ActionBar a menu aktivity.
  • Spustenie novej aktivity. Intenty.
  • Spustenie aktivity s očakávaním výsledku.
  • Akcie nad položkou zoznamu: obsluha výberu položky.
  • Výmena informácií medzi aktivitami pomocou intentov.
  • Kontextové menu nad položkou.
  • Logovanie.

Predpripravené súbory

Prvá aktivita: zoznam úloh

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!

Entity a DAO

Entita

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:

  • vráti všetky úlohy
  • nájde úlohu podľa ID
  • uloží (pridá) úlohu s daným ID

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.

Použitie v aplikácii

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

}

Druhá aktivita: detail úlohy

Prispôsobme potom v dialógovom okne našu novú aktivitu:

  • ActivityName: názov triedy pre aktivitu, v tomto prípade TaskDetailActivity.
  • Layout Name: aktivita dostáva svoj vlastný layout
  • Title: čitateľný titulok pre koncového používateľa
  • Menu Resource Name: aktivita dostáva tiež svoje vlastné menu
  • Launcher Activity: začiarknuté len pre hlavnú aktivitu, čo nie je tento prípad
  • Hierarchical Parent: umožní nastaviť inú triedu inej aktivity, ktorá bude považovaná za logického rodiča v hierarchickej navigácii. V tomto prípade ňou pokojne môže byť aktivita so zoznamom TaskListActivity.
  • Package Name: názov balíčka pre triedu novej aktivity

Layout detailovej aktivity

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.

Atribúty v hlavičke

Každý layout má tiež svoje vlastné atribúty. Poďme si ich popísať:

  • samotný layout sa tvári ako komponent, ktorý zaberá istú výšku a šírku. Hodnota 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.
  • padding znamená vypchávka (ako v CSS, teda vzduch medzi okrajom a vnútrom elementu. Nastavíme preddefinované konštanty @dimen/activity_vertical_margin, ktoré nastavia "vhodné" množstvo vzduchu.
  • stretchColumns umožňuje jednoducho nastaviť stĺpec, ktorý sa bude automaticky naťahovať do šírky, ak je na displeji dosť miesta. V tomto prípade chceme naťahovať druhý stĺpec, teda stĺpec s indexom 0.
  • ostatné atribúty sú buď internou záležitosťou Androidu (tools:context) alebo vlastnosťou XML dokumentu (xmlns:android a xmlns:tools).

Spustenie novej aktivity a intenty

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.

Intenty

Presnejšie povedané, intent predstavuje správu medzi dvoma aktivitami, ktorá v sebe nesie dve zložky:

  • akú akciu má používateľ v úmysle vykonať (čo chcem dosiahnuť)
  • a aké dodatočné dáta sú potrebné na výber správnej aktivity (aké dodatočné dáta v požiadavke nastavím?)

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:

  • akciu, v tomto prípade všeobecnú systémovú akciu odošli správu (možno mailovú, možno SMS)
  • dáta, reprezentované hashmapu, kde pod systémový kľúč uvedieme pole adresátov.
  • intent odošleme do systému metódou 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.

Príklad intentu: spustenie prehliadača

Ď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

Spustenie novej aktivity

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

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:

Definícia tlačidiel action baru

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.

Oživenie tlačidla v action bare

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 megaifu 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.

Spustenie druhej aktivity.

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

Pridávanie nových úloh

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.

Úloha je modelom

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.

Životný cyklus časť 3: 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...

Nedokonalý cyklus

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

Čo sa presne deje?

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:

  • Prípad 1: onPause() sa volá skôr než onSaveInstanceState()
    • v metóde onCreate() sa vytvorí nová inštancia Tasku
    • používateľ otočí telefón
    • zavolá sa metóda onPause(), kde sa úloha task uloží a trieda TaskDao jej pridelí identifikátor
    • zavolá sa metóda onSaveInstanceState(), kde sa úloha (spolu s ID) uloží do bundle
    • aktivita je zničená a vytvorí sa nanovo
    • zavolá sa metóda onCreate(), kde príde bundle, z ktorého sa vytiahne úloha task s ID
    • ak používateľ opustí aktivitu, opäť sa zavolá metóda onPause(), kde sa už len aktualizuje task v rámci metódy saveOrUpdate()
  • Prípad 2: onSaveInstanceState() sa volá skôr než onPause()
    • v metóde onCreate() sa vytvorí nová inštancia Tasku
    • používateľ otočí telefón
    • zavolá sa metóda onSaveInstanceState(), kde sa úloha (spolu s ID) uloží do bundle
    • zavolá sa metóda onPause(), kde sa úloha task uloží a trieda TaskDao jej pridelí identifikátor.
    • už tuto nastáva chyba, pretože v bundli je úloha bez identifikátora!

Ukladanie priebežného i perzistentného stavu

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.

Úprava úloh a komunikácia medzi aktivitami

Spustenie novej aktivity

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?

Komunikácia medzi aktivitami

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.

Odosielanie údajov z aktivity

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 aktivite

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():

  • ak je aktivita vyvolaná v editačnom režime, máme v intente identifikátor úlohy. Vytiahneme z databázy úlohu, a priradíme ju do premennej task.
  • ak nemáme v intente identifikátor úlohy, aktivita je vyvolaná v režime vytvárania. V takom prípade máme dva podprípady:
    • aktivita bola zastrelená a obnovuje sa, pričom máme k dispozícii bundle. Z bundlu tak vytiahneme inštanciu Tasku a vložíme ju do inštančnej premennej.
    • aktivita sa vytvára nanovo, čo znamená, že vytvárame novú inštanciu Tasku.

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.

Odstraňovanie úloh

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

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

Manuálne ukončenie aktivity

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:

  • ukončenie aktivity z kódu cez finish(). V tomto prípade je aktivita ukončená, a zavolajú sa príslušné metódy životného cyklu (onPause(), onStop() a onDestroy()).
  • ukončenie aktivity pomocou hardvérového tlačidla Back. Štandardné správanie ukončí aktivitu pomocou finish(), ale možno ho prekryť v metóde onBackPressed().
  • ukončenie aktivity pomocou spätnej šípky v lište akcií. Ak má aktivita definovaného rodiča, pri štandardnom správaní sa zjaví na lište akcií spätná šípka, ktorá vráti používateľa do koreňovej (hlavnej) aktivity aplikácie a aktuálnu aktivitu ukončí.

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

Prispôsobenie formátovania položiek

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 TextViewu 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.

Logovanie

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 logcate 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ášky
  • d: debug, ladiace hlášky
  • i: info, informačné hlášky
  • w: warn, varovné hlášky
  • e: error, chyby

a komická úroveň:

  • wtf: pre situácie, ktoré by nemali nikdy nastať.

Ďalšie materiály

Hotový projekt

Iné články