Menu

Vyvíjíme pro Android – vylepšujeme aktivity

image00

Tento článek vyšel nejprve na webu abclinuxu.cz. Jde o třetí díl ze série článků o vývoji pro platformu Android.

Minule jsme si ukázali jednoduchou aplikaci složenou ze dvou aktivit. V tomto díle si příklad dále rozšíříme a ukážeme si nové konstrukty. Celkově probereme množství různých témat vztahujících se k aktivitám.

Používání listů

Použití view prvků, které umí zobrazit větší množství dat, není úplně triviální. Takovými views, jež vyžadují použití adaptéru, jsou například ListView, Spinner, Gallery nebo GridView.

Adaptér – třída vzniklá implementací interface Adapter – má funkci mostu mezi AdapterView (předek zmiňovaných views) a daty, která dané view zobrazují. Při použití ListView nám adaptér vytvoří most mezi seznamem položek a daty samotnými. Existuje několik abstraktních tříd adaptérů, jejichž poděděním snadno vytvoříte požadovanou funkcionalitu.

Nyní se vrátíme k našemu minulému příkladu. V něm jsme si vytvořili dvě aktivity. K tomuto příkladu si přidáme třetí aktivitu SelectText, která bude zobrazovat seznam řetězců. Kliknutím na položku seznamu ji vybereme a vrátíme předešlé aktivitě.

Aktivitu se seznamem lze vytvořit buď tak, že ji podědíme od třídy Activity a vytvoříme jednoduchý layout obsahující prvek ListView. Nebo lépe naši třídu podědíme od třídy ListActivity, která je připravená přesně pro tento účel a není třeba jí dodávat žádný layout definující obsah.

Zdrojový kód aktivity SelectText bude tento:

public class SelectText extends ListActivity {
    public static String INTENT_BUNDLE_TEXT = "text";
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setListAdapter(new SelectTextAdapter(this));
        ListView lv = getListView();
        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            public void onItemClick(AdapterView<?> parent,
                                    View view, int position, long id) {
                Intent appResultIntent = new Intent();
                appResultIntent.putExtra(INTENT_BUNDLE_TEXT,
                                         (String)parent.getItemAtPosition(position));
                setResult(RESULT_OK, appResultIntent);
                finish();
            }
        });
    }
}

Zde zavolání funkce onCreate() předka (ListActivity) způsobí nasetupování obsahu, což bychom jinak museli při použití vlastního layoutu dělat ručně. ListView, které je zde interně v ListActivity použito, se získá pomocí funkce getListView(). Pro přidání reakce na kliknutí na položku listu je třeba přidat OnItemClickListener, jeho použití je téměř identické jako v minulém příkladu u třídy ReturnText.

Důležitý příkaz setListAdapter(new SelectTextAdapter(this)) nastavuje adaptér, jenž se bude starat o data. K vytvoření tohoto adaptéru použijeme abstraktní třídu BaseAdapter:

public class SelectTextAdapter extends BaseAdapter {
    private Context mContext;
    static final String[] texts = {"Android", "Google", "abclinuxu.cz"};
    public SelectTextAdapter(Context context) {
        super();
        this.mContext = context;
    }
    public int getCount() {
        return texts.length;
    }
    public Object getItem(int position) {
        return texts[position];
    }
    public long getItemId(int position) {
        return position;
    }
    public View getView(int position, View convertView, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(
                                                    Context.LAYOUT_INFLATER_SERVICE);
        final TextView tv = (TextView)inflater.inflate(R.layout.select_text_item, null);
        tv.setText((String)getItem(position));
        return tv;
    }
}

Náš adaptér předává seznamu statické řetězce definované v kódu. Toto jistě není správný způsob, jak mít v aplikaci uložena data – jde pouze o zjednodušení. Lepší by bylo definovat si data v resources jako pole řetězců.

SelectTextAdapter obsahuje konstruktor, který má jediný parametr, a to kontext. Interface Context slouží k přístupu k aplikačním zdrojům. Context je velice často používaný, samotná třída Acitivity je jeho potomkem, lze tedy předávat referenci na aktuální aktivitu. Dále jsou zde čtyři funkce, jež je nutné naimplementovat. Poslední z nich, funkce getView(), vytváří views, které budou tvořit prvky seznamu. Pro vytvoření view z xml layoutu se používá třída LayoutInflater. Její instance se nikdy nevytváří přímo, místo toho se využívá instance, která se získá z contextu. Prvky seznamu jsou zde vytvořeny z jednoduchého layoutu select_text_item.xml. Ten obsahuje pouze jedno TextView:

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

