Vyvíjíme pro Android: Dialogy a activity

V dnešním díle seriálu Vyvíjíme pro Android se naučíme pracovat s dialogy, popíšeme si životní cyklus Activit a Fragmentů a ukážeme si, jak uchovat jejich stav, přestože je konkrétní objekt z paměťových důvodů zničen.

Seriál: Vyvíjíme pro Android (14 dílů)

  1. Vyvíjíme pro Android: Začínáme 15.6.2012
  2. Vyvíjíme pro Android: První krůčky 22.6.2012
  3. Vyvíjíme pro Android: Suroviny, Intenty a jednotky 29.6.2012
  4. Vyvíjíme pro Android: Bližší pohled na pohledy – 1. díl 13.7.2012
  5. Vyvíjíme pro Android: Bližší pohled na pohledy – 2. díl 13.7.2012
  6. Vyvíjíme pro Android: Fragmenty a SQLite databáze 20.7.2012
  7. Vyvíjíme pro Android: Preference, menu a vlastní Adapter 27.7.2012
  8. Vyvíjíme pro Android: Intenty, intent filtry a permissions 3.8.2012
  9. Vyvíjíme pro Android: Content providery 10.8.2012
  10. Vyvíjíme pro Android: Dialogy a activity 17.8.2012
  11. Vyvíjíme pro Android: Stylování a design 24.8.2012
  12. Vyvíjíme pro Android: Notifikace, broadcast receivery a Internet 31.8.2012
  13. Vyvíjíme pro Android: Nahraváme aplikaci na Google Play Store 7.9.2012
  14. Vyvíjíme pro Android: Epilog 14.9.2012

Minulý článek se věnoval content providerům. Řekli jsme si, co to vlastně je, naučili jsme se je používat a nakonec i vytvářet. Předělali jsme aplikaci Poznámkový blok z dílu Fragmenty a SQLite databáze, v níž jsme kromě content providerů použili i znalosti o intentech a intent filtrech nabyté ještě o týden dříve. Dnes se budeme věnovat dialogům a zároveň s tím si ukážeme životní cyklus activit a fragmentů, povíme si, jaké úkony se kdy provádějí, a naučíme se uchovávat stav activity či fragmentu i například v případě, kdy Android z paměťových důvodů zničí jejich, v danou chvíli nepoužívanou, instanci.

Životní cyklus Activity

Při programování konzolové javové aplikace musíte u hlavní třídy implementovat statickou metodu main. Ta se spustí po spuštění aplikace a obstará vytvoření všech potřebných tříd, spuštění důležitých služeb, prostě všechno. Pokud nepočítáme vynucené ukončení programu uživatelem, výjimky a další chyby, to, jak a jak dlouho program poběží, závisí právě na metodě  main.

Android je v tomhle naopak velmi dominantní a má pocit, že může všem diktovat, co a jak mají kdy dělat (on v podstatě může…). Proto se v něm žádné metody main nenacházejí. On se sám postará o vytvoření instancí (nejen) activit, stejně tak jako o jejich zničení. A v průběhu života určuje (nejen) activitám, co mají dělat.

To jim určuje pomocí metod životního cyklu. Ty mají přesně dané pořadí, v jakém se budou spouštět, a stejně tak situaci, při které se spustí. Více nám snad napoví následující diagram:


Autor: Android documentation

Životní cyklus Activity

Ještě než začnu popisovat jednotlivé metody, rozdělíme si (po vzoru dokumentace activit, která je mimochodem moc hezky udělaná a umíte-li anglicky, doporučuji ji přečíst, určitě dobře doplní můj výklad a pomůže ho lépe pochopit) její životní cyklus na tři stavy:

Běžící
Activity je v popředí a dostává informace o uživatelském vstupu. Všechny Activity, s nimiž jako uživatel pracujete, jsou ve stavu běžící (running či resumed).
Přestávka
Když si Activity bere přestávku (je paused), znamená to, že je částečně překrytá jinou Activitou (nikoli svým vlastním dialogem!) nebo že je nad ní jiná Activity, která je však průhledná. Activity je o přestávce tedy sice (částečně) vidět, ale nedostane se k ní žádný uživatelský vstup. Při opravdu kritickém nedostatku paměti může být objekt takovéto Activity zničen (a posléze případně znovu obnoven, o tom později).
Zastavená
Activity je zastavená tehdy, když není vidět, ale framework ještě nezničil její objekt (tzn. má dostatek místa v paměti a je reálně možné, že se bude uživatel k dané Activity chtít vrátit). Samozřejmě ani nepřijímá uživatelský vstup.

