Přejít k navigační liště

Zdroják » Mobilní vývoj » Vyvíjíme pro Android: Fragmenty a SQLite databáze

Vyvíjíme pro Android: Fragmenty a SQLite databáze

V dnešním díle seriálu Vyvíjíme pro Android se naučíme pracovat s vestavěnou SQLite databází a seznámíme se s fragmenty. To všechno na už poněkud rozsáhlejší ukázkové aplikaci.

Až dodnes jsme se věnovali tomu, jak vyvíjet uživatelské rozhraní aplikace. Naposledy jsme, hned ve dvojčlánku, poznali všechna možná view i s jejich metodami. Ale téměř každá aplikace potřebuje ukládat data. Ať už jde o seznam poznámek, nějaká uživatelská nastavení, vygenerovaný obrázek či trasu, kterou uživatel ujel na kolečkových bruslích.

Data můžete na Androidu ukládat několika způsoby. Každý se hodí na něco jiného. Do vestavěné SQLite databáze, jež je tématem dnešního článku, půjdou data typu seznam úkolů. Interní úložiště je určené pro soubory, které mají být smazány při smazání aplikace a které nesmí být dostupné ze vnějšku. Externí úložiště je vlastně veřejný filesystem, z nějž může soubory číst, mazat, ukládat či upravovat každý. Shared preferences je key-value úložiště, které umí pracovat jen s primitivními typy. Jeho název by sice napovídal, že je určeno pro různá uživatelská nastavení (pro což mimochodem vskutku slouží a Android poskytuje několik tříd, které tento úkol zjednodušují na úplné minimum), ale použít lze a používá se pro všechna jednoduchá key-value  data.

Fragmenty

Než začneme programovat (přičemž se za běhu seznámíme se vším potřebným), musím vám povědět něco o tom, co jsou to vlastně fragmenty. Už minule jsem vám doporučil přečtení mého dřívějšího článku Dej Androidu tablety!, kde jsem se fragmentům věnoval. Pokud jste tak doteď nepodnikli, doporučuji vám přečíst si ho nyní, neboť nemá smysl se opakovat, a tak budu dnes stručný.

V Androidu 3.0, který reagoval na vzestup poptávky po tabletech, musel Google řešit problém, jak umožnit vývojářům co nejjednodušeji vytvářet aplikace, jejichž rozhraní se dokáže přizpůsobit jak fyzicky velkým, tak malým displejům. A jako řešení tohoto problému zavedli fragmenty.

Fragmenty celkově označují nový přístup ke tvorbě uživatelského rozhraní, kdy mezi Activity a View vstupuje ještě jedna vrstva, a to Fragment. Takový Fragment má za úkol „obalit” nějaký funkční celek, tedy například seznam poznámek, zobrazení jedné poznámky (titulek a text) či formulář přidávající novou poznámku; a to jak potřebná View, tak metody s nimi související. Fragment obalující seznam poznámek se postará například o naplnění ListView daty či zobrazení kontextového menu nad položkou, pakliže ji uživatel „přidržel”.

Fragment ale nemůže vědět všechno. Například neví, co se má stát po kliknutí na název nějaké poznámky. A od toho jsou zde Activity. Každá Activity může obsahovat libovolné množství Fragmentů, které se chovají víceméně jako View. Dají se deklarovat v layoutovém XML souboru anebo přidat v kódu (ačkoli trochu jiným mechanismem). Activity může volat jejich metody, nastavovat jim posluchače událostí (pokud je Fragment nabízí). V případě, že bez nastaveného posluchače by neměl Fragment valného smyslu, může si jeho nastavení dokonce vynutit.

Support Library

K čemu nám ale jsou fragmenty, když jsou až od Androidu 3.0, tedy API 11? Na to Google samozřejmě myslel. Nikdo by nezačal fragmenty používat, kdyby to znamenalo, že musí zavrhnout podporu všech jedničkových a dvojkových Androidů. A proto vznikla takzvaná Support (někdy Compatibility) Library (někdy Package). Ta portuje fragmenty (a nejen fragmenty) až na API 4. Odteď budu ve všech projektech, kde budeme používat fragmenty (což budou prakticky všechny, v nichž bude jen trochu složitější uživatelské rozhraní), automaticky předpokládat, že máte Support Library přidanou do projektu jako knihovnu. To se dělá tak, že kliknete pravým tlačítkem na váš projekt  → Android tools → Add Support Library.

Tím končí úvodní teorie a můžeme jít programovat. Zbytek si povíme za běhu.

Poznámkový blok

Příklady fragmentů jsem nezvolil náhodně, jde o jejich konkrétní využití v dnešní aplikaci. Naprogramujeme si totiž poznámkový blok.

Každá poznámka se bude skládat z titulku, textu a nějakého id. Poznámky uložíme do SQLite databáze (jež sama zvolí jejich id). Veškerá práce s databází bude ve třídě Notes, která nabídne metody pro vytvoření a smazání poznámky a získání jedné a všech poznámek. Potom budeme potřebovat Fragment zobrazující formulář pro přidání poznámky, Fragment zobrazující jednu poznámku a Fragment, který zobrazí seznam poznámek, umožní uživatelům dlouhým klepnutím vyvolat menu, z nějž lze poznámku smazat, a krátkým klepnutím zobrazit detail poznámky. Ještě k tomu musíme přidat nějaké Activity, které Fragmenty správně zobrazí a samozřejmě nějaké suroviny.

Pro práci s vestavěnou SQLite databází potřebujete alespoň úplné základy SQL. Až na vytvoření tabulek se sice přímo s SQL prakticky nepracuje, ale některé metody přebírají jako argument část SQL dotazu.

Notes

Třída Notes bude takovým modelem, bude zařizovat veškerou komunikaci s databází. Musí umět vytvořit databázi a tabulku, pakliže neexistují, a k tomu bude zapouzdřovat všechny dotazy na databázi.

SQLiteOpenHelper

SQLiteOpenHelper je třída, která zjednodušuje vytváření a otevírání databáze. Díky ní musíme implementovat jen tři metody: Konstruktor zavolá svého rodiče a předá mu Context, verzi databáze a jméno databáze, metoda onCreate je zavolána tehdy, když otevíraná databáze neexistuje, a metoda onUpgrade přijde ke slovu tehdy, když existující databáze má nižší verzi než je ta, kterou dostal jako parametr konstruktor SQLiteOpenHelper. Dokud databázi neměníte, nemusí tahle metoda dělat nic smysluplného.