Spuštění této aktivity a zpracování jejího výsledku je naprosto identické jako u třídy ReturnText, kterou jsme si vytvořili minule. Nyní se přesuneme na nové téma.

Dialogy

Občas bývá potřeba sdělit uživateli aplikace nějakou informaci nebo dostat jeho souhlas/odmítnutí. K takovému účelu se používají dialogy. Android nabízí jednak předdefinované jednoduché dialogy, ale také možnost vytvářet vlastní dialogy. Dokonce je možné zobrazit celou aktivitu jako plovoucí dialog. Nejprve se podíváme na vytváření jednoduchého potvrzovacího dialogu.

Všechny dialogy by se měly vytvářet ve funkci onCreateDialog(). K vytvoření jednoduchého dialogu s jedním až třemi tlačítky lze využít třídu AlertDialog.Builder. Funkce onCreateDialog() s kódem pro vytvoření jednoduchého dialogu s dvěma tlačítky by potom vypadala takto:

protected Dialog onCreateDialog(int id) {
    Dialog dialog;
    switch(id) {
    case DIALOG_YESNO:
        {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("Přejete si ukončit aplikaci");
            builder.setPositiveButton("Ano", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    Hello.this.finish();
                }
            });
            builder.setNegativeButton("Ne", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    TextView tv = (TextView)findViewById(R.id.text);
                    tv.setText("Odpověď je ne");
                }
            });
            dialog = builder.create();
        }
        break;
    default:
        dialog = null;
    }
    return dialog;
}

Nejprve je nutné vytvořit instanci statické třídy AlertDialog.Builder. Pomocí ní se specifikuje, jak bude dialog vypadat. Nastavíme zobrazovanou zprávu a přidáme dvě tlačítka. Reakcí na stisk tlačítka “Ano” bude ukončení aktivity, reakcí na stisk tlačítka “Ne” bude nastavení textu pro TextView.

Nyní zbývá vyřešit, jak systému sdělit, že chceme dialog vytvořit. Pro to stačí zavolat funkci showDialog(DIALOG_YESNO), kde DIALOG_YESNO je unikátní identifikátor tohoto dialogu.

Dialog s vlastním vzhledem a funkcionalitou vytvoříme instanciováním třídy Dialog. V následujícím příkladu nejprve vytvoříme dialog. Druhý příkaz pak říká, že nechceme, aby dialog měl titulek. Dále mu nastavíme layout specifikující jeho vzhled. Nakonec přidáme akci vyvolanou při stisku tlačítka v dialogu – text v edit boxu v dialogu se zkopíruje do TextView v aktivitě a dialog se zavře pomocí funkce dismissDialog().

case DIALOG_CUSTOM:
    {
        dialog = new Dialog(Hello.this, android.R.style.Theme_Dialog);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.setContentView(R.layout.my_dialog);
        final Dialog d = dialog;
        Button b = (Button)dialog.findViewById(R.id.btn_dialog_ok);
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TextView tv = (TextView)findViewById(R.id.text);
                EditText et = (EditText)d.findViewById(R.id.edit_text);
                tv.setText(et.getText());
                dismissDialog(DIALOG_CUSTOM);
            }
        });
    }
    break;

Layout pro dialog se tvoří stejně jako pro jakoukoliv aktivitu:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/edit_text" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btn_dialog_ok"
        android:text="Nastavit" />
</LinearLayout>

Při práci s dialogy často využijeme funkci onPrepareDialog(), která nám dobře poslouží, pokud obsah dialogů není statický – chceme-li, aby zobrazoval nějakou měnící se informaci. V této funkci poté nastavíme aktuální obsah dialogu. Funkce onPrepareDialog() se volá vždy před zobrazením dialogu, naproti tomu onCreateDialog() se volá pouze před prvním vytvořením dialogu.

Zmínil jsem se také o tom, že aktivitu je možné zobrazit jako plovoucí dialog. Taková aktivita se vytváří úplně stejně jako kterákoliv jiná, s jediným rozdílem – je třeba jí nastavit jiný styl. Konkrétně lze v manifestu nastavit aktivitě atribut android:theme="@android:style/Theme.Dialog". Aktivita se spouští pomocí intentu, nikoliv pomocí funkce showDialog(), jako klasický dialog.