Při změně stavů se volají jednotlivé metody životního cyklu.

Metody životního cyklu Activity

Když se androidí framework se dozví, že má být spuštěna nějaká Activity (uživatel klepl na ikonu v launcheru, vybral ji jako cíl implicitního intentu, atd.), vytvoří objekt oné Activity, zařídí všechno potřebné okolo (nový proces, …) a zavolá metodu onCreate(). Ta přebírá nějaký parametr, o tom později. V onCreate()  si Activity nastaví content view a vytvoří všechny objekty, které bude potřebovat po dobu své existence. Neměla by například spouštět vlákna, která budou nějakým způsobem upravovat UI (ale vlákna, která například stahují nějaká data, spouštět může).

Po zavolání onCreate() je Activity neviditelná a nepřijímá uživatelský vstup, je zastavená.

Téměř všechny metody životního cyklu musí zavolat i příslušnou metodu na rodiči své třídy. U tříd, které to dělat nemusí (a ono to je vždy celkem logické), to uvedu.

Ihned po onCreate() se volá metoda onStart(). V onStart()  by se měly provést všechny úkony, které jsou potřeba, aby se Activity mohla zobrazit. Například můžete zaregistrovat broadcast receiver, který hlídá změny dat zobrazovaných tou Activity, nebo spustit vlákno, které pravidelně stahuje data z internetu a v Activity je zobrazuje.

Po zavolání onStart() má Activity přestávku. V naprosté většině případů bude následovat metoda onResume(), ale někdy (pokud se při běhu onStart() ukáže, že spuštěná Activity už vlastně není potřeba) může následovat  onStop().

Metoda onResume() provází Activity koncem přestávky a začátkem běhu, to znamená, že po jejím provedení je Activity vidět a přijímá vstup. Metoda onResume() může být volána docela často, takže by se v ní neměly provádět žádné náročné operace. Je používána například k nastavení posluchače nějakému senzoru – získávání dat ze senzorů je poměrně náročné na baterku, proto se omezuje na nezbytně nutnou dobu.

Po onResume() přichází vždy onPause(). Ta znamená, že má Activity opět přestávku. Kromě zrušení všech akcí provedených v onResume() (vlastně každá metoda životního cyklu (s výjimkou onRestart()) má svou protikladnou metodu) je onPause() také ideální kandidát na uložení uživatelem provedených akcí na datech, která se netýkají jen té konkrétní instance Activity. (Jednoduše řečeno, pokud máte Activity na vytvoření/upravení poznámky a nechcete tam dávat tlačítko na uložení – které je na Androidu opravdu poněkud nepatřičné –, metoda onResume() je ideálním kandidátem na jejich uložení do databáze. Zatímco na uchování informace, kterou položku má uživatel zrovna rozkliknutou, slouží jiné metody.)

Před Honeycombem mohla být instance Activity po zavolání metody onPause() při opravdu kritických situacích zničena (pro nás to znamená právě jen tu nutnost ukládat uživatelovu práci v onPause()). Ale obvykle po ní následuje buď opět onResume(), pokud se Activity zase dostala do popředí, anebo metoda  onStop().

onStop() je protikladná metoda onStart(), Activity z přestávky přechází na stav zastavená. Při ní tedy zrušíme všechno, co jsme v onStart()  vytvořili.

Po onStop() může být instance Activity zničena (nijak se tím nezatěžujte), nebo přijít buď metoda onRestart(), které hned po sobě zavolá onStart(), anebo metoda  onDestroy().

V metodě onDestroy() (protikladu onCreate()) byste po sobě měli uklidit to, co jste vytvořili v onCreate() (třeba běžící vlákna).

onDestroy() může být zavolána ze dvou důvodů. Buď Activity končí (tzn. byla zavolána metoda finish(), ať už explicitně, anebo proto, že uživatel zmáčkl tlačítko zpět a v zásobníku bylo nahoře spuštění ukončované Activity), anebo ji androidí framework ničí dočasně, aby uvolnil místo v paměti. Tyto dva stavy můžete rozlišit metodou isFinishing(), ale nemyslím si, že to někdy budete potřebovat.