Podíváme se tedy, jak vypadá začátek souboru Notes.java (deklaraci namespace a importy jsem vynechal). Nejprve vytvoříme důležité konstanty, jako název databáze nebo názvy jednotlivých sloupců. Asi se hodí říci, že název databáze musí být unikátní v rámci aplikace, nikoli v rámci celého telefonu, takže nemusíte používat žádné namespace prefixy. Jako jméno primárního číselného klíče se v celém Androidu používá "_id". Ačkoli to není nutné pro funkčnost samotné databáze (primární klíč se může jmenovat jinak, anebo tam ani být nemusí), je to potřeba například pro SimpleCursorAdapter, s nímž budeme pracovat později.

public class Notes {

    protected static final String DATABASE_NAME = "notepad";
    protected static final int DATABASE_VERSION = 2;

    protected static final String TB_NAME = "notes";

    // Speciální hodnota "_id", pro jednodušší použití SimpleCursorAdapteru
    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_TITLE = "title";
    public static final String COLUMN_NOTE = "note";

    private SQLiteOpenHelper openHelper;

Potom deklarujeme třídu DatabaseHelper, která dědí od SQLiteOpenHelper a pomůže nám databázi vytvořit či otevřít. V onCreate  spustíme přímo SQL dotaz, který vytvoří tabulku s požadovanými sloupci požadovaného typu. Hodnota sloupce _id se bude nastavovat automaticky. V onUpgrade  zde jednoduše smažeme tabulku a vytvoříme novou, tím by však uživatel přišel o data. Ve skutečnosti by bylo potřeba pro každou novou verzi databáze vytvořit pro každou tabulku, která se v dané verzi mění, nějaký SQL řetězec, který ji upgraduje bez ztráty dat (asi ALTER TABLE ...). V konstruktoru třídy Notes se jen vytvoří DatabaseHelper. (Pozor, neotevře se žádné spojení s databází, to je drahé jak časově, tak prostředkově. Spojení budeme vytvářet tehdy, až budou potřeba.)

    static class DatabaseHelper extends SQLiteOpenHelper {

        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TB_NAME + " ("
                    + COLUMN_ID + " INTEGER PRIMARY KEY,"
                    + COLUMN_TITLE + " TEXT NOT NULL,"
                    + COLUMN_NOTE + " TEXT NOT NULL"
                    + ");");
        }

        /*
         * Ve skutečnosti je potřeba, abychom uživatelům nemazali data, vytvořit
         * pro každou změnu struktury databáze nějaký upgradovací nedestruktivní
         * SQL příkaz.
         */
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS notes");
            onCreate(db);
        }
    }

    public Notes(Context ctx) {
        openHelper = new DatabaseHelper(ctx);
    }

Než si ukážeme zbytek kódu, musíme si vysvětlit, jak fungují metody pro dotazování databáze. Na Androidu není zvykem vytvořit celý SQL řetězec a zavolat metodu SQLiteDatabase.rawQuery(String sql, String sqlArgs) (která se ale pro dotazy typu SELECT, INSERT atp. použít dá, narozdíl od execSQL, která je určena pro dotazy nevracející data). Místo toho nám Android poskytl metodu query (má více signatur) pro dotazy SELECT, metody insert, insertOrThrow a insertWithOnConflict pro INSERT, z nichž použijeme tu první (která se od druhé liší tím, jak ohlásí chybu), update a updateWithOnConflict pro UPDATE, a delete pro příkaz  DELETE.

Vezměme si třeba argumenty metody query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit). První argument, table, neznamená nic jiného než jméno tabulky, z níž se bude SELECT-ovat. Řetězcové pole columns obsahuje názvy těch sloupců, které chceme získat. Pokud předáte null, dostanete všechny sloupce, což se ale nedoporučuje, jednak kvůli samodokumentovatelnosti kódu, jednak kvůli paměťovým optimalizacím. Nechceme-li získat všechny řádky, předáme selection, což je to, co by v SQL řetězci přišlo bezprostředně za WHERE. Místo hodnot, s nimiž porovnáváte, použijte otazník a samotnou hodnotu pak vložte do pole selectionArgs. Další argumenty, groupBy, having, orderBy a limit přebírají jako hodnotu přesně to, co byste do SQL řetězce napsali za odpovídající příkaz. Jako kterýkoli argument kromě table můžete předat  null.

Kdybychom třeba měli tabulku s demografickými údaji obyvatelů Kocourkova a chtěli se zeptat na jména mužů starších 40 let, kterážto by měla být seřazena sestupně podle věku, mohl by dotaz vypadat nějak takto:

db.query("kocourkov", new String[]{"jmeno"}, "pohlavi = ? AND vek > ?", new String[]{"muz", "40"}, null, null, "vek DESC");

Můžete si všimnout, že jsem vynechal argument limit, neboť existuje i varianta metody query bez něj. V reálném případě bychom místo řetězců použili nějaké konstanty. Takovéto volání metody query by zhruba odpovídalo následujícímu SQL příkazu:

SELECT jmeno FROM kocourkov WHERE pohlavi = "muz" AND vek > 40 ORDER BY vek DESC

Argumenty metody delete jsou podmnožinou těch, o nichž jsme mluvili, neboť při mazání nemá smysl něco seskupovat či řadit. Metoda vrací počet smazaných řádků, pokud je nastavený parametr where. Pokud chcete smazat všechny řádky a zároveň zjistit, kolik že jste jich smazali, předejte jako where něco, co je vždy pravda. Třeba  "1".

Metoda insert(String table, String nullColumnHack, ContentValues values) má dva zatím neznámé atributy. Values jsou hodnoty nového řádku, reprezentované objektem ContentValues. Je to velmi jednoduchý objekt, stačí nám znát jeho konstruktor bez argumentů a metoda put, jejímž prvním argumentem je název odpovídajícího sloupce a druhým je hodnota, na níž ho chceme nastavit. NullColumnHack je jméno nějakého sloupce, který může být NULL. Je tam proto, že kdyby se jako values předal prázdný objekt ContentValues, nešlo by vytvořit validní SQL dotaz. Je-li nastaven parametr nullColumnHack, explicitně se daný sloupec nastaví na NULL. Pokud to nepotřebujete, můžete jako hodnotu tohoto argumentu předat null. Metoda vrátí id nově přidaného řádku, anebo -1, pokud se řádek nepodařilo přidat (a zároveň jsme nepoužili takovou variantu metody insert, která by vyhodila výjimku).

A nakonec metoda update, která kombinuje argumenty insert a delete. Sloupce uvedené ve values nahradí, ty neuvedené nechá být. Vrátí počet ovlivněných řádků.

Asi jste si všimli, že jsem vynechal návratovou hodnotu metody query. Ta totiž vrací objekt Cursor, který reprezentuje vrácená data a umožňuje s nimi paměťově efektivně pracovat. Je jednoduchý, ale přesto si ho pořádně představíme až tehdy, když ho budeme potřebovat.

