Tvorba aplikací pro Android

08

Ukládání dat do souboru

Veškeré změny, které jsme v naší ap­likaci pro­ved­li, se zatím nikam neukládají. Po vyp­nutí ap­likace začínáme znovu s původními daty. Nyní se naučíme ukládat data do souborů pro pozdější použití. Pro ukládání souborů použijeme serializaci dat. Jedná se o zabudovaný mech­anis­mus jazyka Java, který umožňuje převést ob­jekty Javy na pos­loup­nost byte a tu posléze uložit do souboru. Tento pos­tup je výhodný pro svou jedno­duc­host. Neumožňuje ale zpracování dat jinou ap­likací. Pro ex­port a možnost použití dat v jiné ap­likaci jsou výhodnější formáty XML či CSV.


Soubo­ry budeme ukládat do in­ter­ního úložiště mobil­ního zařízení. Pos­tup pro ukládání na SD kartu by byl o něco složitější, v tomto směru odkážeme čtenáře na studium dalších pra­m­enů, například kapitolu Sav­ing Files na ser­veru De­veloper­.Android.com.


Otevření souboru pro zápis

Pro otevření souboru v in­ter­ním úložišti můžeme použít metodu openFileOutput třídy Context. Tuto metodu dědí třída Activity. Jako para­metr předáme metodě openFileOutput název souboru (jako tex­tový řetězec) a režim přístupu. Režim přístupu zadáme pomocí číselné konstan­ty de­finované ve třídě Context. Pro in­terní úložiště se běžné používá režim přístupu MODE_PRIVATE.

FileOutputStream fos;
fos = context.openFileOutput(Konstanty.nazevSouboru(), Context.MODE_PRIVATE);

Serializace datových ob­jektů

Ob­jekty, které chceme přip­ravit pro ukládání do souboru pro­střed­nictvím serializace musí im­plemen­tovat rozhraní java.io.Serializable. Stejně tak musí rozhraní Serializable im­plemen­tovat i všechny třídy ob­jektů, ze kterých je serializovaný prvek složen. Naštěstí většina běžně používaných tříd Javy rozhraní im­plemen­tuje.


Pokud je třída serializovatelná, stačí pro uložení ob­jektů této třídy do souboru vytvořit ObjectOutputStream, kterému jako para­metr předáme FileOutputStream, vytvořený metodou Context.openFileOutput (...). Následně použijeme metodu writeOb­ject(…) - třídy ObjectOutputStream.


Nezapomeň­te výstupní pro­udy po použití uzavřít metodou close()!

ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(data);
oos.close();

Načtení serializovaných dat

Pro načtení již uložených dat použijeme opačný pos­tup. Vytvoříme FileInputStream (opět pomocí třídy Context), vytvoříme ObjectInputStream a serializované ob­jekty převedeme na ob­jekty Javy metodou readObject(…). Výs­tupem jsou původní data. Náv­ratovým datovým typem metody readObject () je třída Object, je tedy třeba následně provést přetypování na správný datový typ. Chceme-li být op­atrní, nejprve zkontrolujeme správnost dat a možnost přetypování operátorem instaceof:

FileInputStream fis;
ObjectInputStream ois;
fis = context.openFileInput(nazevSouboru);
ois = new ObjectInputStream(fis);
Object dataObject = ois.readObject();
ois.close();
if (! (dataObject instanceof PuvodniTridaDat)) {
    throw new IOException("Nesprávný obsah souboru, nepodařilo se dekódovat obsah souboru!");
}
PuvodniTridaDat data = (PuvodniTridaDat) vstup; // Přetypování na původní třídu.

Přehled souborů, mazání souboru

Pokud vytvoříme více souborů, může se hodit je­jich sez­nam. Sez­nam souborů, které ap­likace v in­ter­ním úložišti vytvořila, získáme metodou fileList() třídy Context. Sez­nam získáme jako pole řetězců. Pro výpis můžeme použít násle­dující kód:

String seznamSouboru = "Složka: " + this.getFilesDir() + "\n" +
        "Soubory aplikace: ";
for (String nazevSouboru : this.fileList()) {
    seznamSouboru += nazevSouboru + ", ";
}
System.out.println(seznamSouboru);

Pro smazání souboru, který již nepot­řebujeme, můžeme použít metodu deleteFile(nazev) třídy Context:

this.deleteFile(nazevSouboru);

Příklad Můj pestrý život – ukládání dat