Ukládání stavu Activity

Teď se nebudeme bavit o datech, s nimiž Activity jen pracuje, tedy například rozepsaný e-mail, upravovaný kontakt atp., ale o datech, která si sama vytvoří a potřebuje je jen pro sebe. Například zadaný příklad v kalkulačce, rozkliknutá záložka s textem či vyhledané slovo ve slovníku.

Je totiž hrozně fajn, když se poté, co si zatelefonujete, napíšete SMS anebo třeba odskočíte zahrát hru (ale předtím používanou Activity jen upozadíte tlačítkem home) a pak se znovu vrátíte k původnímu úkolu, zobrazí vše tak, jak bylo (zadaný příklad v kalkulačce, rozkliknutá záložka s textem či vyhledané slovo ve slovníku). Protože Android může instance Activit libovolně ukončovat, na jejich zachování se spolehnout nemůžeme. Mohli bychom všechna tato data uložit do databáze, třeba v metodě onStop(), ale kromě toho, že je to strašně složité, bychom narazili na problém, neboť jedna Activity může mít najednou spuštěných (nebo „spuštěných”, tzn. že instance ve skutečnosti neexistují, ale je zachován jejich stav) více instancí (například fotoaparát může mít jednu, kterou jste spustili z domovské obrazovky a v níž zrovna provádíte různá nastavení, a druhou, kterou spustila nějaká Activity, jež po vás chce, abyste pořídili obrázek vaší kočky.

Naštěstí nám na to nabízí Android řešení, a tím je hlavně metoda onSaveInstanceState(Bundle outState). Ta jako parametr přebírá Bundle, do něhož můžete libovolně vložit ( putBoolean(), putString() atd.) nějaká data a Android je za vás spojí s konkrétní instancí Activity a uloží je i přesto, že je instance Activity zničená. Když se má potom ta instance Activity použít, znovu se vytvoří a metodám onCreate() a onRestoreInstanceState() se předá předtím vytvořený Bundle (pokud žádný předtím vytvořený nebyl, onCreate() dostane null a onRestoreInstanceState() by se neměla zavolat).

V naprosté většině případů metodu onRestoreInstanceState() (volá se po onStart()) používat nebudete a vystačíte si s  onCreate().

Často dokonce nebudete muset používat ani  onSaveInstanceState(). Androidí implementace Activity.onSaveInstanceState() (určitě zase musíte zavolat super.onSaveInstanceState(outState)) zavolá tuhle metodu na všech svých view, a pokud mají nastavené id, umějí svůj stav uložit samy. Takže o EditText-y aj. se starat nemusíte.

Teď víte všechno o životním cyklu Activit. Respektive, všechno nevíte. Existuje například metoda onPostCreate(), která se zavolá těsně po onCreate(). Ale ta je hlavně pro účely samotného frameworku.

Životní cyklus Fragmentů

Než se vrhneme na dialogy a programování, povím vám ještě o životním cyklu Fragmentů. Ty mají stejné metody jako Activity, ale k tomu ještě něco navíc:


Autor: Android documentation

Životní cyklus Fragmentu. To, že nejsou šipky například od onPause() k onResume()  neznamená, že by byl životní cyklus Fragmentu přímočarý.

 

Metoda onAttach() se zavolá, když je Fragment spojen s nějakou Activity (ta je mu předána jako argument). V této metodě, která se volá ještě před fragmentím onCreate() , jsme prováděli kontrolu, zda Activity implementuje požadované rozhraní.

onCreateView() se volá po onCreate() a musí vrátit View, které bude v uživatelském rozhraní Fragment reprezentovat (ve skutečnosti nemusí, Fragment může být i bez rozhraní jako nějaký pracant na pozadí (background worker), ale tehdy musí být přidán v kódu, a abych se přiznal, nevím, kdy bych něco takového chtěl použít). onCreateView() je tou metodou, u níž není potřeba volat předka (ale můžete to udělat, třeba u ListFragment-u něco provést, ale vrátit výchozí layout).

onActivityCreated() se volá po onCreateView() tehdy, když skončilo onCreate() Activity, která Fragment obsahuje. Pokud byl Fragment přidán v kódu dodatečně a onCreate() Activity dávno doběhlo, zavolá se bezprostředně po  onCreateView().

V onDestroyView(), volaném po onStop(), pokud má být Fragment zničen, bychom měli uklidit všechno, co jsme vytvořili v onCreateView()  a nejsou to samotná View, která uklidí framework sám.

onDetach() se volá po onDestroy() a je to protiklad  onAttach().

Ukládání stavu Fragmentu

Ukládání stavu Fragmentu je velmi podobné Activity, v podstatě se jen uložené Bundle předává více metodám ( onCreate(), onCreateView() a onActivityCreated()) a naopak neexistuje  onRestoreInstanceState().

Dialogy

Tohle už bude jen stručná teorie předtím, než začneme programovat a dialogy i životní cykly si procvičíme v praxi.

Dialog je…prostě dialog. Slouží například k potvrzení nějaké destruktivní akce, výběru jedné položky z mnoha, zobrazení progress baru anebo zobrazení nějakých informací, na něž nestačí toast. Pomocí dialogů je také řešeno nastavování některých preferencí.

Takhle může vypadat dialog.

 

Dialog je do jisté míry samostatná entita, ale zároveň žije spolu s Activity, která se o něj musí starat a musí ho zničit, když je ničena sama. Proto nestačí vytvořit objekt Dialog a zavolat na něm metodu show() (respektive stačilo by, ale pak byste se o něj museli starat sami).

Dialog

Před Androidem 3.0 se to řešilo tak, že se každému dialogu přiřadil unikátní číselný identifikátor a když jsme ho chtěli zobrazit, zavolali jsme Activity.showDialog(int id), což způsobilo spuštění metody onCreateDialog(), v níž se vytvořil Dialog podle předaného id, pak ho metoda vrátila a Android se o něj postaral. A ještě existovala metoda onPrepareDialog(). Zatímco onCreateDialog() se zavolala jednou a pak, přestože byl třeba schovaný, uchovával se objekt Dialog v paměti, onPrepareDialog() se volala znovu při každém novém zobrazení toho dialogu.

Sice píšu v minulém čase, ale přesto vás s tímhle způsobem musím seznámit, neboť je naprosto běžný u starších, nefragmentových aplikací, ale bohužel i u některých fragmentových.

DialogFragment

Správně se to ale od Androidu 3.0 dělá jinak, fragmentově.

Vychází se z toho, že s Fragmenty umí Activity pracovat velmi dobře, a tak se nemusí starat ještě o další typ objektů. Vznikla tak třída DialogFragment (pěkná dokumentace, vedle Using DialogFragments nejlepší zdroj informací). Ta obohacuje třídu Fragment o schopnost zobrazit se jako dialog (ale neztrácí schopnost zobrazit se normálně jako view).

Vytvoříte si tedy vlastní třídu, budete dědit od DialogFragment, ale jinak se můžete chovat, jako byste dělali s normálním Fragmentem. Rozdíl je ve způsobu zobrazení. Zatímco u normálního Fragmentu vytvoříte FragmentTransaction, provedete změny a pak to commitnete, DialogFragment nabízí metody show(FragmentManager fm, String tag) potažmo show(FragmentTransaction ft, String tag), které ho zobrazí jako dialog. Zároveň máte k dispozici metodu getDialog(), jež vrátí objekt Dialog, na němž můžete například nastavit titulek. Pokud není Fragment zobrazen jako dialog, ale normálně (viz getShowsDialog()), vrátí  null.

Teď ale zase vznikl problém, že existovalo plno tříd, které dědily od Dialog-u a implementovaly vlastní dialogy, a teď by najednou byly k ničemu. Proto DialogFragment obsahuje i metodu onCreateDialog(), v níž můžete vytvořit objekt Dialog a vrátit ho – on se zobrazí (tím však váš DialogFragment ztrácí schopnost zobrazit se jako normální Fragment).

To by byl stručný úvod do dialogové problematiky (nebojte, pokud to nechápete, pochopíte při programování), ale ještě než se vrhneme na programování, představím vám vestavěné  Dialog-y.

Ty jsou mnohem detailněji popsány také v Dialogs, včetně odkazů na další zdroje.

AlertDialog

Třída AlertDialog je nejčastější dialog, který umí zobrazit nějaký text, až tři tlačítka a případně seznam nějakých řetězců s tím, že uživatel vybere jeden klepnutím, tentýž seznam, akorát s RadioButton-y, anebo dotřetice takový seznam, ale s možností výběru více položek pomocí CheckBox-ů. My si ukážeme jeden s textem a tlačítky a jeden s jednoduchým seznamem, jak používat ostatní určitě pochopíte z dokumentace.

AlertDialog má ještě jedno specifikum, a to třídu AlertDialog.Builder. Ta umožní pomocí fluent interface vytvořit takový AlertDialog, jaký potřebujete.

ProgressDialog

ProgressDialog slouží ke zobrazení progress baru a my se mu věnovat nebudeme, stejně jako dalším následujícím dialogům.

DatePickerDialog

Jak už název napovídá, DatePickerDialog umí vybrat datum.

TimePickerDialog

TimePickerDialog slouží k vybrání času.

ColorPickerDialog

ColorPickerDialog není v Androidu vestavěný, ale protože vím, že když jsem ho potřeboval na svůj Game of Life Wallpaper, hledal jsem ho dlouho, odkážu vás na hezký funkční AmbilWarna Color Picker.

Programujeme

Naše ukázková aplikace se bude skládat z jedné fragmentové Activity, která bude v levém sloupci obsahovat ListFragment se seznamem dialogů a bude mít volitelný pravý sloupec. Tato Activity také implementuje všechny metody životního cyklu, v nichž zobrazí toast s názvem metody, která zrovna probíhá (a to samé zapíše i do logu). Protože je ale opruz mít tolik toastů, nad ListFragment-em nabídne ještě tlačítko, které umožní vypnout (resp. zapnout) zobrazování toastů. To, jestli jsou toasty vypnuté, si bude Activity pamatovat, i když bude její instance fakticky zničena a pak znovu obnovena.

Vždycky, když se změní orientace obrazovky, je viditelná Activity zničena a znovu vytvořena, přičemž se Android pokusí zachovat její stav. Tím tedy můžeme jednoduše vyzkoušet, zda nám uchovávání stavu funguje.

Dialogy nabídneme tři. První bude klasický AlertDialog nabízející vypnutí Activity, který zobrazíme starým způsobem. Potom si vytvoříme DialogFragment, který ale bude obalovat zase jen AlertDialog, tentokrát s výběrem nejoblíbenějšího zviřátka. Nakonec naprogramujeme DialogFragment s vlastním View, který na jednosloupcovém layoutu zobrazíme jako dialog a na dvousloupcovém ho dáme do druhého sloupce. Na tomto fragmentu si zároveň vyzkoušíme životní cyklus fragmentů.

DialogsListFragment

Začneme nejjednodušší částí, a to DialogsListFragment-em. Ten dědí od ListFragment-u, zobrazuje seznam dialogů a umožňuje uživateli si vybrat. To, co se má udělat s tím, co uživatel vybere, nechá na Activity:

public class DialogsListFragment extends ListFragment {

    private String[] items = {
            "OutdatedAlertDialog",
            "ModernAlertDialog",
            "SampleDialogFragment"
    };

    private int[] ids = {
            OUTDATED_ALERT_DIALOG,
            MODERN_ALERT_DIALOG,
            SAMPLE_DIALOG_FRAGMENT
    };

    public static final int OUTDATED_ALERT_DIALOG = 0;
    public static final int MODERN_ALERT_DIALOG = 1;
    public static final int SAMPLE_DIALOG_FRAGMENT = 2;

    OnDialogsListItemClickListener listener;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        try {
            listener = (OnDialogsListItemClickListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement OnDialogsListItemClickListener");
        }
    }

    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_1, items));
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        listener.onClick(ids[position]);
    }

    public static interface OnDialogsListItemClickListener {
        public void onClick(int dialog);
    }
}