Toast

Jiným užitečným konstruktem v systému je toast. Toast má sice k dialogu blízko, ale dialog to není. Je to malé view, které se vytvoří jako poloprůhledné plovoucí okno. Rozdíl oproti dialogům je, že toast nemá fokus a nelze na něm provádět akce. Pouze na určitou dobu zobrazí textovou zprávu. Elementy nacházející se pod ním jsou aktivní pro všechny uživatelské akce. Jeho vytvoření je jednoduché, stačí použít:

Toast.makeText(this, R.string.my_msg, Toast.LENGTH_SHORT).show();

První parametr je kontext, druhý zobrazovaná zpráva a třetí doba, po kterou bude zpráva zobrazena.

AsyncTask

Nyní na chvíli opustíme dialogy a podíváme se na něco jiného. Generická abstraktní třída AsyncTask umí asynchonně spustit jiný kód. Užitečná je zejména v tom, že nemusíte ručně vytvářet nová vlákna a starat se o ně. Zároveň zajišťuje informativní nástroj pro zjištění stavu tasku. AsyncTask tak často využijeme při ošetření časově náročnějších operací. Například můžeme chtít, aby se během výpočtu zobrazoval dialog informující uživatele o prováděném výpočtu. K tomu lze použít například instanci třídy ProgressDialog. AsyncTask potom můžeme zavolat následovně:

private void compute() {
    AsyncTask<String,Void,Long> task = new AsyncTask<String, Void, Long>() {
        private ProgressDialog mProgressDialog;
        protected Long doInBackground(String... params) {
            return Computation.computeSomething(params);
        }
        protected void onPreExecute() {
            mProgressDialog = ProgressDialog.show(Hello.this, "", "Počítám...", true);
        }
        protected void onPostExecute(Long result) {
            mProgressDialog.dismiss();
            TextView tv = (TextView)findViewById(R.id.text);
            tv.setText("Výsledek je " + result);
        }
    };
    task.execute(new String[]{"data1", "data2"});
}

Význam generických parametrů je po řadě – typ vstupních parametrů, typ updatového parametru a typ výsledku. V tomto kódu vytvoříme AsyncTask, který dostává jako parametry řetězce (String). V průběhu výpočtu neposílá žádnou informaci (Void) o svém pokroku a vrácená hodnota je typu Long.

Funkce onPreExecute() se volá před započetím práce asynchronního tasku – zde vytváříme instanci progress dialogu, která informuje uživatele o probíhající činnosti. Ve funkci doInBackground() probíhá výpočet. A konečně ve funkci onPostExecute() se zpracovává vrácený výsledek – v tomto příkladě zobrazení výsledku v TextView. AsyncTask se spouští zavoláním funkce execute() s parametry.

Pokud bychom nepoužili AsyncTask ani vlastní řešení za pomocí vláken, aplikace by se z pohledu uživatele stala neresponzivní, čemuž je nutné se vždy vyhnout.
Práce s asynchronními tasky vypadá snadně, je ovšem nutné dát si pozor na jejich použití ve vztahu k životnímu cyklu aktivit. Především je nutné si ohlídat, kde tasky skladujeme a inicializujeme, neboť změna konfigurace telefonu (způsobená například otočením displeje) defaultně resetuje aktivitu. To při špatném použití asynchronních tasků může vést k pádu aplikace.

Závěr

Ukázali jsme si několik důležitých konstruktů, se kterými se programátor při vytváření aktivit často setká. Pověděli jsme si něco o listech a adaptérech. Ukázali jsme si, jak vytvářet dialogy a toasty. A nakonec jsme zmínili i AsyncTask, který nám dokáže ušetřit spoustu práce s vlákny.

 

Komentáře

Honza

Honza

18.5.2011 10:58

Dík Tomáši za tyhle články. Už dlouho plánuju, že taky něco zkusím pro Androida naprogramovat a tohle mi určitě do začátku hodně pomůže. Zdravíme z LIMu :-)

Rob

Rob

20.5.2011 12:22