Teď už víme všechno potřebné a můžeme si ukázat zbytek třídy Notes. Všimněte si metod SQLiteOpenHelper.getReadableDatabase(), resp. SQLiteOpenHelper.getWritableDatabase(), které vrátí objekt SQLiteDatabase, v prvním případě jen pro čtení, v tom druhém i s možností zapisovat. Tyto objekty fungují do té doby, než se na nich zavolá metoda close, anebo se zavolá metoda SQLiteOpenHelper.close(), která zavře všechny otevřené databáze vytvořené tím konkrétním SQLiteOpenHelperem. Tu jsme zvolili my s tím, že necháme na uživatelích naší třídy, aby tuhle metodu zavolali tehdy, až nebudou potřebovat otevřené databáze a data z nich získaná, tedy objekty Cursor. V případě vkládací či mazací metody můžeme databázi zavřít hned.

    public static final String[] columns = { COLUMN_ID, COLUMN_NOTE,
            COLUMN_TITLE };

    protected static final String ORDER_BY = COLUMN_ID + " DESC";


    public Cursor getNotes() {
        SQLiteDatabase db = openHelper.getReadableDatabase();
        return db.query(TB_NAME, columns, null, null, null, null, ORDER_BY);
    }

    public Cursor getNote(long id) {
        SQLiteDatabase db = openHelper.getReadableDatabase();
        String[] selectionArgs = { String.valueOf(id) };
        return db.query(TB_NAME, columns, COLUMN_ID + "= ?", selectionArgs,
                null, null, ORDER_BY);
    }

    public boolean deleteNote(long id) {
        SQLiteDatabase db = openHelper.getWritableDatabase();
        String[] selectionArgs = { String.valueOf(id) };

        int deletedCount = db.delete(TB_NAME, COLUMN_ID + "= ?", selectionArgs);
        db.close();
        return deletedCount > 0;
    }

    public long insertNote(String title, String text) {
        SQLiteDatabase db = openHelper.getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put(COLUMN_TITLE, title);
        values.put(COLUMN_NOTE, text);

        long id = db.insert(TB_NAME, null, values);
        db.close();
        return id;
    }

    public void close() {
        openHelper.close();
    }
}

Poslední složená závorka byla zavírací závorkou třídy Notes. Tu tímto máme hotovou a jdeme se vrhnout na fragmenty.

NotesListFragment

Stejně tak jako existovala ListActivity, existuje i ListFragment (a přestože odkazuji do dokumentace na android.app.ListFragment, vy děďte od android.support.v4.app.ListFragment (obecně, kdykoli dostanete při importu na výběr z více tříd stejného jména, vyberte tu ze Support Library, pokud nemáte dobrý důvod udělat to jinak)). Tato třída nabízí některá zjednodušení pro práci s ListView. Nabízí i vestavěný layout, ale my tentokrát chceme, aby se uživateli, když zatím nemá uložené žádné poznámky, zobrazil text "Nemáte uloženy žádné poznámky.", a to červeně, takže si vytvoříme vlastní layout. Na ten jsou kladeny jisté podmínky, a to, že ListView musí mít id @android:id/list a TextView, které se zobrazí, kdyby mělo ListView být prázdné, musí mít id  @android:id/empty:

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

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="1"
        android:visibility="gone" />

    <TextView
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/no_notes_available"
        android:textColor="#f00"
        android:visibility="visible" />

</LinearLayout>

Fragment nenabízí žádnou metodu setContenView. Místo toho implementujeme metodu View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState). LayoutInflater je šikovná třída, jejíž metoda inflate umí z XML definice layoutu vytvořit strom View. My použijeme konkrétně metodu inflate(int resource, ViewGroup root, boolean attachToRoot), které předáme identifikátor layoutové suroviny, potom ViewGroup, které bude daný fragment obsahovat ( container z onCreateView), a false, jež značí, že vytvořený strom nemá být rovnou vložen do  root u.

Protože bez možnosti kliknutí na název poznámky by fragment neměl takový smysl, donutíme Activity používající NotesListFragment, aby implementovala OnNoteClickedListener, což je rozhraní definované uvnitř třídy NotesListFragment. Pro otestování, zda ho třída implementuje, je vhodná metoda onAttach(Activity activity), která se zavolá tehdy, když je Fragment umístěn do nějaké Activity. Tam zkusíme předanou activity přetypovat na OnNoteClickedListener, a pokud přetypování selže, vyhodíme výjimku.

Metoda onActivityCreated se zavolá poté, co skončí metoda onCreate „rodičovské” Activity. Tam můžeme bezpečně naplnit ListView daty atd.

Pojďme se tedy podívat na začátek NotesListFragment.java (namespace a importy vynechány). Volání registerForContextMenu způsobí, že ListView bude reagovat na podržení nějaké položky tím, že zobrazí kontextové menu. K tomu se dostaneme za chvilku. updateList je vlastní metoda, tu implementujeme také za brzy.

public class NotesListFragment extends ListFragment {

    OnNoteClickedListener listener;

    public static interface OnNoteClickedListener {
        public void onNoteClicked(long id);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.notes_list, container, false);
        return view;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            listener = (OnNoteClickedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement OnNoteClickedListener");
        }
    }

    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        registerForContextMenu(getListView());
        updateList();
    }