DialogyActivity

DialogActivity dědí od FragmentActivity (všechny Fragmenty i DialogFragmenty jsou zase ze support library!). Má trochu složitější layout, ale už jsme jednou takový dělali, takže pokud tomu nebudete rozumět, mrkněte na odkázaný článek.

Soubor /res/layout/single_column_main.xml obsahuje View společná pro jednosloupcovou i dvousloupcovou variantu, tedy v podstatě levý sloupec, což znamená tlačítko na přepnutí zobrazování toastů a potom kontejner pro  DialogsListFragment:

<?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" >

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:onClick="toggleToasts"
        android:text="@string/toggle_toasts" />

    <FrameLayout
        android:id="@+id/left_column"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

Do souboru /res/layout/main.xml vlastně jen umístíme single_column_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <include layout="@layout/single_column_main" />

</FrameLayout>

A nakonec soubor /res/layout-w660dp/main.xml (tu složku si vytvořte a klidně můžete místo w660dp použít land, abyste mohli i na telefonu překlopením otestovat dvousloupcový layout, jen pozor na problém se SampleDialogFragment-em). Ten jen definuje dva sloupce, do levého vkládá single_column_main.xml, pravý nechává volný:

<?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="horizontal" >

    <FrameLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" >

        <include layout="@layout/single_column_main" />
    </FrameLayout>

    <FrameLayout
        android:id="@+id/right_column"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