Doplňme předchozí příklad o ukládání dat do souboru. Většinu kódu přidáme do třídy Data. Nesmíme zapomenout im­plemen­tovat rozhraní java.io Serializable u tříd DataZáznam. Následně upravíme kód metody nactiData () ve třídě Data tak, aby při vytvoření okna načetla data ze souboru.

  1. Im­plemen­tujte rozhraní java.io.Serializable ve třídách DataKlientuKlient:

  2. public class Data extends Observable implements Observer, Serializable {
    
    

    public class Zaznam extends Observable  implements Serializable {
    
    

  3. Dále upravíme metodu nactiData() a přidáme metodu ulozData() ve třídě Data. Ty budou provádět samotné ukládání a načítání dat. Pro zařazení souboru do správného kon­textu (přiřazení k naší ap­likaci) potřebují obě metody jako para­metr dos­tat aktuální kon­text ap­likace (in­stan­ci třídy MainActivity).

  4. public void nactiData(Context context) {
        // Zatím vytvořím prázdnou kolekci záznamů. Následně ji zkusím přepsat daty, načtenými
        //  ze souboru, pokud soubor existuje.
        this.data = new ArrayList<Zaznam>();
        // Pokusím se načíst data ze souboru:
        try {
            FileInputStream fis = context.openFileInput(Konstanty.nazevSouboru());
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object vstup = ois.readObject();
            if (vstup instanceof ArrayList) {
                this.data = (ArrayList<Zaznam>) vstup;
            }
        } catch (FileNotFoundException ex) {
            Toast.makeText(context, "Soubor zatím neexistuje!", Toast.LENGTH_SHORT).show();
        } catch (IOException ex) {
            Toast.makeText(context, "Chyba při čtení vstupního souboru: "+ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
        } catch (ClassNotFoundException ex) {
            Toast.makeText(context, "Nesprávný formát vstupního souboru: "+ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
        }
        // Pokud je kolekce prázdná, naplním ji vzorovými daty. To proto, že zatím aplikace neumí
        //  přidat nové záznamy sama. Později tuto část vypustíme:
        if (this.data.isEmpty()) {
            this.data.add(new Zaznam("Programování pro Android", "Tvorba aplikací pro Android.", true));
            this.data.add(new Zaznam("Hra na kytaru", "Základní akordy na kytaru.", false));
            this.data.add(new Zaznam("Terra Mystica", "Naučil jsem se novou deskovou hru.", false));
        }
        // Nastav všem záznamům pozorovatele, aby se jejich změny hlásily:
        for (Zaznam z : this.data) {
            z.addObserver(this);
        }
        // Upozorním na změnu dat pozorovatele:
        this.setChanged();
        this.notifyObservers();
    }
    
    
    public void ulozData(Context context) {
        try {
            FileOutputStream fos = context.openFileOutput(Konstanty.nazevSouboru(), Context.MODE_PRIVATE);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(this.data);
        } catch (IOException ex) {
            Toast.makeText(context, "Chyba při zápisu do souboru: "+ ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
        }
    }
    

  5. Co se týče volby názvu souboru, doporučujeme podobná nas­tav­ení ukládat do jedné třídy s názvem Konstanty. Všechna nas­tav­ení tak budou po hromadě a lze je v budouc­nu jedno­duše ukládat do souboru či jiným způsobem zpřís­tupnit uživateli či dalším vývojářům.

  6. public class Konstanty {
        public static String nazevSouboru() { return "data.dat"; }
    }
    

  7. V pos­ledním kroku upravíme metodu onCreate(...) třídy MainActivity tak, aby metodě nactiData() předala kon­text.

  8. protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Vytvoření a inicializace třídy Data:
        this.zaznamy = new Data();
        this.zaznamy.nactiData(this);
        // Registrace jako pozorovatel -- chceme být informování o změnách v datech
        this.zaznamy.addObserver(this);
        // Vytvoření ListView a propojení s daty:
        this.seznam = (ListView) this.findViewById(R.id.lv_seznam);
        this.seznam.setAdapter(new ArrayAdapter<Zaznam>(this, android.R.layout.simple_list_item_1, this.zaznamy.getData()));
        this.seznam.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> zvolenyView, View polozka, int poziceVAdapteru, long cisloRadkuUvnitrView) {
                zvolenaPolozka(zvolenyView, polozka, poziceVAdapteru, cisloRadkuUvnitrView);
            }
        });
    }
    

  9. Poz­namenej­me, že z hledis­ka oddělení logiky ap­likace od zob­razovací části (návrhový vzor MVC apod.) by bylo vhodnější nevypisovat chybové hlášení přímo ve třídě Data. Bylo by vhodnější pouze vygenerovat vlastní vyjímku, která by nesla in­for­maci o nastálé akci. Samostné roz­hodnutí, zda uživateli in­for­maci o chybě zob­razit for­mou vys­kakovacího okna či jinak, by měla učinit hlavní ak­tivita, nikoli datová třída Data. Im­plemen­taci tohoto řešen ponec­háme jako cvičení z výjimek v Javě (ex­cep­tion) pro čtenáře.

  10. Zbývá doplnit možnost data do souboru uložit (zavolat metodu ulozData třídy Data). Můžeme přidat tlačítko a volat metodu z jeho kódu, nebo přidáme hlavní menu. Pos­tup pro přidání menu popíšeme v další kapitole.

Shrnutí

  1. Vstupní a výstupní proud pro práci se soubo­ry získáme metodami openFileOutput resp. openFileInput třídy Context.

  2. Pro jedno­duché ukládání dat můžeme použít serializaci. Ukládáné třídy musí im­plemen­tovat rozhraní Serializable a pro zápis a čtení použijeme třídy ObjectOutputStream a ObjectInputStream.

  3. Sez­nam souborů ap­likace a mazání souborů použijeme třídami fileList()deleteFile() třídy Context.