V metodě updateList budeme používat SimpleCursorAdapter. (Pozor! Importujte android.support.v4.widget.SimpleCursorAdapter, což je třída ze SupportLibrary, která implementuje konstruktor přidaný až v API 11. Ten, který se používal dříve, je nyní deprecated.) To je další vestavěný Adapter. Tento, narozdíl od minule představeného ArrayAdapter u, naplňuje ListView daty z Cursoru, což se nám hodí, neboť Cursor vrací metody třídy Notes. Jeho konstruktor přebírá nějaký Context (v případě fragmentu výsledek volání ( getActivity()), identifikátor surovinového layoutu, který zobrazí každý řádek Cursoru, samotný Cursor, řetězcové pole, což je pole názvů sloupců, které chceme zobrazit, pole integerů, což je pole id těch view, do kterých chceme dané sloupce zobrazit, a jeden integer flags který určuje něco, co nás teď zase tak nemusí zajímat, a proto předáme  0.

Mezi námi, ten argument flags je to jediné, v čem se konstruktory navenek liší. V tomhle případě mají určitě navrch zastánci nastavování Target API version na co nejnižší číslo, alespoň z pohledu uživatele, který musí použít třídu ze Support Library jen proto, aby vypnul to, co nabízí navíc. Ale zase na druhou stranu, kdybychom třídu Notes implementovali jako content provider (možná někdy příště) a implementovali potřebné věci, díky parametru flags bychom se mohli zbavit ručního updatování seznamu poznámek, když je nějaká smazána či přidána.

Znovu připomenu, že SimpleCursorAdapter vyžaduje, aby předaný Cursor obsahoval sloupec _id. Hodnotu tohoto sloupce potom dostanete například v metodě, která se zavolá při klepnutí na nějakou položku seznamu. Díky tomu tu položku můžete jednoduše identifikovat.

Protože zobrazujeme jen jeden text, jako layout pro každý řádek Cursoru použijeme androidí vestavěný android.R.layout.simple_list_item_1, který obsahuje jedno TextView s id  android.R.id.text1:

    public void updateList() {
        Context ctx = getActivity();
        Notes notes = new Notes(ctx);

        String[] from = { Notes.COLUMN_TITLE };
        int[] to = { android.R.id.text1 };

        ListAdapter adapter = new SimpleCursorAdapter(ctx,
                android.R.layout.simple_list_item_1, notes.getNotes(), from,
                to, 0);

        setListAdapter(adapter);

        notes.close();
    }

Po Activity jsme vyžadovali, aby uměla reagovat na klepnutí na nějakou položku seznamu. Tak to teď implementujeme, je to opravdu triviální:

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

A už nám zbývá jen vytvořit kontextové menu nad každou položkou, nabízející její smazání. První krok jsme už udělali tím, že jsme zavolali registerForContextMenu(getListView()). Dále ještě musíme implementovat metodu onCreateContextMenu, která se zavolá při vytváření kontextového menu a má za úkol přidat potřebné položky, a metodu onContextItemSelected, jež se zavolá po zvolení nějaké položky kontextového menu:

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo){
        super.onCreateContextMenu(menu, v, menuInfo);
        menu.add(0, MENU_DELETE_ID, 0, R.string.delete);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
        switch (item.getItemId()) {
            case MENU_DELETE_ID:
                deleteNote(info.id);
                return true;
            default:
                return super.onContextItemSelected(item);
        }
    }

    private void deleteNote(long id){
        Context ctx = getActivity();
        Notes notes = new Notes(ctx);

        if(notes.deleteNote(id)){
            Toast.makeText(ctx, R.string.note_deleted, Toast.LENGTH_SHORT).show();
            updateList();
        } else{
            Toast.makeText(ctx, R.string.note_not_deleted, Toast.LENGTH_SHORT).show();
        }
    }
}

A máme hotový NotesListFragment.

Někoho možná napadlo, jaktože jsem mluvil o tom, že Fragment si nemá všímat svého okolí, a přitom jsem přímo v něm používal třídu Notes, což je hodně daleké okolí. Samozřejmě jsem mohl jednak při vytváření Fragmentu rovnou požádat o Cursor a jednak vyžadovat po Activity, aby implementovala ještě další interface a aby si uměla poradit se smazáním poznámky. Koneckonců tento přístup jsem zvolil u Fragmentu pro přidání poznámky a sami uvidíte, jaké výhody a jaké nevýhody to přineslo. Já osobně se v tomto případě přikláním k akademicky možná ne úplně čistému, ale rozhodně jednoduššímu a praktičtějšímu používání Notes v rámci Fragmentů.

AddNoteFragment

AddNoteFragment je jednoduchý Fragment, který zobrazí formulář umožňující přidat novou poznámku. Upozorním jen na dvě věci: Přímo na třídě Fragment není definovaná metoda findViewById. Musíte zavolat getView, díky čemuž získáte View vrácené onCreateView, a vyhledávat na něm. Druhé upozornění je na atribut android:onClick, který mi u Fragmentu nefungoval, metoda se hledala na Activity obsahující Fragment.

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

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="@string/add_note"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <EditText
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/title"
        android:inputType="text"
        android:lines="1" />

    <EditText
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="top|left"
        android:hint="@string/text"
        android:inputType="textMultiLine" />

    <Button
        android:id="@+id/submit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="@string/submit" />

</LinearLayout>
public class AddNoteFragment extends Fragment {
    OnAddNoteListener listener;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            listener = (OnAddNoteListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement OnAddNoteListener");
        }
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.add_note, null);

        Button submit = (Button)view.findViewById(R.id.submit);
        submit.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                onSubmitClicked();
            }
        });

        return view;
    }

    public void onSubmitClicked(){
        View root = getView();

        String title = ((EditText)root.findViewById(R.id.title)).getText().toString();
        String text = ((EditText)root.findViewById(R.id.text)).getText().toString();

        listener.onAddNote(title, text);
    }

    public static interface OnAddNoteListener {
        public void onAddNote(String title, String text);
    }
}

SingleNoteFragment

SingleNoteFragment je dnešní poslední Fragment, který zobrazí jednu poznámku. Layout má naprosto nezajímavý,

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

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:textAppearance="?android:attr/textAppearanceMedium" />

</LinearLayout>

i začátek jeho kódu nepřináší nic nového,

public class SingleNoteFragment extends Fragment {

    private long id;

    public SingleNoteFragment(long id) {
        this.id = id;
    }

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

        TextView title = (TextView) root.findViewById(R.id.title);
        TextView text = (TextView) root.findViewById(R.id.text);

        Notes notes = new Notes(getActivity());
        Cursor note = notes.getNote(id);

ale pak zjistíme, že potřebujeme pracovat s Cursorem. Potřebujeme nějak získat první řádek (on jich ani víc nemá, teoreticky jich dokonce může mít méně, ačkoli prakticky by se stát nemělo, že by SingleNoteFragment dostal id neexistující poznámky) a z něj vytáhnout titulek a text poznámky. Nejprve zjistíme, jaké indexy mají v rámci Cursoru námi chtěné sloupce:

        int titleIndex = note.getColumnIndex(Notes.COLUMN_TITLE);
        int textIndex = note.getColumnIndex(Notes.COLUMN_NOTE);

Potom ověříme, máme-li alespoň jeden záznam k dispozici. Pokud ne, zobrazíme nějakou chybovou hlášku:

        if (note.getCount() < 1) {
            title.setText(R.string.error);
            title.setError("");
        }

A pokud záznam k dispozici máme, přejdeme na první řádek Cursoru, získáme z něj požadované řetězce a ty umístíme do dříve získaných TextView. Kdybychom měli řádků více, můžeme moveToNext volat, dokud nám bude vracet true. (V případě našeho Fragmentu by asi bylo dokumentačně lepší místo moveToNext použít moveToFirst, ale moveToNext funguje také a je univerzálnější, tak jsem použil to, abyste si zapamatovali užitečnější metodu.)