</LinearLayout>

Samotná třída DialogyActivity také není složitá:

V onCreate()  zkusí získat uloženou informaci o tom, zda má zobrazit toasty, a když už pracuje se savedInstanceState, tak do logu zobrazí jeho obsah. Kromě toho nastaví content view, přidá do levého sloupce DialogsListFragment (které definované v layoutu působilo problémy tehdy, když byl jako dialog zobrazen SampleDialogFragment a změnila se orientace displeje nebo se všechny třídy zničily a znovu vytvořily z jiných důvodů). Potom zobrazí (možná) toast o tom, že jsme v metodě onCreate() a zjistí, jestli má k dispozici pravý sloupec. Ještě předtím jsou definovány čtyři konstanty, které nás v tuto chvíli nezajímají:

public class DialogyActivity extends FragmentActivity implements
        OnDialogsListItemClickListener {

    private static final String KEY_SHOW_TOASTS = "showToasts";
    private static final String MODERN_ALERT_DIALOG = "modernAlertDialog";
    private static final String SAMPLE_DIALOG_FRAGMENT = "sampleDialogFragment";
    private static final int SIMPLE_DIALOG = 0;

    private boolean showToasts = true;
    private boolean dualPane;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_TOASTS)) {
            showToasts = savedInstanceState.getBoolean(KEY_SHOW_TOASTS);
            Log.d("keys", Arrays.toString(savedInstanceState.keySet().toArray(new String[0])));
        }

        setContentView(R.layout.main);

        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.add(R.id.left_column, new DialogsListFragment());
        ft.commit();

        showToast("onCreate");

        dualPane = findViewById(R.id.right_column) != null;
    }