Vysvetlenie AsyncTasku ocenujem rovnako ako aj zbytok. Len by som sa pristavil pri listview. ked som zacinal s nim tak bolo docela zlozite ho pochopit preto by som ta doplnil :
1. pri vytvarani listview je potrebne mat dva layouty 1. ktory definuje podkladovu obrazovku a v nej musi byt komponenta ListView s parametrom android:id=”@android:id/list” (je to povinne aby to vedel oncreate najst). Tento layout je nacitavany v onCreate activity. Druhy layout je nacitavany v adapteri a definuje ako bude polozka menu zobrazena a co obsahuje … jej android:id je uz definovatelne volne.
2. casto sa stava ze ked je list prazdny tak by tam clovek chcel mat nejake info. Toto sa da riesit pridanim View/Layoutu do podkladoveho xml s parametrom android:id=”@id/android:empty” a v nom definovat ako ma prazdny list byt zobrazeny (textove info s TextView alebo aj nieco komplexnejsie)
3. ucelom ListView casto nie je len zobrazenie zoznamu ale aj praca s nim (napr mazanie zaznamov). K tomuto sluzi zavolanie metod add, remove v adapteri a nasledne volanie obnovy obrazovky cez notifyDataSetChanged()… kedze sa casto s obsahom pracuje v inom threade tak potrebne toto volanie obalit handler.post() ktore zabezpeci obnovu v vychodzom vlakne ktore ma pristup k tomu zdroju.
4. okrem kratkeho kliknutia (ala lave tlacitko) je mozne na jednotlivych polozkach zavolat aj “kontextove menu” k spracovaniu potom sluzia metody onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) = vytvorenie kontextoveho menu a onContextItemSelected(MenuItem item) = spracovanie
5. tak ako si vytvoril adapter (triedu) je to mozne ale pri vacsom mnozstve poloziek je to docela narocne na pamat kedze sa vytvaraju stale nove views .. lepsie je recyklovat povodne … myslim, ze v clanku mi chyba zmienka ze sa jedna o techniku, ktora je vhodna pri mensom pocte zaznamov a doporucit optimalizaciu.

Uff to som sa rozpisal :)

Aďo

Aďo

10.6.2011 15:34

Zaujimavy a velmi poucny clanok, dozvedel som sa vela veci!
Prepacte, ak predbieham serial, ale chcel by som, aby sa moje Android aplikacie dali presunut na SD kartu. Prosim Vas, napiste niekto, ako sa to da zariadit.
A este 1 otazka: Metodou finish() v Activity sa ukonci len aktivita, nie cela aplikacia. Je vhodne ukoncit celu Android aplikaciu (napr. po stlaceni tlacidla “Ukoncit” v mojom menu) metodami finish() v aktivite a nasledne System.exit(0)?

Tomas

Tomáš Kypta 0

10.6.2011 18:41

Aďo napsal: 10.6.2011 15:34

Zaujimavy a velmi poucny clanok, dozvedel som sa vela veci! Prepacte, ak predbieham serial, ale chcel by som, aby sa moje Android aplikacie dali presunut na SD kartu. Prosim Vas,…

Pro možnost uložení na SD kartu je nutné upravit manifest. Konkrétně je třeba nastavit atribut android:installLocation tagu manifest (více informací zde).
Ukončit aktivitu pomocí finish() je v pořádku. To ale nelze říct o ukončování aplikací. Svoji aplikaci ručně neukončujte, o ukončování aplikací se stará operační systém! Principy Android OS jsou trochu jiné, než na co je člověk zvyklý třeba z desktopu. Systém se sám rozhodne, kdy aplikace není dále potřeba resp. systémové prostředky jsou vyžadovány jinde, a ukončí Vaši aplikaci. Tlačítko “ukončit” ve Vaší aplikaci jde proti principům Android OS.
Volání System.exit() se nedoporučuje používat, může to mít některé nepředvídatelné následky, což může způsobit divné chování Vaší aplikace.

Aďo

Aďo

10.6.2011 21:34

Diky moc, zasa viem cosi viac a tesim sa na dalsiu cast seriala.

Kolman

Kolman

9.3.2012 16:49

Váš serial o vývoji na androida je velmi zajímavý a pochopitelně napsaný, je škoda, že jste nevytvořil více částí. Byl bych jenom rád, sám se ted pokouším nějakou aplikaci nakodit.

RSS (komentáře k článku)