        else {
            note.moveToNext();
            title.setText(note.getString(titleIndex));
            text.setText(note.getString(textIndex));
        }

A nakonec uzavřeme kurzor, databázi, vrátíme vytvořené View a uzavřeme deklaraci metody i třídy:

        note.close();
        notes.close();
        return root;
    }
}

Při vydání článku jsme ve tvorbě třídy SingleNoteFragment zapoměli na důležitou věc, viz Errata.

Tím máme hotové všechny Fragmenty. Ale ještě nás čekají Activity, do nichž Fragmenty umístíme.

Na tabletu nám bude stačit jedna Activity, NotepadActivity  – ta hlavní. Ale na telefonech musíme vytvořit další dvě pomocné Activity, neboť tam dokáže NotepadActivity  zobrazit pouze NotesListFragment. Na AddNoteFragment budeme potřebovat AddNoteActivity a na SingleNoteFragment využijeme  SingleNoteActivity.

Telefony je označení pro zařízení se šířkou menší než je ta, kterou níže definujeme jako minimální pro dvousloupcový layout. Tablety jsou zařízení, na nichž se NotepadActivity zobrazí dvousloupcově.

SingleNoteActivity

Začneme SingleNoteActivity, která je nejjednodušší. V Intentu dostane jako extra id poznámky určené ke zobrazení, v metodě onCreate potom vytvoří SingleNoteFragment, předá mu id a umístí ho do FrameLayoutu. Jelikož její layoutový soubor obsahuje právě jen FrameLayout s id single_note_container, nebudu ho zde ani ukazovat.

Každá Activity, která má pracovat s Fragmenty ze Support Library, musí dědit od FragmentActivity. To je třída, která sama dědí od Activity (takže se nebojte, že byste měli zapomenout to, co jste se o Activitách už naučili) a přidává podporu Fragmentů (ze Support Library).

public class SingleNoteActivity extends FragmentActivity {
    public static final String EXTRA_ID = "id";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.single_note_container);

        long id = getIntent().getLongExtra(EXTRA_ID, -1);

        Fragment f = new SingleNoteFragment(id);

Aha. Ale jak fragment umístit do vytvořeného kontejneru? Na to existuje třída, která se jmenuje FragmentManager (v našem případě android.support.v4.app.FragmentManager). Ta se stará o přidávání, odebírání nebo nahrazování Fragmentů v jednotlivých View (a zdaleka nejen o to). Abych byl přesnější, o to, co jsem vyjmenoval, se ve skutečnosti stará třída FragmentTransaction (resp. její supportový ekvivalent), kterou získáme z FragmentManageru zavoláním metody beginTransaction. V rámci transakce přidáme, odebere, vyměníme či přeházíme Fragmenty a transakci commit neme. (Proč je na to potřeba transakce, si ukážeme u  NotepadActivity.)

Pracujeme-li se Support Library, tzn. s  FragmentActivity, android.support.v4.app.Fragment atd., musíme pro získání FragmentManageru použít metodu getSupportFragmentManager. Kdybyste se rozhodli vyvíjet jen pro Android 3.0 a vyšší, a Support Library tudíž nepoužívat, příslušná metoda se jmenuje getFragmentManager a je definována přímo na  Activity.

        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.container, f);
        ft.commit();
    }
}

AddNoteActivity

Protože jsme se rozhodli, že AddNoteFragment nechá uložení nové poznámky na někom jiném, musí AddNoteActivity implementovat rozhraní AddNoteFragment.OnAddNoteListener. A abychom ukládání neimplementovali dvakrát, AddNoteActivity tuhle povinnost přenese na NotepadActivity. Využije přitom toho, že jedna Activity může spustit druhou s tím, že po ní chce, aby jí vrátila nějaká data. Ta spuštěná Activity běží tak dlouho, jak je jí libo, a když má všechna data získaná, vloží je do Intentu, zavolá setResult(int resultCode, Intent data), které předá vytvořený Intent a RESULT_OK nebo RESULT_CANCELED ( RESULT_FIRST_USER necháme stranou). Potom zavolá finish, čímž se ukončí a nechá na frameworku, aby pustil do popředí Activity, která tuto spustila, a předal ji Intent s daty.

public class AddNoteActivity extends FragmentActivity implements AddNoteFragment.OnAddNoteListener{
    public static final String EXTRA_TITLE = "title";
    public static final String EXTRA_TEXT = "text";


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.add_note_container);
    }

    public void onAddNote(String title, String text) {
        Intent result = new Intent();
        result.putExtra(EXTRA_TITLE, title);
        result.putExtra(EXTRA_TEXT, text);

        setResult(RESULT_OK, result);
        finish();
    }
}

Asi se ptáte, kam se poděl AddNoteFragment. Ten, protože nepotřebuje žádné speciální parametry, je definovaný v layoutu:

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

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.example.notepad.AddNoteFragment" />

</FrameLayout>

NotepadActivity

Zbývá nám už jen NotepadActivity, která je ovšem ze všech Activit nejkomplikovanější. Na telefonu totiž zobrazí jen NotesListFragment a při kliknutí na nějakou poznámku (či při žádosti o vytvoření nové poznámky) spustí speciální Activity, na tabletu bude mít dva sloupce. V levém zobrazí totéž co na telefonu, tedy tlačítko s nabídkou vytvoření nové poznámky a hned pod ním seznam poznámek, ale obsah pravého se bude střídat. Někdy tam nebude nic, někdy AddNoteFragment a jindy  SingleNoteFragment.

Nejdříve si vytvoříme layoutový soubor, který bude obsahovat to telefonu i tabletu společné, tedy levý tabletový sloupec. Pojmenujeme ho  /res/layout/single_column_main.xml.

<?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="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onAddNoteClicked"
        android:text="@string/add_note" />

    <fragment
        android:id="@+id/notes_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        class="com.example.notepad.NotesListFragment" />

</LinearLayout>

A tímto vám představuji nový element, který můžete použít v layoutových souborech, a to <include>. Ten umožní vložit jeden layout do druhého. V nejjednodušší podobě mu stačí jen atribut layout (bez namespace android:), jehož hodnotou je odkaz na vkládanou layoutovou surovinu. Dokáže si ale poradit s jakýmkoli layoutovým atributem. V takovém případě se tento atribut nastaví kořenovému View vkládaného layoutu. Pokud tam je daný atribut už přítomen, přepíše se.

V /res/layout/main.xml  jsem musel navíc přidat FrameLayout, přestože je v podstatě zbytečný. Android si ale neuměl poradit s tím, když bylo <include> kořenovým elementem. (A single_column_main  jako contentView nastavit nemohu, neboť právě díky layoutu rozliším tablet od telefonu.)