Po metodě onCreate() přicházejí na řadu implementace dalších metod životního cyklu, které ale jen zobrazují informaci o tom, že jsou:

    @Override
    protected void onRestart() {
        super.onRestart();
        showToast("onRestart");
    }
    @Override
    protected void onStart() {
        super.onStart();
        showToast("onStart");
    }
    @Override
    protected void onResume() {
        super.onResume();
        showToast("onResume");
    }
    @Override
    protected void onPause() {
        super.onPause();
        showToast("onPause");
    }
    @Override
    protected void onStop() {
        super.onStop();
        showToast("onStop");
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        showToast("onDestroy");
    }

Pak je implementována metoda onSaveInstanceState(), která uloží showToasts a metoda onRestoreInstanceState(), opět jen logovací:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putBoolean(KEY_SHOW_TOASTS, showToasts);

        super.onSaveInstanceState(outState);

        showToast("onSaveInstanceState");
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState){
        super.onRestoreInstanceState(savedInstanceState);
        showToast("onRestoreInstanceState");
    }

Metody showToast() a toggleToasts() jsou jednoduché:

    public void toggleToasts(View v) {
        showToasts = !showToasts;
    }

    private void showToast(String text) {
        if (showToasts) {
            Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        }
        Log.d("DialogyActivity", text);
    }

onClick() je metoda vynucená OnDialogsListItemClickListener-em. Zatímco showDialog() je metoda vestavěná, showModernAlertDialog() a showSampleDialogFragment() si budeme muset vytvořit sami:

    public void onClick(int dialog) {
        switch (dialog) {
        case DialogsListFragment.OUTDATED_ALERT_DIALOG:
            showDialog(SIMPLE_DIALOG);
            break;
        case DialogsListFragment.MODERN_ALERT_DIALOG:
            showModernAlertDialog();
            break;
        case DialogsListFragment.SAMPLE_DIALOG_FRAGMENT:
            showSampleDialogFragment();
            break;
        }
    }

Pak přichází onCreateDialog(), kde používáme AlertDialog.Builder. To, myslím, nepotřebuje komentář:

    protected Dialog onCreateDialog(int id) {
        switch (id) {
        case SIMPLE_DIALOG:
            return new AlertDialog.Builder(this)
                    .setMessage(R.string.alert_dialog_basic_message)
                    .setTitle(R.string.alert_dialog_basic_title)
                    .setCancelable(false)
                    .setPositiveButton(R.string.switchoff,
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                        int id) {
                                    DialogyActivity.this.finish();
                                }
                            })
                    .setNeutralButton(R.string.maybe,
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                        int id) {
                                    if (Math.random() > 0.5) {
                                        DialogyActivity.this.finish();
                                    } else {
                                        dialog.cancel();
                                    }
                                }
                            })
                    .setNegativeButton(R.string.no,
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                        int id) {
                                    dialog.cancel();
                                }
                            }).create();

        default:
            return null;
        }
    }

ModernAlertDialogFragment používá na vytvoření dialogu metodu onCreateDialog(), takže ho můžeme zobrazit jen jako dialog:

    private void showModernAlertDialog(){
        new ModernAlertDialogFragment().show(getSupportFragmentManager(), MODERN_ALERT_DIALOG);
    }

Naopak SampleDialogFragment zobrazíme jako dialog jen tehdy, když nemáme k dispozici druhý sloupec. Pak už následují jen uzavírací závorky deklarace třídy  DialogsActivity:

    private void showSampleDialogFragment(){
        DialogFragment f = new SampleDialogFragment();
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        if(dualPane){
            ft.replace(R.id.right_column, f, SAMPLE_DIALOG_FRAGMENT);
            ft.commit();
        }else{
            f.show(ft, SAMPLE_DIALOG_FRAGMENT);
        }
    }
}

ModernAlertDialogFragment

ModernAlertDialogFragment je úplně triviální. V onCreateDialog  opět používá AlertDialog.Builder, tentokrát ale vytváří AlertDialog s výběrem nějakého řetězce (řetězce bychom správně měli všechny přesunout do surovin, ale myslím, že by to trochu zkomplikovalo chápání kódu):

public class ModernAlertDialogFragment extends DialogFragment implements OnClickListener {

    private String[] items = {
            "Želva",
            "Kůň",
            "Mravenečník",
            "Lenochod",
            "Kráva",
            "Žížala",
            "Komár",
            "Psobotnice"
    };

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new AlertDialog.Builder(getActivity())
                    .setItems(items, this)
                    .setTitle("Které je Tvoje nejoblíbenější zviřátko?")
                    .create();

    }

    public void onClick(DialogInterface dialog, int which) {
        Toast.makeText(getActivity(), "Tvoje nejoblíbenější zviřátko je: "+items[which], Toast.LENGTH_SHORT).show();
        this.dismiss();
    }
}

SampleDialogFragment

A nakonec SampleDialogFragment. Ten bude zobrazovat číslo ukazující, kolikrát na něj uživatel klepl. To číslo se zachová, i když se SampleDialogFragment zničí a pak znovu vytvoří. Kromě toho implementuje všechny metody životního cyklu, tentokrát bez toastů, jen logy:

public class SampleDialogFragment extends DialogFragment implements OnClickListener{

    private static final String KEY_CLICK_COUNT = "clickCount";
    private int clickCount = 0;

    private TextView clickCountDisplay;