<?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 nemůže být kořenovým elementem -->

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

</FrameLayout>

Teď si vytvořte novou podsložku /res s názvem layout-w660dp. (Vzpomínáte na modifikátory, o nichž jsme si povídali nedávno?) Hodnota minimální šířky je zvolena tak, že by to měly být zhruba dva Nexusy S na šířku. Ale není to žádné standardní pravidlo, záleží na konkrétní situaci.

Do této složky potom vytvořte nový soubor a pojmenujte ho main.xml (ne, to není chyba, jmenuje se stejně). Jeho obsah bude následující: (Opět, šířka levého sloupce nemá žádný hlubokomyslný základ, dokonce by možná bylo hezčí, kdybych nastavil nějaký poměr pomocí  layout_weight.)

<?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="330dp"
        android:layout_height="match_parent" >

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

Do FrameLayoutu s id right_column budeme vkládat různé Fragmenty.

A tím nám už zbývá jen NotepadActivity. V onCreate  nastavíme contentView na main a zjištěním existence pravého panelu šikovně určíme, zda jsme na tabletu:

public class NotepadActivity extends FragmentActivity implements
        NotesListFragment.OnNoteClickedListener,
        AddNoteFragment.OnAddNoteListener {

    private boolean dualPane;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        dualPane = findViewById(R.id.right_column) != null;
    }

Při klepnutí na poznámku v  NotesListFragment u musíme zobrazit její detail.

    public void onNoteClicked(long id) {
        showNote(id);
    }

    private void showNote(long id){
        if(dualPane){

Pokud jsme na tabletu, umístíme SingleNoteFragment do pravého sloupce, jehož případný předchozí obsah odstraníme. Tím, že zavoláme metodu FragmentTransaction.addToBackStack zajistíme, že klepne-li uživatel na tlačítko zpět, nově přidaný Fragment zmizí a znovu se zobrazí to, co tam bylo předtím. Jako parametr předáme null a nebudeme si toho moc všímat.

            Fragment f = new SingleNoteFragment(id);
            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.right_column, f);
            ft.addToBackStack(null);
            ft.commit();
        }

Pokud máme k dispozici jen jeden sloupec, spustíme  SingleNoteActivity.

        else{
            Intent i = new Intent(this, SingleNoteActivity.class);
            i.putExtra(SingleNoteActivity.EXTRA_ID, id);
            startActivity(i);
        }
    }

Klepne-li uživatel na tlačítko „Přidat poznámku”, zavolá se metoda onAddNoteClicked. Ta buď umístí do pravého sloupce AddNoteFragment,

    public void onAddNoteClicked(View v){
        if(dualPane){
            Fragment f = new AddNoteFragment();
            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.right_column, f);
            ft.addToBackStack(null);
            ft.commit();
        }

anebo spustí AddNoteActivity, ale tím, že zavolá startActivityForResult, dá najevo, že si přeje dostat data vrácená spuštěnou Activity. (To proběhne v metodě onActivityResult, a aby spuštějící Activity poznala, která že spuštěná Activity jí zrovna teď něco vrátila, nastaví se nějaký  requestCode.)

        else{
            Intent i = new Intent(this, AddNoteActivity.class);
            startActivityForResult(i, REQUEST_ADD_NOTE);
        }
    }

    private static final int REQUEST_ADD_NOTE = 0;

V onActivityResult  zjistíme, zdali vše proběhlo v pořádku (pokud ne – uživatel třeba ukončil AddNoteActivity tlačítkem zpět , ignorujeme to), a pokud ano, zavoláme metodu onAddNote, jíž musíme kvůli AddNoteFragment.OnAddNoteListener stejně implementovat.

    @Override
    protected void onActivityResult (int requestCode, int resultCode, Intent data){
        if(requestCode == REQUEST_ADD_NOTE){
            if(resultCode != RESULT_OK)
                return;

            String title = data.getStringExtra(AddNoteActivity.EXTRA_TITLE);
            String text = data.getStringExtra(AddNoteActivity.EXTRA_TEXT);

            onAddNote(title, text);
        }

        super.onActivityResult(requestCode, resultCode, data);
    }

A nakonec, dnes už opravdu poslední kód, metoda onAddNote. Ta zkusí poznámku uložit.

    public void onAddNote(String title, String text) {
        Notes notes = new Notes(this);
        long id = notes.insertNote(title, text);

Pokud se to povede, získá NotesListFragment a donutí ho obnovit své ListView,

        if(id > -1){
            ((NotesListFragment) getSupportFragmentManager().findFragmentById(
                    R.id.notes_list)).updateList();

a pokud je ještě k tomu uživatel na tabletu, rovnou mu nově vytvořenou poznámku zobrazí.

            if(dualPane)
                showNote(id);
        }

Pokud se uložení z nějakého důvodu nepovede, uživatel na to bude upozorněn.

        else{
            Toast.makeText(this, R.string.note_not_added, Toast.LENGTH_LONG).show();
        }
    }
}

Tím jsme dokončili NotepadActivity, a pokud všechny Activity správně zapíšeme do manifestu, měli bychom mít funkční spustitelný poznámkový blok, který se umí přizpůsobit tabletům.

Procvičování

Zkuste přidat možnost upravit poznámku. Zda jako formulář použijete upravený AddNoteFragment, anebo si vytvoříte nový, je pro účely procvičování jedno. Pokud se naskytne nějaký problém, rád porádím, budu-li vědět.

Závěr

Dnes jsme se naučili pracovat s SQLite databází, používat Fragmenty a k tomu jsme si trochu rozšířili znalosti některých už dříve představených tříd. Po přečtení dnešního článku jste už schopni vytvořit poměrně složitou funkční aplikaci. Zdrojové kódy dnešní aplikace si můžete stáhnout tady.

Příště se podíváme na preference, tedy druhou možnost ukládání dat v Androidu, a naučíme se, jak jednoduše vytvořit nastavení. A co dál? Mám vymyšlená témata ještě na několik dalších článků, ale znovu apeluji na vás, čtenáře, abyste si řekli, o co máte zájem. S jakými problémy jste se při vlastních experimentech setkali, co byste ještě chtěli umět. Bude to pro mě velmi cenná inspirace.

Tip na konec

Naučte se rovnou přemýšlet ve fragmentech. Když si budete rozmýšlet novou androidí aplikaci, rovnou si ji v duchu rozdělte na kousky, které půjdou umístit do Fragmentů. Fragmenty nejenže umožňují celkem jednoduše a rychle vyrobit přizpůsobivý layout, ale i dělí aplikaci na menší a jednodušší části a ve výsledku tak pomáhají vytvořit čistší návrh.

Komentáře

Subscribe
Upozornit na
guest
34 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
razor

Ahoj, vyvíjím miniaplikaci využívající sensory, tak proč ne třeba článěk o sensorech? ;-)

j3nda

ahoj,
me by zajimal vyvoj graficky ladenych aplikaci jako napr:
(pixely, pozicovani, fadeIn/Out, …, prip. JOGL/OpenGL)
– mapy ~ opengl 2d/3d
– hry ~ opengl 2d/3d

Rado2

ja som si myslel, že sa za chvíľu naučím OpenGL ES pre android, ale keď som zistil aké zložité je vykresliť obyčajný trojuholník, tak som pochopil, že bez nejakého enginu to vôbec nemá zmysel, teda pokiaľ človeka nebavia napĺňanie a konverzie bufferov a písanie si vlastných algoritmov na všetko, vrátane najjednoduhšieho osvetlenia, atď.

j3nda

no pro opengl je frameworku nekolik (alespon co si z dob c++ pamatuju – nevim jak je na tom android. v jave jsem objevil JOGL). a predpokladam, ze se i neco na 3d najde nekde na sourceforge anebo freshmeatu.

Matěj Konečný

Předně se omlouvám, že píšu nepřihlášen – přihlášenému mi to Zdroják vůbec nedovolí.

Pokud se budeme věnovat grafice, bude to maximálně na úrovni „Získej Canvas, ten má tyhle kreslící metody:” S OpenGL jsem jednak vůbec nikdy nedělal, takže rozhodně nejsem ten správný na to, abych o něm psal, jednak by vysvětlování OpenGL na Androidu bylo na další seriál. Stejně tak se nebudeme věnovat RenderScriptu.

Obecně je tento seriál mířen spíše na základy vývoje pro Android, čímž mám na mysli to, že neplánuju probírat věci, které skoro nikdo nikdy nepoužije, nikoliv, že by se vůbec neprobíralo do hloubky.

Díky za super články, konečně někdo, kdo neskončil u nastavení vývojového prostředí :o)

Určitě bych uvítal možnosti komunikace se serverem a především push notifikace

Matěj Konečný

Ahoj,

komunikace se serverem bude minimálně po úroveň toho, jak správně na Androidu vytvářet asynchronní požadavky tak, abychom neblokovali UI vlákno.

Push notifikace mi přijdou jako ne až tak důležitá vlastnost, takže s nimi v tuhle chvíli do seriálu nepočítám. Pokud bude dostatečný zájem, může to někdy vyjít jako samostatný článek tady na Zdrojáku nebo u mě na blogu (až založím :D – pracuju na tom).

Co sis Ty všechno představoval pod pojmem komunikace se serverem? Budu moc rád za inspiraci :).

Ahoj,

koukám, že na mailové notifikace se tady spolehnout nemůžu.

Ideální představa je samozřejmě vzdálené napojení databáze (mssql, mysql), ale vzhledem k tomu, že jsem žádnou rozumnou knihovnu nenašel, je to asi trošku moc.
Možná, pokud máš v rukávu nějaké hezké řešení pomocí api nad databází- s tím, že by se mohl článek víc věnovat bezpečnosti. Přece jen, většina tutorialů posílá plain text atd. – těžky říct, zda to už není příliš za rozsahem serialu

rogisoft

Zdravím!
Předně díky za super seriál. S androidem začínám (jinak 20-letá praxe programování pro platformy MS v Delphi), nainstaloval jsem si Eclipse, Android SDK 4.1 (API 16) a postupuji podle tohoto seriálu.
Všechny příklady dělám pod verzí 4.1 a nakonec i fungují. :-)
Když však přidám do View EditText, jedno jestli přes XML nebo vizuálně myší, tak mi vyběhne chyba:

java.util.Lin­kedHashMap.el­dest()Ljava/u­til/Map$Entry;
Exception details are logged in Window > Show View > Error Log

Když Layout přepnu na API 15, tak chyba zmizí a aplikace je OK. Všechny Layouty, kde je EditText, mám tak v nižší verzi API 15 ostatní v API 16.
Faktem je, že aplikace nakonec v AVD běží normálně bez problémů.
Nevíte někdo v čem je problém? Díky

Ještě jeden dotaz. Jak si správně vytvořit AVD emulující tablet? Použil jsem API 16 a skin VXGA800-7in. Při pokusu o start mi AVD zatuhne a musím ho ukončit hlašením Windows (mám Win7).
Taky předem díky za odpověď.

Matěj Konečný

Ahoj,
kde je problém, opravdu nevím. Zkoušel jsem použít EditText na Jelly Bean, ale problém se mi žádný neobjevil. Můžeš prosím ukázat, jaký kód Ti působí problémy? Co se AVD emulujícího tablet týče, zkus nastavit rozlišení tak, aby bylo širší než vyšší. A zároveň dostatečně velké.

phoose

Ahoj, po dlouhé době mám konečně čas se zase vrátit k tomuto seriálu. Obsahuje spousty užitečných informací a jednotlivé postupy jsou pěkně vysvětlené. Mám ale s tímto dílem jeden drobný problém. Stáhl jsem si ukázkový zdrojový kód, abych si napřed vyzkoušel, jak se aplikace bude ve výsledku chovat. Ten jsem normálně spustil a fungoval. Pak jsem tedy začal celý program psát podle článku, abych se zase něčemu novému přiučil. Tady jsem ale narazil na problém. Kdykoliv se v článku vyskytuje nějaký zdrojový kód (nebo výpis XML atd.), bylo by možné třeba pod tento výpis uvést, do jakého souboru tento kód patří? Začal jsem být zmatený hned v odstavci s nadpisem NotesListFragment. Je tam XML výpis layoutu, ale nikde jsem nenašel, jak se soubor s tímto layoutem má jmenovat. Vzhledem k tomu, že se z jeho názvu vytváří jeho ID v programu, jedná se podle mě o dost podstatnou informaci. Ono ten název souboru možná jde vypátrat ze zdrojových kódů, ale je to zdlouhavé a pokud je tam layoutů víc, tak už to není tak snadné. Bylo by tedy možné výpisy kódů doplnit o název souboru, ze kterých jsou? Díky :-)

phoose