    @Override
    public void onAttach(Activity activity){
        super.onAttach(activity);
        log("onAttach");
    }

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);

        if(savedInstanceState != null)
                clickCount = savedInstanceState.getInt(KEY_CLICK_COUNT, 0);

        log("onCreate");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        View v = inflater.inflate(R.layout.sample_dialog_fragment, container, false);

        v.setOnClickListener(this);
        clickCountDisplay = (TextView)v.findViewById(R.id.click_count_display);

        if(getDialog() != null)
            getDialog().setTitle(R.string.click);

        displayClickCount();

        log("onCreateView");

        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState){
        super.onActivityCreated(savedInstanceState);

        // savedInstanceState je ten stejný objekt Bundle jako v onCreate() a onCreateView()

        log("onActivityCreated");
    }

    @Override
    public void onStart(){
        super.onStart();
        log("onStart");
    }

    @Override
    public void onResume(){
        super.onResume();
        log("onResume");
    }

    @Override
    public void onPause(){
        super.onPause();
        log("onPause");
    }

    @Override
    public void onStop(){
        super.onStop();
        log("onStop");
    }

    @Override
    public void onDestroyView(){
        super.onDestroyView();
        log("onDestroyView");
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putInt(KEY_CLICK_COUNT, clickCount);

        super.onSaveInstanceState(outState);

        log("onSaveInstanceState");
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
        log("onDestroy");
    }

    @Override
    public void onDetach(){
        super.onDetach();
        log("onDetach");
    }



    private void displayClickCount(){
        clickCountDisplay.setText(String.valueOf(clickCount));
    }

    public void onClick(View v) {
        clickCount++;
        displayClickCount();
    }

    private void log(String text){
        Log.d("SampleDialogFragment", text);
    }
}

Layout SampleDialogFragment-u je jednoduchý:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="40dp" >

    <TextView
        android:id="@+id/click_count_display"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>

Problém se SampleDialogFragmentem

Není to žádný problém, který by nás měl vyloženě trápit. Jde jen o to, že pokud si zobrazíte SampleDialogFragment jako dialog a pak překlopíte displej, čímž splníte podmínku dvousloupcového layoutu (ať už dostatečnou šířku, anebo landscape, pokud jste to tak nastavili), zůstane zobrazený pořád jako dialog. A naopak, pokud ho zobrazíte jako druhý sloupec, pak překlopíte displej na výšku a druhý sloupec zmizí, SampleDialogFragment bude v paměti pořád jako obsah druhého sloupce (o čemž se můžete přesvědčit opětovným překlopením), jen nebude vidět.

Šlo by to vyřešit kontrolou v onCreate()  třídy  DialogsActivity, ale protože Fragment, který byl jednou dialogem, už nemůže být vložen normálně, musel by se SampleDialogFragment vytvořit znovu s počtem klepnutí předaným jako parametr, což by ale devalvovalo naši snahu o ukázku  onSaveInstanceState().

Tramtadadáá, aplikaci máme hotovou.

Procvičování

Zkuste vyřešit problém se SampleDialogFragmentem. Zkuste předělat Poznámkový blok tak, aby se přidání poznámky řešilo DialogFragmentem a ukládání pomocí  onPause().

Závěr

Dnes jsme se naučili pracovat s dialogy a konečně jsme si pořádně vysvětlili životní cykly Activit a Fragmentů. Zdrojové kódy dnešní aplikace si můžete stáhnout tady.

A náš seriál se pomalu blíží ke konci. Je o něco delší, než jsem původně předpokládal, a to jsem z něj vyškrtl některá témata. Snažím se totiž věnovat věcem, se kterými se každý z vás setká naprosto běžně. Exotičtější části androidího frameworku si nechám na případné samostatné články, pokud bude zájem.

Tip na konec

Přečtěte si Android Design Guidelines pro Dialogy (a nemusíte jen pro ně). V jednom z následujících dílů sice Guidelines nakousneme, ale jen zlehýnka.

Matěj začal programovat ve třinácti v PHP, pak v JavaScriptu a Lispu. Nakonec si koupil Androida, a tak programuje hlavně pro něj.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 6

Přehled komentářů

Stanley ... pokud bude zájem.
marwyn Re: ... pokud bude zájem.
Filip Re: ... pokud bude zájem.
msx Re: Vyvíjíme pro Android: Dialogy a activity
Matěj Konečný Re: Vyvíjíme pro Android: Dialogy a activity
msx Re: Vyvíjíme pro Android: Dialogy a activity
Zdroj: https://www.zdrojak.cz/?p=3697