Chápu, u těch tříd to je docela zbytečné, tam je to docela jasné co kam patří. Ale třeba u těch layoutů by se to docela hodilo, protože zrovna v tomto díle je jich docela dost, a proto jsem se v nich pomalu přestal orientovat. Občas ne všemu napoprvé úplně rozumím a v takovém případě jen slepě opíšu kousek kódu s tím, že ho pochopím později, až když ho mám napsaný. A když z kódu třeba nepoznám, že zrovna tento layout je R.layout.neco, tak mi to pak nebude fungovat jak má :-)
Případně mě napadá ještě jiný způsob. Napřed dát do textu výpis kódu, kde se s daným layoutem pracuje a až pak vypsat ten layout. V takovém případě by bylo jasné, že layout zrovna s tímto jménem se bude vytvářet a nebude potřeba v článku přeskakovat nahoru a dolů a hledat ty správné názvy :-)

KiN

Dobrý den,

v článku (mimochodem, děkuji za celou sérii) uvádíte, ze SQLite je implementována v Android OS. Pokud tedy vyvíjím na Windows XP, předpokládám, že je součástí AVD. … Při pokusu o běh této aplikace na AVD se mi při stisku tlačítka „Přidat poznámku“ objevil FATAL ERROR, tak hledám, co by mohlo být toho příčinou a existenci SQLite tímto pokládám za vyřešenou.

Děkuji předem za potvrzení či vyvrácení mého předpokladu.

S pozdravem, KiN

KiN

Dobrý den,

děkuji za brzkou odpověď. … Chybu jsem už našel, resp. bylo jich tam víc:
– v Manifestu nebyly Activity.
– dva layouty soupeřily o stejné ID.

Nicméně mě napadla otázka: Bylo by možné, aby v layoutu add_note ten <EditText android:id=“@+id/­text“ … /> zabíral jen určitý počet řádků … například 4? Pokud ano, jak toho docílit?

S pozdravem a přáním vytrvat v osvětě co nejdéle,
KiN

KiN

Tak už jsem na víceřádkovost přišel:
<EditText android:id=“@+id/­text“
android:layou­t_width=“match_pa­rent“
android:layou­t_height=“wrap_con­tent“
android:hint=“@strin­g/text“
android:input­Type=“textMul­tiLine“
android:lines=“5″ />

Jirka

Původně mi aplikace při otočení displeje (v okamžiku prohlížení detailu poznámky) padala. Ukázalo se, že stačilo vyřešit jinak předávání id – nevyužívat k tomu konstruktor SingleNoteFrag­mentu, ale metodu setArguments, konkrétně v metodě NotepadActivi­ty.showNote() a SingleNoteActi­vity.onCreate().
Přidání konstruktoru s argumenty se u fragmentů nedoporučuje, konkrétně Eclipse mě před tím varovalo (a díky tomu jsem tu chybu odstranil).
Pro úplnost ještě dodám, že není dobré používat pro předání parametrů nějakou vlastní public metodu fragmentu, protože toto řešení přestává fungovat při znovuvytváření (?) fragmentu (při to nastalo právě při otočení displeje). SetArguments() toto právě řeší.

jirkam01

Ten odkaz jsem nenašel dostatečně blízko. Aspoň jsem si na to přišel sám a budu si to pořádně pamatovat :) Každopádně díky za vstřícnou reakci.

jirkam01

Narazil jsem teď na oficiální návod na fragmenty: http://developer.android.com/training/basics/fragments/index.html
Je tam zásadní rozdíl oproti tomuto článku v tom, že tam si vystačí s jedinou aktivitou. Takže SingleNoteActivity a AddNoteActivity vlastně nejsou potřeba, i na úzkém telefonu bude všechno řídit NotepadActivity. Bude mít layout, kam se bude vkládat fragment podle potřeby (takže jedna aktivita, jeden layout a do něj dynamicky podle situace vkládáno několik různých fragmentů: AddNoteFragment, SingleNoteFragment a NotesListFragment). Viz ten návod.
Má nějakou výhodu vytvářet takto víc aktivit?

Kubicon

Při zobrazení poznámky a otočení telefonu nebo vysunutí hardwarové klávesnice (restart aplikace, znova proběhne onCreate) aplikace padá. Jak to?

Liner

Ahoj. Android vidim poporvý v životě a javu tak lehce před několika lety, takže možná až moc hloupá otazka. Ale…v NoteListFragment, když chceme, aby reagoval na kliknuti na položku a přepišeme onListItemClick, proc musime zavadet nejaky listener? nešlo by to vyrešit jednodušeji? Mě přijde, že je tam jakoby navic. Patři tam asi určitě, když tam je… ale nejde mito uplně hlavy. Stejně tak proč je nakonci kodu public static interface OnNoteClickedListener {
public void onNoteClicked(long id);
}

a jeste vyznam toho interface v deklaraci tehle metody.

Možná až moc zmatenej dotaz, ale tohle jediny mi nejde nejak moc do hlavy.

Předem díky za odpověd

Petr

Ahoj, nevím, jestli to tu ještě někdo vůbec čte, ale kdyby náhodou… Nefunguje mi (pomocí emulátoru) vícesloupcový layout pro systém verze 2.2. Vždycky se zobrazí jednosloupcový layout. Zkoušel jsem ten adresář „layout-w660dp“ různě přenastavovat, ale nic se nechytlo. Na verzi 4.0.x mi to běželo v pohodě. Já měl za to, že díky té support knihovně bude fungovat i toto? V čem by mohl být problém? Nebo to skutečně na 2.2 nefunguje?

Petr

A opět si odpovím sám :)
Modifikátory typu w600dp jsou známé až od verze 3.2 (API level 13). Pro nižší verze funguje např. values-large-land.

Tomáš

Jak by se dala jednoduše změnit barva ListView(textu)? textColor nereaguje

Tonda

Tutorial je na nic.. Jako by to psal nejvetší nerd na světe v podstatě jen pro sebe a nepočital s tím že to někdo čte. Napřiklad neni vysvětleno do jakeho souboru kod psat. Je tam kus kodu a pod nim se mluvi uplně o něčem jinem. Není to prostě krok po kroku.. Ale jiank snaha byla

Vitek

Ve fragmentu NotesListFragment chybí deklarace konstanty:
private static final int MENU_DELETE_ID = 0;
…když by to někoho mátlo :-)

Majky

je niekde zdroják z tutorialu fragmenty?

Martin Hassman

Odkaz na něj je v textu na konci článku.

Pepin

Caused by: android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.example.notepad.SingleNoteFragment: make sure class name exists, is public, and has an empty constructor that is public

Když otočím zařízení,kde je již načten nadpis i poznámka (dva panely) a má dojít i k otočení aplikace, tak spadne na notepad.SingleNoteActivity.onCreate(SingleNoteActivity.java:12) na tom super onCreate(…..
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // tady to spadne při otočení zařízením
setContentView(R.layout.single_note_container);

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.